784 lines
22 KiB
Markdown
784 lines
22 KiB
Markdown
|
|
# SAM API 보안 가이드
|
||
|
|
|
||
|
|
## 개요
|
||
|
|
|
||
|
|
SAM API는 다층 보안 구조를 통해 무단 접근과 악의적 공격으로부터 시스템을 보호합니다.
|
||
|
|
|
||
|
|
**최종 업데이트:** 2025-12-26
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 보안 아키텍처
|
||
|
|
|
||
|
|
### 다층 방어 구조 (Defense in Depth)
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────┐
|
||
|
|
│ Layer 1: Nginx (L7 Application Layer) │
|
||
|
|
│ - 악의적 경로 패턴 차단 │
|
||
|
|
│ - 의심스러운 User-Agent 차단 │
|
||
|
|
│ - Rate Limiting (Nginx 레벨) │
|
||
|
|
└─────────────────────────────────────────────────┘
|
||
|
|
↓
|
||
|
|
┌─────────────────────────────────────────────────┐
|
||
|
|
│ Layer 2: Laravel Rate Limiting │
|
||
|
|
│ - IP 기반 속도 제한 (10회/분) │
|
||
|
|
│ - API Key 없는 요청 차단 │
|
||
|
|
└─────────────────────────────────────────────────┘
|
||
|
|
↓
|
||
|
|
┌─────────────────────────────────────────────────┐
|
||
|
|
│ Layer 3: API Key 검증 (글로벌 미들웨어) │
|
||
|
|
│ - 모든 요청 API Key 필수 │
|
||
|
|
│ - 화이트리스트 라우트 제외 │
|
||
|
|
│ - 보안 로그 자동 기록 │
|
||
|
|
└─────────────────────────────────────────────────┘
|
||
|
|
↓
|
||
|
|
┌─────────────────────────────────────────────────┐
|
||
|
|
│ Layer 4: Sanctum 토큰 인증 │
|
||
|
|
│ - Bearer 토큰 검증 │
|
||
|
|
│ - 사용자 컨텍스트 설정 │
|
||
|
|
│ - 테넌트 격리 │
|
||
|
|
└─────────────────────────────────────────────────┘
|
||
|
|
↓
|
||
|
|
┌─────────────────────────────────────────────────┐
|
||
|
|
│ Layer 5: 권한 검증 (Permission Check) │
|
||
|
|
│ - 메뉴 기반 권한 체크 │
|
||
|
|
│ - Role 기반 접근 제어 │
|
||
|
|
└─────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Layer 1: Nginx 보안
|
||
|
|
|
||
|
|
### 악의적 경로 패턴 차단
|
||
|
|
|
||
|
|
**위치:** `docker/nginx/nginx.conf`
|
||
|
|
|
||
|
|
```nginx
|
||
|
|
# 경로 탐색 공격 차단
|
||
|
|
if ($request_uri ~* "(\.\.\/|\.\.\\|etc\/passwd|\.env|\.git|\.htaccess|\.sql|@fs\/)") {
|
||
|
|
return 403;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**차단 패턴:**
|
||
|
|
- `../`, `..\` - 디렉토리 탐색 공격
|
||
|
|
- `etc/passwd` - 시스템 파일 접근 시도
|
||
|
|
- `.env` - 환경 변수 파일 접근
|
||
|
|
- `.git`, `.htaccess`, `.sql` - 민감한 파일 접근
|
||
|
|
- `@fs/` - Vite 경로 탐색 공격
|
||
|
|
|
||
|
|
**응답:** 403 Forbidden
|
||
|
|
|
||
|
|
### 의심스러운 User-Agent 차단
|
||
|
|
|
||
|
|
```nginx
|
||
|
|
# 보안 스캔 도구 차단
|
||
|
|
if ($http_user_agent ~* "(sqlmap|nikto|nmap|masscan|metasploit|nessus)") {
|
||
|
|
return 403;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**차단 도구:**
|
||
|
|
- sqlmap - SQL 인젝션 스캐너
|
||
|
|
- nikto - 웹 서버 스캐너
|
||
|
|
- nmap - 포트 스캐너
|
||
|
|
- masscan - 대량 포트 스캐너
|
||
|
|
- metasploit - 침투 테스트 프레임워크
|
||
|
|
- nessus - 취약점 스캐너
|
||
|
|
|
||
|
|
**응답:** 403 Forbidden
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Layer 2: Rate Limiting
|
||
|
|
|
||
|
|
### Laravel Rate Limiter
|
||
|
|
|
||
|
|
**위치:** `app/Http/Middleware/ApiRateLimiter.php`
|
||
|
|
|
||
|
|
```php
|
||
|
|
// IP 기반 속도 제한
|
||
|
|
$key = 'api-key-attempts:' . $request->ip();
|
||
|
|
|
||
|
|
if ($this->limiter->tooManyAttempts($key, 10)) {
|
||
|
|
return response()->json([
|
||
|
|
'message' => 'Too many attempts. Please try again later.',
|
||
|
|
'retry_after' => $seconds,
|
||
|
|
], 429);
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->limiter->hit($key, 60); // 1분 동안 유지
|
||
|
|
```
|
||
|
|
|
||
|
|
**설정:**
|
||
|
|
- **제한:** 10회/분 (IP별)
|
||
|
|
- **대상:** API Key 없는 요청
|
||
|
|
- **유지 시간:** 60초
|
||
|
|
- **응답 코드:** 429 Too Many Requests
|
||
|
|
|
||
|
|
**로그:**
|
||
|
|
```php
|
||
|
|
Log::warning('API Rate Limit Exceeded', [
|
||
|
|
'ip' => $request->ip(),
|
||
|
|
'uri' => $request->getRequestUri(),
|
||
|
|
'retry_after' => $seconds,
|
||
|
|
]);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Layer 3: API Key 인증
|
||
|
|
|
||
|
|
### 글로벌 미들웨어
|
||
|
|
|
||
|
|
**위치:** `bootstrap/app.php`
|
||
|
|
|
||
|
|
```php
|
||
|
|
$middleware->append(ApiRateLimiter::class); // 1. Rate Limiting
|
||
|
|
$middleware->append(ApiKeyMiddleware::class); // 2. API Key 검증
|
||
|
|
```
|
||
|
|
|
||
|
|
**실행 순서:** Rate Limiting → API Key 검증
|
||
|
|
|
||
|
|
### API Key 검증 로직
|
||
|
|
|
||
|
|
**위치:** `app/Http/Middleware/ApiKeyMiddleware.php`
|
||
|
|
|
||
|
|
```php
|
||
|
|
// 1. 화이트리스트 체크
|
||
|
|
$publicRoutes = [
|
||
|
|
'api/v1/login',
|
||
|
|
'api/v1/signup',
|
||
|
|
'api/v1/register',
|
||
|
|
'api/v1/refresh',
|
||
|
|
'api/v1/debug-apikey',
|
||
|
|
'api-docs', // Swagger UI
|
||
|
|
'api-docs/*', // Swagger 하위 경로
|
||
|
|
'docs/api-docs.json', // Swagger JSON
|
||
|
|
'up', // Health check
|
||
|
|
];
|
||
|
|
|
||
|
|
// 2. API Key 검증
|
||
|
|
$apiKey = $request->header('X-API-KEY');
|
||
|
|
$validApiKey = DB::table('api_keys')
|
||
|
|
->where('key', $apiKey)
|
||
|
|
->where('is_active', true)
|
||
|
|
->exists();
|
||
|
|
|
||
|
|
// 3. 보안 로그 기록
|
||
|
|
if (!$validApiKey) {
|
||
|
|
Log::warning('Unauthorized API access attempt', [
|
||
|
|
'ip' => $request->ip(),
|
||
|
|
'uri' => $request->getRequestUri(),
|
||
|
|
'method' => $request->method(),
|
||
|
|
'user_agent' => $request->userAgent(),
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 화이트리스트 (인증 제외 라우트)
|
||
|
|
|
||
|
|
**공개 엔드포인트:**
|
||
|
|
- `api/v1/login` - 로그인
|
||
|
|
- `api/v1/signup` - 회원가입
|
||
|
|
- `api/v1/register` - 테넌트 등록
|
||
|
|
- `api/v1/refresh` - 토큰 갱신
|
||
|
|
- `api/v1/debug-apikey` - API Key 디버깅
|
||
|
|
- `api-docs/*` - Swagger UI
|
||
|
|
- `docs/api-docs.json` - Swagger JSON
|
||
|
|
- `up` - Health check
|
||
|
|
|
||
|
|
**특징:**
|
||
|
|
- 와일드카드 지원 (`fnmatch()` 사용)
|
||
|
|
- 공개 라우트는 로깅 제외
|
||
|
|
- API Key 검증 스킵
|
||
|
|
|
||
|
|
### 보안 로그
|
||
|
|
|
||
|
|
**로그 레벨:**
|
||
|
|
- **Log::info** - 정상 API 요청
|
||
|
|
- **Log::warning** - 무단 접근 시도
|
||
|
|
|
||
|
|
**로그 내용:**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"ip": "213.136.76.215",
|
||
|
|
"uri": "/@fs/etc/passwd",
|
||
|
|
"method": "GET",
|
||
|
|
"user_agent": "Mozilla/5.0 ..."
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**민감 정보 제외:**
|
||
|
|
- `password`
|
||
|
|
- `password_confirmation`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Layer 4: Sanctum 토큰 인증
|
||
|
|
|
||
|
|
### 토큰 구조
|
||
|
|
|
||
|
|
**액세스 토큰:**
|
||
|
|
- 만료 시간: 2시간 (120분)
|
||
|
|
- 용도: API 호출 인증
|
||
|
|
- 형식: `{token_id}|{plain_text_token}`
|
||
|
|
|
||
|
|
**리프레시 토큰:**
|
||
|
|
- 만료 시간: 7일 (10080분)
|
||
|
|
- 용도: 액세스 토큰 갱신
|
||
|
|
- 특징: 일회성 사용 (사용 후 삭제)
|
||
|
|
|
||
|
|
### 토큰 갱신 플로우
|
||
|
|
|
||
|
|
```
|
||
|
|
1. 클라이언트: POST /api/v1/refresh
|
||
|
|
Headers: X-API-KEY, Authorization: Bearer {refresh_token}
|
||
|
|
|
||
|
|
2. 서버: 리프레시 토큰 검증
|
||
|
|
- 유효성 체크
|
||
|
|
- 만료 시간 체크
|
||
|
|
- 사용자 확인
|
||
|
|
|
||
|
|
3. 서버: 기존 리프레시 토큰 삭제
|
||
|
|
- 일회성 사용 보장
|
||
|
|
|
||
|
|
4. 서버: 새 토큰 발급
|
||
|
|
- 새 액세스 토큰 생성
|
||
|
|
- 새 리프레시 토큰 생성
|
||
|
|
|
||
|
|
5. 응답:
|
||
|
|
{
|
||
|
|
"access_token": "...",
|
||
|
|
"refresh_token": "...",
|
||
|
|
"expires_in": 7200,
|
||
|
|
"expires_at": "2025-11-13 21:30:00"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 토큰 만료 에러 처리
|
||
|
|
|
||
|
|
**위치:** `app/Exceptions/Handler.php`
|
||
|
|
|
||
|
|
```php
|
||
|
|
if ($exception instanceof AuthenticationException) {
|
||
|
|
$bearerToken = $request->bearerToken();
|
||
|
|
if ($bearerToken) {
|
||
|
|
$token = PersonalAccessToken::findToken($bearerToken);
|
||
|
|
if ($token && $token->expires_at && $token->expires_at->isPast()) {
|
||
|
|
return response()->json([
|
||
|
|
'success' => false,
|
||
|
|
'message' => __('error.token_expired'),
|
||
|
|
'error_code' => 'TOKEN_EXPIRED',
|
||
|
|
], 401);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**프론트엔드 처리:**
|
||
|
|
```javascript
|
||
|
|
if (response.error_code === 'TOKEN_EXPIRED') {
|
||
|
|
// 자동 리프레시 토큰으로 재발급
|
||
|
|
const newTokens = await refreshToken(refreshToken);
|
||
|
|
// 원래 요청 재시도
|
||
|
|
return retryRequest(originalRequest, newTokens.access_token);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Layer 5: 권한 검증 (Permission System)
|
||
|
|
|
||
|
|
### 권한 시스템 개요
|
||
|
|
|
||
|
|
SAM은 **Spatie Permission** 패키지를 기반으로 한 다층 권한 시스템을 사용합니다.
|
||
|
|
|
||
|
|
**3단계 권한 구조:**
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────┐
|
||
|
|
│ 1. 사용자 역할 권한 │
|
||
|
|
│ User → Role → Permissions │
|
||
|
|
│ (model_has_roles → role_has_perms) │
|
||
|
|
└────────────────────────────────────────┘
|
||
|
|
+
|
||
|
|
┌────────────────────────────────────────┐
|
||
|
|
│ 2. 사용자 직접 권한 │
|
||
|
|
│ User → Permissions │
|
||
|
|
│ (model_has_permissions) │
|
||
|
|
└────────────────────────────────────────┘
|
||
|
|
+
|
||
|
|
┌────────────────────────────────────────┐
|
||
|
|
│ 3. 부서 역할 권한 │
|
||
|
|
│ User → Department → Role → Perms │
|
||
|
|
│ (department_user → model_has_roles) │
|
||
|
|
└────────────────────────────────────────┘
|
||
|
|
↓
|
||
|
|
┌────────────────────────────────────────┐
|
||
|
|
│ UNION (중복 제거) │
|
||
|
|
│ → 최종 사용자 권한 목록 │
|
||
|
|
└────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
**특징:**
|
||
|
|
- 멀티테넌트 지원 (tenant_id로 격리)
|
||
|
|
- 메뉴 기반 세분화된 권한
|
||
|
|
- 다형성(Polymorphic) 구조로 유연한 확장
|
||
|
|
- Permission Override로 임시/긴급 권한 제어
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 권한 패턴 및 타입
|
||
|
|
|
||
|
|
**권한 명명 규칙:**
|
||
|
|
```
|
||
|
|
menu:{menu_id}.{permission_type}
|
||
|
|
```
|
||
|
|
|
||
|
|
**권한 타입 (7가지):**
|
||
|
|
|
||
|
|
| 타입 | 약자 | 설명 | 예시 |
|
||
|
|
|------|------|------|------|
|
||
|
|
| view | V | 조회 | 목록/상세 보기 |
|
||
|
|
| create | C | 생성 | 신규 데이터 등록 |
|
||
|
|
| update | U | 수정 | 기존 데이터 편집 |
|
||
|
|
| delete | D | 삭제 | 데이터 삭제 |
|
||
|
|
| approve | A | 승인 | 워크플로우 승인 |
|
||
|
|
| export | E | 내보내기 | Excel/PDF 다운로드 |
|
||
|
|
| manage | M | 관리 | 전체 관리 권한 |
|
||
|
|
|
||
|
|
**권한 예시:**
|
||
|
|
```
|
||
|
|
menu:1.view → 대시보드 보기
|
||
|
|
menu:2.create → 제품 생성
|
||
|
|
menu:2.update → 제품 수정
|
||
|
|
menu:2.delete → 제품 삭제
|
||
|
|
menu:3.approve → 주문 승인
|
||
|
|
menu:4.export → 재고 내보내기
|
||
|
|
menu:5.manage → 사용자 관리
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 권한 조회 로직
|
||
|
|
|
||
|
|
**구현 위치:**
|
||
|
|
- **API:** `app/Services/MemberService.php` - `getUserInfoForLogin()`
|
||
|
|
- **Admin:** `app/Filament/Resources/Users/Tables/UsersTable.php` - `getAccessibleMenusCount()`
|
||
|
|
|
||
|
|
**권한 조회 쿼리 구조:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
// 1. 사용자 역할 권한
|
||
|
|
$userRolePermissions = DB::table('model_has_roles')
|
||
|
|
->join('role_has_permissions', 'model_has_roles.role_id', '=', 'role_has_permissions.role_id')
|
||
|
|
->join('permissions', 'role_has_permissions.permission_id', '=', 'permissions.id')
|
||
|
|
->where('model_has_roles.model_type', User::class)
|
||
|
|
->where('model_has_roles.model_id', $userId)
|
||
|
|
->where('model_has_roles.tenant_id', $tenantId)
|
||
|
|
->where('permissions.name', 'like', 'menu:%.view')
|
||
|
|
->select('permissions.name');
|
||
|
|
|
||
|
|
// 2. 사용자 직접 권한
|
||
|
|
$userDirectPermissions = DB::table('model_has_permissions')
|
||
|
|
->join('permissions', 'model_has_permissions.permission_id', '=', 'permissions.id')
|
||
|
|
->where('model_has_permissions.model_type', User::class)
|
||
|
|
->where('model_has_permissions.model_id', $userId)
|
||
|
|
->where('model_has_permissions.tenant_id', $tenantId)
|
||
|
|
->where('permissions.name', 'like', 'menu:%.view')
|
||
|
|
->select('permissions.name');
|
||
|
|
|
||
|
|
// 3. 부서 역할 권한
|
||
|
|
$departmentRolePermissions = DB::table('department_user')
|
||
|
|
->join('model_has_roles', function ($join) {
|
||
|
|
$join->on('department_user.department_id', '=', 'model_has_roles.model_id')
|
||
|
|
->where('model_has_roles.model_type', '=', Department::class);
|
||
|
|
})
|
||
|
|
->join('role_has_permissions', 'model_has_roles.role_id', '=', 'role_has_permissions.role_id')
|
||
|
|
->join('permissions', 'role_has_permissions.permission_id', '=', 'permissions.id')
|
||
|
|
->where('department_user.user_id', $userId)
|
||
|
|
->where('department_user.tenant_id', $tenantId)
|
||
|
|
->where('permissions.name', 'like', 'menu:%.view')
|
||
|
|
->select('permissions.name');
|
||
|
|
|
||
|
|
// 4. 모든 권한 통합 (UNION + 중복 제거)
|
||
|
|
$allPermissions = $userRolePermissions
|
||
|
|
->union($userDirectPermissions)
|
||
|
|
->union($departmentRolePermissions)
|
||
|
|
->pluck('name')
|
||
|
|
->toArray();
|
||
|
|
```
|
||
|
|
|
||
|
|
**권한 파싱:**
|
||
|
|
```php
|
||
|
|
// menu:123.view → 메뉴 ID 123 추출
|
||
|
|
foreach ($allPermissions as $permName) {
|
||
|
|
if (preg_match('/^menu:(\d+)\.view$/', $permName, $matches)) {
|
||
|
|
$allowedMenuIds[] = (int) $matches[1];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Permission Override (우선순위 제어)
|
||
|
|
|
||
|
|
**시간 기반 권한 제어:**
|
||
|
|
|
||
|
|
```php
|
||
|
|
// permission_overrides 테이블
|
||
|
|
[
|
||
|
|
'tenant_id' => 1,
|
||
|
|
'model_type' => 'App\Models\Members\User',
|
||
|
|
'model_id' => 123,
|
||
|
|
'permission_id' => 456,
|
||
|
|
'effect' => 1, // 1=ALLOW, -1=DENY
|
||
|
|
'effective_from' => '2025-11-13 00:00:00',
|
||
|
|
'effective_to' => '2025-11-20 23:59:59',
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
**우선순위:**
|
||
|
|
1. **Override DENY** (-1) - 최우선 차단
|
||
|
|
2. **Override ALLOW** (1) - 명시적 허용
|
||
|
|
3. **Base Permission** - 역할/부서/직접 권한
|
||
|
|
|
||
|
|
**최종 권한 계산:**
|
||
|
|
```php
|
||
|
|
foreach ($allMenuPermissions as $permName) {
|
||
|
|
if (preg_match('/^menu:(\d+)\.view$/', $permName, $matches)) {
|
||
|
|
$menuId = (int) $matches[1];
|
||
|
|
|
||
|
|
// Override DENY 체크 (강제 차단)
|
||
|
|
if (isset($overrides[$permName]) && $overrides[$permName]->effect === -1) {
|
||
|
|
continue; // 이 메뉴는 차단됨
|
||
|
|
}
|
||
|
|
|
||
|
|
// Override ALLOW 또는 기본 권한
|
||
|
|
if (
|
||
|
|
(isset($overrides[$permName]) && $overrides[$permName]->effect === 1) ||
|
||
|
|
in_array($permName, $basePermissions, true)
|
||
|
|
) {
|
||
|
|
$allowedMenuIds[] = $menuId;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**사용 사례:**
|
||
|
|
- **임시 권한 부여:** 프로젝트 기간 동안만 특정 메뉴 접근 허용
|
||
|
|
- **긴급 권한 차단:** 보안 사고 발생 시 즉시 권한 제거
|
||
|
|
- **휴가 기간 제한:** 특정 기간 동안 권한 자동 차단
|
||
|
|
- **시간대별 접근 제어:** 업무 시간에만 권한 부여
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 메뉴별 권한 매트릭스 뷰
|
||
|
|
|
||
|
|
**Admin 패널:** `http://admin.sam.kr/admin/permissions`
|
||
|
|
|
||
|
|
**테이블 구조:**
|
||
|
|
|
||
|
|
| 메뉴 ID | 메뉴명 | V (조회) | C (생성) | U (수정) | D (삭제) | A (승인) | E (내보내기) | M (관리) |
|
||
|
|
|---------|--------|----------|----------|----------|----------|----------|-------------|----------|
|
||
|
|
| 1 | 대시보드 | 홍길동, 김철수 | - | - | - | - | - | - |
|
||
|
|
| 2 | 제품 관리 | 홍길동, 김철수 | 홍길동 | 홍길동 | 관리자 | - | 김철수 | 관리자 |
|
||
|
|
| 3 | 주문 관리 | 전체팀 | 영업팀 | 영업팀 | 관리자 | 관리자 | 회계팀 | 관리자 |
|
||
|
|
|
||
|
|
**특징:**
|
||
|
|
- 각 Row = 하나의 메뉴
|
||
|
|
- 각 권한 타입별 Column에 해당 권한을 가진 사용자 목록 표시
|
||
|
|
- 사용자 배지에 마우스 오버 시 `user_id` 툴팁 표시
|
||
|
|
- 권한 없는 경우 `-` 표시
|
||
|
|
- 3가지 권한 소스 (역할/부서/직접) 모두 통합하여 표시
|
||
|
|
|
||
|
|
**구현 코드:**
|
||
|
|
```php
|
||
|
|
// app/Filament/Resources/Permissions/Tables/PermissionsTable.php
|
||
|
|
protected static function getUsersWithPermission(int $menuId, string $permissionType): string
|
||
|
|
{
|
||
|
|
$permissionName = "menu:{$menuId}.{$permissionType}";
|
||
|
|
|
||
|
|
// 3가지 권한 소스 UNION
|
||
|
|
$userIds = $userRoleQuery
|
||
|
|
->union($userDirectQuery)
|
||
|
|
->union($departmentRoleQuery)
|
||
|
|
->pluck('user_id')
|
||
|
|
->unique()
|
||
|
|
->toArray();
|
||
|
|
|
||
|
|
// 사용자 배지 HTML 생성
|
||
|
|
$users = User::whereIn('id', $userIds)->orderBy('name')->get();
|
||
|
|
foreach ($users as $user) {
|
||
|
|
$badges[] = sprintf(
|
||
|
|
'<span title="%s">%s</span>',
|
||
|
|
htmlspecialchars($user->user_id),
|
||
|
|
htmlspecialchars($user->name)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return implode(', ', $badges);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 권한 할당 방법
|
||
|
|
|
||
|
|
**1. 역할에 권한 할당:**
|
||
|
|
```php
|
||
|
|
$role = Role::findByName('영업팀', 'web');
|
||
|
|
$role->givePermissionTo([
|
||
|
|
'menu:2.view',
|
||
|
|
'menu:2.create',
|
||
|
|
'menu:3.view',
|
||
|
|
]);
|
||
|
|
```
|
||
|
|
|
||
|
|
**2. 사용자에게 직접 권한 할당:**
|
||
|
|
```php
|
||
|
|
$user = User::find(123);
|
||
|
|
$user->givePermissionTo('menu:5.manage');
|
||
|
|
```
|
||
|
|
|
||
|
|
**3. 부서에 역할 할당:**
|
||
|
|
```php
|
||
|
|
$department = Department::find(1);
|
||
|
|
$department->assignRole('영업팀');
|
||
|
|
```
|
||
|
|
|
||
|
|
**4. Permission Override 설정:**
|
||
|
|
```php
|
||
|
|
DB::table('permission_overrides')->insert([
|
||
|
|
'tenant_id' => 1,
|
||
|
|
'model_type' => User::class,
|
||
|
|
'model_id' => 123,
|
||
|
|
'permission_id' => 456,
|
||
|
|
'effect' => -1, // DENY
|
||
|
|
'effective_from' => now(),
|
||
|
|
'effective_to' => now()->addDays(7),
|
||
|
|
]);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 권한 체크 (Controller/Service)
|
||
|
|
|
||
|
|
**CheckPermission Middleware:**
|
||
|
|
```php
|
||
|
|
// routes/api.php
|
||
|
|
Route::get('/products', [ProductController::class, 'index'])
|
||
|
|
->middleware(['auth:sanctum', 'permission:menu:2.view']);
|
||
|
|
```
|
||
|
|
|
||
|
|
**서비스 레이어:**
|
||
|
|
```php
|
||
|
|
if (!auth()->user()->can('menu:2.create')) {
|
||
|
|
throw new \Exception(__('error.permission_denied'), 403);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**권한 확인 헬퍼:**
|
||
|
|
```php
|
||
|
|
// 단일 권한 체크
|
||
|
|
auth()->user()->can('menu:2.view');
|
||
|
|
|
||
|
|
// 여러 권한 중 하나라도 있으면
|
||
|
|
auth()->user()->hasAnyPermission(['menu:2.view', 'menu:2.manage']);
|
||
|
|
|
||
|
|
// 모든 권한 필요
|
||
|
|
auth()->user()->hasAllPermissions(['menu:2.view', 'menu:2.create']);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 보안 모니터링
|
||
|
|
|
||
|
|
### 로그 파일
|
||
|
|
|
||
|
|
**1. 보안 로그**
|
||
|
|
```bash
|
||
|
|
# 무단 접근 시도
|
||
|
|
tail -f storage/logs/laravel.log | grep "Unauthorized API access attempt"
|
||
|
|
|
||
|
|
# Rate Limit 초과
|
||
|
|
tail -f storage/logs/laravel.log | grep "API Rate Limit Exceeded"
|
||
|
|
```
|
||
|
|
|
||
|
|
**2. Nginx 로그**
|
||
|
|
```bash
|
||
|
|
# 접근 로그
|
||
|
|
tail -f /var/log/nginx/api.sam.kr_access.log
|
||
|
|
|
||
|
|
# 에러 로그 (403 차단)
|
||
|
|
tail -f /var/log/nginx/api.sam.kr_error.log
|
||
|
|
```
|
||
|
|
|
||
|
|
### 공격 패턴 분석
|
||
|
|
|
||
|
|
**자주 발생하는 공격:**
|
||
|
|
|
||
|
|
1. **경로 탐색 (Path Traversal)**
|
||
|
|
```
|
||
|
|
GET /@fs/etc/passwd
|
||
|
|
GET /../../../etc/passwd
|
||
|
|
GET /api/../.env
|
||
|
|
```
|
||
|
|
**대응:** Nginx에서 403 차단
|
||
|
|
|
||
|
|
2. **보안 스캔**
|
||
|
|
```
|
||
|
|
User-Agent: sqlmap/1.0
|
||
|
|
User-Agent: nikto/2.1.5
|
||
|
|
```
|
||
|
|
**대응:** Nginx User-Agent 필터링
|
||
|
|
|
||
|
|
3. **무차별 대입 (Brute Force)**
|
||
|
|
```
|
||
|
|
POST /api/v1/login (반복)
|
||
|
|
```
|
||
|
|
**대응:** Rate Limiting (10회/분)
|
||
|
|
|
||
|
|
4. **API Key 누락**
|
||
|
|
```
|
||
|
|
GET /api/v1/users (X-API-KEY 없음)
|
||
|
|
```
|
||
|
|
**대응:** 401 Unauthorized + 보안 로그
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 보안 체크리스트
|
||
|
|
|
||
|
|
### 개발 시
|
||
|
|
|
||
|
|
- [ ] 모든 API 엔드포인트에 `auth.apikey` 미들웨어 적용
|
||
|
|
- [ ] 민감한 정보 로깅 제외 (`password`, `password_confirmation`)
|
||
|
|
- [ ] FormRequest로 입력 검증
|
||
|
|
- [ ] SQL 인젝션 방지 (Eloquent ORM 사용)
|
||
|
|
- [ ] XSS 방지 (출력 시 이스케이핑)
|
||
|
|
- [ ] CSRF 보호 (Sanctum 자동 적용)
|
||
|
|
|
||
|
|
### 배포 전
|
||
|
|
|
||
|
|
- [ ] `.env` 파일 보안 설정 확인
|
||
|
|
- [ ] API Key 로테이션
|
||
|
|
- [ ] Nginx 보안 규칙 테스트
|
||
|
|
- [ ] Rate Limiting 임계값 검토
|
||
|
|
- [ ] HTTPS 인증서 유효성 확인
|
||
|
|
- [ ] 방화벽 규칙 설정
|
||
|
|
|
||
|
|
### 운영 중
|
||
|
|
|
||
|
|
- [ ] 매일 보안 로그 검토
|
||
|
|
- [ ] 주간 공격 패턴 분석
|
||
|
|
- [ ] 월간 토큰 만료 정책 검토
|
||
|
|
- [ ] 분기별 API Key 갱신
|
||
|
|
- [ ] 반기별 침투 테스트
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 보안 사고 대응
|
||
|
|
|
||
|
|
### 1단계: 즉시 조치
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# 1. 의심스러운 IP 차단 (Nginx)
|
||
|
|
# /etc/nginx/conf.d/blocked_ips.conf
|
||
|
|
deny 213.136.76.215;
|
||
|
|
|
||
|
|
# 2. Nginx 재시작
|
||
|
|
sudo systemctl reload nginx
|
||
|
|
|
||
|
|
# 3. 활성 세션 강제 종료
|
||
|
|
php artisan sanctum:prune-expired --hours=0
|
||
|
|
|
||
|
|
# 4. API Key 비활성화
|
||
|
|
UPDATE api_keys SET is_active = 0 WHERE key = 'suspicious_key';
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2단계: 로그 분석
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# 공격 패턴 분석
|
||
|
|
grep "213.136.76.215" /var/log/nginx/api.sam.kr_access.log
|
||
|
|
|
||
|
|
# 영향받은 엔드포인트 확인
|
||
|
|
grep "Unauthorized API access attempt" storage/logs/laravel.log | grep "213.136.76.215"
|
||
|
|
|
||
|
|
# 시간대별 요청 횟수
|
||
|
|
awk '{print $4}' /var/log/nginx/api.sam.kr_access.log | cut -d: -f1-2 | uniq -c
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3단계: 복구 및 강화
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# 1. 모든 사용자 비밀번호 초기화 (필요 시)
|
||
|
|
# 2. 새 API Key 발급
|
||
|
|
# 3. 토큰 만료 시간 단축 (임시)
|
||
|
|
# 4. Rate Limiting 임계값 강화
|
||
|
|
# 5. 추가 보안 규칙 적용
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## FAQ
|
||
|
|
|
||
|
|
### Q1. API Key는 어디서 발급받나요?
|
||
|
|
|
||
|
|
**A:** 관리자 패널(admin.sam.kr)에서 발급합니다.
|
||
|
|
```sql
|
||
|
|
-- api_keys 테이블 구조
|
||
|
|
id, tenant_id, name, key, is_active, created_at, updated_at
|
||
|
|
```
|
||
|
|
|
||
|
|
### Q2. Rate Limiting이 너무 엄격해요.
|
||
|
|
|
||
|
|
**A:** `ApiRateLimiter.php`에서 임계값 조정:
|
||
|
|
```php
|
||
|
|
if ($this->limiter->tooManyAttempts($key, 10)) { // 10 → 20으로 변경
|
||
|
|
```
|
||
|
|
|
||
|
|
### Q3. 화이트리스트에 라우트를 추가하려면?
|
||
|
|
|
||
|
|
**A:** `ApiKeyMiddleware.php` 수정:
|
||
|
|
```php
|
||
|
|
$publicRoutes = [
|
||
|
|
// 기존 라우트...
|
||
|
|
'api/v1/public-data', // 추가
|
||
|
|
];
|
||
|
|
```
|
||
|
|
|
||
|
|
### Q4. 특정 IP만 허용하려면?
|
||
|
|
|
||
|
|
**A:** Nginx 설정 추가:
|
||
|
|
```nginx
|
||
|
|
# 화이트리스트 IP만 허용
|
||
|
|
allow 203.0.113.0/24;
|
||
|
|
allow 198.51.100.50;
|
||
|
|
deny all;
|
||
|
|
```
|
||
|
|
|
||
|
|
### Q5. 토큰 만료 시간을 변경하려면?
|
||
|
|
|
||
|
|
**A:** `.env` 파일 수정:
|
||
|
|
```env
|
||
|
|
SANCTUM_ACCESS_TOKEN_EXPIRATION=120 # 2시간 → 4시간 (240)
|
||
|
|
SANCTUM_REFRESH_TOKEN_EXPIRATION=10080 # 7일 → 14일 (20160)
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 참고 문서
|
||
|
|
|
||
|
|
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||
|
|
- [Laravel Security Best Practices](https://laravel.com/docs/12.x/security)
|
||
|
|
- [Sanctum Documentation](https://laravel.com/docs/12.x/sanctum)
|
||
|
|
- [Nginx Security Tips](https://nginx.org/en/docs/http/ngx_http_access_module.html)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**작성일:** 2025-12-26
|
||
|
|
**버전:** 1.0
|
||
|
|
**담당자:** SAM Development Team
|