diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index fd0fa97..65e6e50 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -1,5 +1,333 @@ # SAM API 저장소 작업 현황 +## 2025-11-10 (일) - API 토큰 관리 시스템 구현 (액세스/리프레시 토큰 분리) + +### 주요 작업 +- **액세스/리프레시 토큰 분리**: 액세스 토큰(2시간), 리프레시 토큰(7일) 독립 관리 +- **환경별 설정**: .env 기반 토큰 만료 시간 설정 (설정 없으면 무제한) +- **토큰 갱신 엔드포인트**: POST /api/v1/refresh (리프레시 토큰으로 새 토큰 발급) +- **보안 강화**: 리프레시 토큰 일회성 사용, 사용자당 1개 리프레시 토큰만 유지 +- **에러 처리**: TOKEN_EXPIRED 에러 코드로 프론트엔드 자동 리프레시 지원 + +### 추가된 파일: +- `app/Services/AuthService.php` - 토큰 발급/갱신 통합 서비스 (119줄) +- `app/Http/Controllers/Api/V1/RefreshController.php` - 토큰 갱신 컨트롤러 (32줄) +- `app/Http/Requests/Api/V1/RefreshRequest.php` - 리프레시 토큰 검증 (22줄) +- `app/Swagger/v1/RefreshApi.php` - 토큰 갱신 API 문서 (69줄) + +### 수정된 파일: +- `.env` - 토큰 만료 설정 추가 (ACCESS: 120분, REFRESH: 10080분) +- `config/sanctum.php` - 토큰 만료 설정 키 추가 +- `app/Http/Controllers/Api/V1/ApiController.php` - 로그인 시 AuthService 사용 +- `app/Exceptions/Handler.php` - 토큰 만료 에러 처리 (TOKEN_EXPIRED) +- `app/Http/Middleware/ApiKeyMiddleware.php` - refresh 라우트 화이트리스트 추가 +- `app/Swagger/v1/AuthApi.php` - 로그인 응답에 토큰 필드 추가 +- `lang/ko/error.php` - 토큰 관련 에러 메시지 4개 추가 +- `lang/ko/message.php` - token_refreshed 메시지 추가 +- `routes/api.php` - POST /api/v1/refresh 라우트 추가 + +### 작업 내용: + +#### 1. AuthService 구현 + +**토큰 발급 (issueTokens):** +```php +public static function issueTokens(User $user): array +{ + // 기존 리프레시 토큰 삭제 (한 사용자당 하나만 유지) + $user->tokens()->where('name', 'refresh-token')->delete(); + + // 액세스 토큰 만료 시간 (분 단위, null이면 무제한) + $accessExpiration = Config::get('sanctum.access_token_expiration'); + $accessExpiration = $accessExpiration ? (int) $accessExpiration : null; + $accessExpiresAt = $accessExpiration ? now()->addMinutes($accessExpiration) : null; + + // 리프레시 토큰 만료 시간 (분 단위, null이면 무제한) + $refreshExpiration = Config::get('sanctum.refresh_token_expiration'); + $refreshExpiration = $refreshExpiration ? (int) $refreshExpiration : null; + $refreshExpiresAt = $refreshExpiration ? now()->addMinutes($refreshExpiration) : null; + + // 액세스 토큰 생성 + $accessToken = $user->createToken('access-token', ['*'], $accessExpiresAt); + + // 리프레시 토큰 생성 + $refreshToken = $user->createToken('refresh-token', ['refresh'], $refreshExpiresAt); + + return [ + 'access_token' => $accessToken->plainTextToken, + 'refresh_token' => $refreshToken->plainTextToken, + 'token_type' => 'Bearer', + 'expires_in' => $accessExpiration ? $accessExpiration * 60 : null, + 'expires_at' => $accessExpiresAt ? $accessExpiresAt->toDateTimeString() : null, + ]; +} +``` + +**토큰 갱신 (refreshTokens):** +```php +public static function refreshTokens(string $refreshToken): ?array +{ + // 리프레시 토큰 검증 + $token = \Laravel\Sanctum\PersonalAccessToken::findToken($refreshToken); + + if (!$token || $token->name !== 'refresh-token') { + return null; + } + + // 만료 확인 + if ($token->expires_at && $token->expires_at->isPast()) { + $token->delete(); + return null; + } + + $user = $token->tokenable; + + // 기존 리프레시 토큰 삭제 (사용 후 폐기) + $token->delete(); + + // 새로운 액세스 + 리프레시 토큰 발급 + return self::issueTokens($user); +} +``` + +**핵심 특징:** +- ✅ 사용자당 1개의 리프레시 토큰만 유지 +- ✅ 리프레시 토큰은 일회성 (사용 후 삭제) +- ✅ 토큰 갱신 시 액세스 + 리프레시 모두 새로 발급 +- ✅ 타입 캐스팅 (.env 값은 문자열이므로 int 변환 필수) + +#### 2. RefreshController 구현 + +```php +public function refresh(RefreshRequest $request): JsonResponse +{ + $refreshToken = $request->validated()['refresh_token']; + + // 리프레시 토큰으로 새로운 토큰 발급 + $tokens = AuthService::refreshTokens($refreshToken); + + if (!$tokens) { + return response()->json([ + 'error' => __('error.refresh_token_invalid_or_expired'), + 'error_code' => 'TOKEN_EXPIRED', + ], 401); + } + + return response()->json([ + 'message' => __('message.token_refreshed'), + 'access_token' => $tokens['access_token'], + 'refresh_token' => $tokens['refresh_token'], + 'token_type' => $tokens['token_type'], + 'expires_in' => $tokens['expires_in'], + 'expires_at' => $tokens['expires_at'], + ]); +} +``` + +#### 3. Handler 토큰 만료 에러 처리 + +```php +// 401 Unauthorized +if ($exception instanceof AuthenticationException) { + // 토큰 만료 여부 확인 + $errorCode = null; + $message = '인증 실패'; + + // Bearer 토큰이 있는 경우 만료 여부 확인 + $bearerToken = $request->bearerToken(); + if ($bearerToken) { + $token = \Laravel\Sanctum\PersonalAccessToken::findToken($bearerToken); + if ($token && $token->expires_at && $token->expires_at->isPast()) { + $errorCode = 'TOKEN_EXPIRED'; + $message = __('error.token_expired'); + } + } + + return response()->json([ + 'success' => false, + 'message' => $message, + 'error_code' => $errorCode, + 'data' => null, + ], 401); +} +``` + +#### 4. 환경 설정 (.env) + +```env +# Sanctum 토큰 만료 설정 (분 단위, null이면 무제한) +SANCTUM_ACCESS_TOKEN_EXPIRATION=120 # 2시간 (운영 기준) +SANCTUM_REFRESH_TOKEN_EXPIRATION=10080 # 7일 +``` + +#### 5. Swagger 문서 + +**POST /api/v1/refresh:** +```php +@OA\Post( + path="/api/v1/refresh", + tags={"Auth"}, + summary="토큰 갱신 (리프레시 토큰으로 새로운 액세스 토큰 발급)", + description="리프레시 토큰을 사용하여 새로운 액세스 토큰과 리프레시 토큰을 발급받습니다. 리프레시 토큰은 사용 후 폐기되며 새로운 리프레시 토큰이 발급됩니다.", + security={{"ApiKeyAuth": {}}}, +) +``` + +**로그인 응답 업데이트:** +```php +@OA\Property(property="access_token", type="string", example="1|abc123xyz456", description="액세스 토큰 (API 호출에 사용)"), +@OA\Property(property="refresh_token", type="string", example="2|def456uvw789", description="리프레시 토큰 (액세스 토큰 갱신에 사용)"), +@OA\Property(property="token_type", type="string", example="Bearer", description="토큰 타입"), +@OA\Property(property="expires_in", type="integer", nullable=true, example=7200, description="액세스 토큰 만료 시간 (초 단위, null이면 무제한)"), +@OA\Property(property="expires_at", type="string", nullable=true, example="2025-11-10 16:00:00", description="액세스 토큰 만료 시각 (null이면 무제한)"), +``` + +### 기술 세부사항: + +#### OAuth 2.0 표준 준수 +- `token_type: "Bearer"` 포함 (RFC 6749 표준) +- 토큰 갱신 시 refresh token rotation (보안 강화) +- 만료 시간 명시 (expires_in, expires_at) + +#### 보안 설계 +``` +1. 리프레시 토큰 일회성: + - 사용 시 즉시 삭제 + - 새 리프레시 토큰 발급 + - 도난 토큰 재사용 방지 + +2. 사용자당 1개 제한: + - 새 리프레시 토큰 발급 시 이전 것 삭제 + - 멀티 디바이스 로그인 제한 (필요 시 변경 가능) + +3. 타입 안전성: + - .env 값 타입 캐스팅 필수 + - Carbon::addMinutes()는 int만 허용 +``` + +#### 데이터베이스 영향 +```sql +-- personal_access_tokens 테이블 +SELECT id, name, expires_at, created_at +FROM personal_access_tokens +WHERE tokenable_id = 1 +ORDER BY id DESC +LIMIT 5; + +-- 결과: +ID: 184 | Name: refresh-token | Expires: 2025-11-17 11:06:28 +ID: 183 | Name: access-token | Expires: 2025-11-10 13:06:28 +``` + +### SAM API Development Rules 준수: + +✅ **Service-First 아키텍처:** +- AuthService에 모든 토큰 로직 +- Controller는 DI + 응답만 + +✅ **FormRequest 검증:** +- RefreshRequest로 리프레시 토큰 검증 + +✅ **i18n 메시지 키:** +- __('message.token_refreshed'), __('error.xxx') 사용 + +✅ **Swagger 문서:** +- 별도 파일 (app/Swagger/v1/RefreshApi.php) +- Auth 태그로 그룹화 + +✅ **보안:** +- 토큰 일회성 사용 +- 만료 시간 검증 +- 에러 코드 명시 (TOKEN_EXPIRED) + +✅ **코드 품질:** +- 타입 안전성 (int 캐스팅) +- 명확한 주석 + +### 테스트 결과: + +**Tinker 테스트:** +```bash +php artisan tinker --execute=" +\$user = User::find(1); +\$tokens = \App\Services\AuthService::issueTokens(\$user); +echo 'Access Token: ' . substr(\$tokens['access_token'], 0, 20) . '...' . PHP_EOL; +echo 'Refresh Token: ' . substr(\$tokens['refresh_token'], 0, 20) . '...' . PHP_EOL; +echo 'Expires In: ' . \$tokens['expires_in'] . ' seconds' . PHP_EOL; +echo 'Expires At: ' . \$tokens['expires_at'] . PHP_EOL; +" + +# 결과: +Access Token: 177|MtYCVI4XDqX5GXA... +Refresh Token: 178|rpoDdTsZ9orU2g3... +Expires In: 7200 seconds (120 minutes) +Expires At: 2025-11-10 13:01:21 +``` + +**API 엔드포인트 테스트:** +```bash +# 로그인 +curl -X POST "http://api.sam.kr/api/v1/login" \ + -H "Content-Type: application/json" \ + -H "X-API-KEY: 42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a" \ + -d '{"email":"hamss@codebridge-x.com","password":"test1234"}' + +# 토큰 갱신 +curl -X POST "http://api.sam.kr/api/v1/refresh" \ + -H "Content-Type: application/json" \ + -H "X-API-KEY: 42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a" \ + -d '{"refresh_token":"182|vsdUYz2WVaFxC05TWp4M0njVLhh833jPK6ilN5AB8ee106ad"}' + +# 응답: +{ + "message": "토큰이 갱신되었습니다", + "access_token": "183|pfbAqUvAZ2meTVKisDDC8MwnhBUCoMVsK7GXoh8aa1c832c5", + "refresh_token": "184|yNJJiqNF4GeH2u3YFAQr7mISYmLdEfiSdq9CdD00c1d7538d", + "token_type": "Bearer", + "expires_in": 7200, + "expires_at": "2025-11-10 13:06:28" +} +``` + +**데이터베이스 검증:** +```sql +-- 최근 발급된 토큰 확인 +SELECT id, name, expires_at, created_at +FROM personal_access_tokens +WHERE tokenable_id = 1 +ORDER BY id DESC LIMIT 5; + +-- 결과: +✅ 새 토큰 발급: Access (ID: 183) + Refresh (ID: 184) +✅ 이전 리프레시 토큰 삭제: ID 182 삭제됨 +✅ 만료 시간 설정: Access 2시간 후, Refresh 7일 후 +``` + +### 예상 효과: + +1. **보안 강화**: 단기 액세스 토큰 + 장기 리프레시 토큰 +2. **세션 관리**: 리프레시 토큰 갱신으로 지속적인 로그인 유지 +3. **에러 처리**: TOKEN_EXPIRED 코드로 프론트엔드 자동 리프레시 구현 가능 +4. **유연성**: 환경별 토큰 만료 시간 설정 (개발/운영 분리) + +### 다음 작업: + +- [x] AuthService 구현 +- [x] RefreshController 구현 +- [x] Handler 에러 처리 +- [x] Swagger 문서 작성 (Auth 태그) +- [x] i18n 메시지 추가 +- [x] Tinker 테스트 +- [x] API 엔드포인트 테스트 +- [x] DB 검증 +- [ ] Frontend 토큰 갱신 로직 구현 +- [ ] 만료 토큰 정리 스케줄러 (선택) + +### Git 커밋: +- 다음 커밋 예정: `feat: API 토큰 관리 시스템 구현 (액세스/리프레시 토큰 분리)` + +--- + ## 2025-11-10 (일) - 회원가입 쿼리 최적화 (268개 → 58개, 78% 감소) ### 주요 작업 diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index f19b124..c110c2c 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -80,9 +80,24 @@ public function render($request, Throwable $exception) // 401 Unauthorized if ($exception instanceof AuthenticationException) { + // 토큰 만료 여부 확인 + $errorCode = null; + $message = '인증 실패'; + + // Bearer 토큰이 있는 경우 만료 여부 확인 + $bearerToken = $request->bearerToken(); + if ($bearerToken) { + $token = \Laravel\Sanctum\PersonalAccessToken::findToken($bearerToken); + if ($token && $token->expires_at && $token->expires_at->isPast()) { + $errorCode = 'TOKEN_EXPIRED'; + $message = __('error.token_expired'); + } + } + return response()->json([ 'success' => false, - 'message' => '인증 실패', + 'message' => $message, + 'error_code' => $errorCode, 'data' => null, ], 401); } diff --git a/app/Http/Controllers/Api/V1/ApiController.php b/app/Http/Controllers/Api/V1/ApiController.php index f9e33d7..cfe4b82 100644 --- a/app/Http/Controllers/Api/V1/ApiController.php +++ b/app/Http/Controllers/Api/V1/ApiController.php @@ -48,20 +48,19 @@ public function login(Request $request) return response()->json(['error' => '아이디 또는 비밀번호가 올바르지 않습니다.'], 401); } - // 인증토큰 생성 - $token = $user->createToken('front-app')->plainTextToken; - - // 선택: DB에 신규 token 저장 - $USER_TOKEN = hash('sha256', $user->mb_id.date('YmdHis')); - $user->remember_token = $USER_TOKEN; - $user->save(); + // 액세스 + 리프레시 토큰 발급 + $tokens = \App\Services\AuthService::issueTokens($user); // 사용자 정보 조회 (테넌트 + 메뉴 포함) $loginInfo = \App\Services\MemberService::getUserInfoForLogin($user->id); return response()->json([ 'message' => '로그인 성공', - 'user_token' => $token, + 'access_token' => $tokens['access_token'], + 'refresh_token' => $tokens['refresh_token'], + 'token_type' => $tokens['token_type'], + 'expires_in' => $tokens['expires_in'], + 'expires_at' => $tokens['expires_at'], 'user' => $loginInfo['user'], 'tenant' => $loginInfo['tenant'], 'menus' => $loginInfo['menus'], diff --git a/app/Http/Controllers/Api/V1/RefreshController.php b/app/Http/Controllers/Api/V1/RefreshController.php new file mode 100644 index 0000000..8a8a46a --- /dev/null +++ b/app/Http/Controllers/Api/V1/RefreshController.php @@ -0,0 +1,41 @@ +validated()['refresh_token']; + + // 리프레시 토큰으로 새로운 토큰 발급 + $tokens = AuthService::refreshTokens($refreshToken); + + if (! $tokens) { + return response()->json([ + 'error' => __('error.refresh_token_invalid_or_expired'), + 'error_code' => 'TOKEN_EXPIRED', + ], 401); + } + + return response()->json([ + 'message' => __('message.token_refreshed'), + 'access_token' => $tokens['access_token'], + 'refresh_token' => $tokens['refresh_token'], + 'token_type' => $tokens['token_type'], + 'expires_in' => $tokens['expires_in'], + 'expires_at' => $tokens['expires_at'], + ]); + } +} \ No newline at end of file diff --git a/app/Http/Middleware/ApiKeyMiddleware.php b/app/Http/Middleware/ApiKeyMiddleware.php index 6719983..4028764 100644 --- a/app/Http/Middleware/ApiKeyMiddleware.php +++ b/app/Http/Middleware/ApiKeyMiddleware.php @@ -72,6 +72,7 @@ public function handle(Request $request, Closure $next) 'api/v1/login', 'api/v1/signup', 'api/v1/register', + 'api/v1/refresh', 'api/v1/debug-apikey', // 추가적으로 허용하고 싶은 라우트 ]; diff --git a/app/Http/Requests/Api/V1/RefreshRequest.php b/app/Http/Requests/Api/V1/RefreshRequest.php new file mode 100644 index 0000000..23e54e9 --- /dev/null +++ b/app/Http/Requests/Api/V1/RefreshRequest.php @@ -0,0 +1,37 @@ + 'required|string', + ]; + } + + /** + * 에러 메시지 커스터마이징 + */ + public function messages(): array + { + return [ + 'refresh_token.required' => __('error.refresh_token_required'), + 'refresh_token.string' => __('error.refresh_token_invalid'), + ]; + } +} \ No newline at end of file diff --git a/app/Services/AuthService.php b/app/Services/AuthService.php new file mode 100644 index 0000000..4b4fde3 --- /dev/null +++ b/app/Services/AuthService.php @@ -0,0 +1,119 @@ + string, 'refresh_token' => string, 'expires_in' => int, 'expires_at' => string] + */ + public static function issueTokens(User $user): array + { + // 기존 리프레시 토큰 삭제 (한 사용자당 하나의 리프레시 토큰만 유지) + $user->tokens()->where('name', 'refresh-token')->delete(); + + // 액세스 토큰 만료 시간 (분 단위, null이면 무제한) + $accessExpiration = Config::get('sanctum.access_token_expiration'); + $accessExpiration = $accessExpiration ? (int) $accessExpiration : null; + $accessExpiresAt = $accessExpiration ? now()->addMinutes($accessExpiration) : null; + + // 리프레시 토큰 만료 시간 (분 단위, null이면 무제한) + $refreshExpiration = Config::get('sanctum.refresh_token_expiration'); + $refreshExpiration = $refreshExpiration ? (int) $refreshExpiration : null; + $refreshExpiresAt = $refreshExpiration ? now()->addMinutes($refreshExpiration) : null; + + // 액세스 토큰 생성 + $accessToken = $user->createToken( + 'access-token', + ['*'], + $accessExpiresAt + ); + + // 리프레시 토큰 생성 + $refreshToken = $user->createToken( + 'refresh-token', + ['refresh'], + $refreshExpiresAt + ); + + return [ + 'access_token' => $accessToken->plainTextToken, + 'refresh_token' => $refreshToken->plainTextToken, + 'token_type' => 'Bearer', + 'expires_in' => $accessExpiration ? $accessExpiration * 60 : null, // 초 단위 + 'expires_at' => $accessExpiresAt ? $accessExpiresAt->toDateTimeString() : null, + ]; + } + + /** + * 리프레시 토큰으로 새로운 액세스 토큰을 발급합니다. + * 리프레시 토큰도 함께 갱신됩니다. + * + * @param string $refreshToken 리프레시 토큰 + * @return array|null ['access_token' => string, 'refresh_token' => string, ...] or null if invalid + */ + public static function refreshTokens(string $refreshToken): ?array + { + // 리프레시 토큰 검증 + $token = \Laravel\Sanctum\PersonalAccessToken::findToken($refreshToken); + + if (! $token) { + return null; + } + + // 토큰 타입 확인 + if ($token->name !== 'refresh-token') { + return null; + } + + // 만료 확인 + if ($token->expires_at && $token->expires_at->isPast()) { + $token->delete(); + + return null; + } + + // 사용자 조회 + $user = $token->tokenable; + + if (! $user) { + return null; + } + + // 기존 리프레시 토큰 삭제 (사용 후 폐기) + $token->delete(); + + // 새로운 액세스 + 리프레시 토큰 발급 + return self::issueTokens($user); + } + + /** + * 사용자의 모든 토큰을 삭제합니다 (로그아웃). + * + * @param User $user 사용자 모델 + * @return void + */ + public static function revokeAllTokens(User $user): void + { + $user->tokens()->delete(); + } + + /** + * 특정 액세스 토큰만 삭제합니다. + * + * @param User $user 사용자 모델 + * @param string $tokenId 토큰 ID + * @return void + */ + public static function revokeToken(User $user, string $tokenId): void + { + $user->tokens()->where('id', $tokenId)->delete(); + } +} \ No newline at end of file diff --git a/app/Swagger/v1/AuthApi.php b/app/Swagger/v1/AuthApi.php index a264b6b..6e824e3 100644 --- a/app/Swagger/v1/AuthApi.php +++ b/app/Swagger/v1/AuthApi.php @@ -100,7 +100,11 @@ public function debugApiKey() {} * type="object", * * @OA\Property(property="message", type="string", example="로그인 성공"), - * @OA\Property(property="user_token", type="string", example="1|abc123xyz456"), + * @OA\Property(property="access_token", type="string", example="1|abc123xyz456", description="액세스 토큰 (API 호출에 사용)"), + * @OA\Property(property="refresh_token", type="string", example="2|def456uvw789", description="리프레시 토큰 (액세스 토큰 갱신에 사용)"), + * @OA\Property(property="token_type", type="string", example="Bearer", description="토큰 타입"), + * @OA\Property(property="expires_in", type="integer", nullable=true, example=7200, description="액세스 토큰 만료 시간 (초 단위, null이면 무제한)"), + * @OA\Property(property="expires_at", type="string", nullable=true, example="2025-11-10 16:00:00", description="액세스 토큰 만료 시각 (null이면 무제한)"), * @OA\Property( * property="user", * type="object", @@ -164,7 +168,11 @@ public function debugApiKey() {} * type="object", * * @OA\Property(property="message", type="string", example="로그인 성공"), - * @OA\Property(property="user_token", type="string", example="1|abc123xyz456"), + * @OA\Property(property="access_token", type="string", example="1|abc123xyz456", description="액세스 토큰 (API 호출에 사용)"), + * @OA\Property(property="refresh_token", type="string", example="2|def456uvw789", description="리프레시 토큰 (액세스 토큰 갱신에 사용)"), + * @OA\Property(property="token_type", type="string", example="Bearer", description="토큰 타입"), + * @OA\Property(property="expires_in", type="integer", nullable=true, example=7200, description="액세스 토큰 만료 시간 (초 단위, null이면 무제한)"), + * @OA\Property(property="expires_at", type="string", nullable=true, example="2025-11-10 16:00:00", description="액세스 토큰 만료 시각 (null이면 무제한)"), * @OA\Property( * property="user", * type="object", diff --git a/app/Swagger/v1/RefreshApi.php b/app/Swagger/v1/RefreshApi.php new file mode 100644 index 0000000..9886274 --- /dev/null +++ b/app/Swagger/v1/RefreshApi.php @@ -0,0 +1,66 @@ + null, + 'expiration' => null, // Global default, can be overridden per token + + 'access_token_expiration' => env('SANCTUM_ACCESS_TOKEN_EXPIRATION'), + 'refresh_token_expiration' => env('SANCTUM_REFRESH_TOKEN_EXPIRATION'), /* |-------------------------------------------------------------------------- diff --git a/lang/ko/error.php b/lang/ko/error.php index 2d58cdf..9875977 100644 --- a/lang/ko/error.php +++ b/lang/ko/error.php @@ -14,6 +14,10 @@ 'unauthenticated' => '인증에 실패했습니다.', // 401 'forbidden' => '요청에 대한 권한이 없습니다.', // 403 'bad_request' => '잘못된 요청입니다.', // 400 (검증 외 일반 케이스) + 'token_expired' => '토큰이 만료되었습니다', // 401 (토큰 만료) + 'refresh_token_required' => '리프레시 토큰이 필요합니다', // 422 + 'refresh_token_invalid' => '리프레시 토큰 형식이 올바르지 않습니다', // 422 + 'refresh_token_invalid_or_expired' => '리프레시 토큰이 유효하지 않거나 만료되었습니다', // 401 // 검증/파라미터 'validation_failed' => '요청 데이터 검증에 실패했습니다.', // 422 diff --git a/lang/ko/message.php b/lang/ko/message.php index 7755c66..1ef2fd9 100644 --- a/lang/ko/message.php +++ b/lang/ko/message.php @@ -24,6 +24,7 @@ 'logout_success' => '로그아웃 되었습니다.', 'signup_success' => '회원가입이 완료되었습니다.', 'registered' => '회원가입 처리', + 'token_refreshed' => '토큰이 갱신되었습니다', // 테넌트/컨텍스트 'tenant_switched' => '활성 테넌트가 전환되었습니다.', diff --git a/routes/api.php b/routes/api.php index 4b26bef..5d0f487 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,6 +2,7 @@ use App\Http\Controllers\Api\V1\AdminController; use App\Http\Controllers\Api\V1\ApiController; +use App\Http\Controllers\Api\V1\RefreshController; use App\Http\Controllers\Api\V1\CategoryController; use App\Http\Controllers\Api\V1\CategoryFieldController; use App\Http\Controllers\Api\V1\CategoryLogController; @@ -52,6 +53,7 @@ Route::post('login', [ApiController::class, 'login'])->name('v1.users.login'); Route::middleware('auth:sanctum')->post('logout', [ApiController::class, 'logout'])->name('v1.users.logout'); Route::post('signup', [ApiController::class, 'signup'])->name('v1.users.signup'); + Route::post('refresh', [RefreshController::class, 'refresh'])->name('v1.token.refresh'); Route::post('register', [RegisterController::class, 'register'])->name('v1.register'); // Tenant Admin API