-
Notifications
You must be signed in to change notification settings - Fork 0
feat: custom exception 추가 #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| name: Deploy Auth to ECS | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| workflow_dispatch: | ||
|
|
||
| env: | ||
| AWS_REGION: ap-northeast-2 | ||
| AWS_ACCOUNT_ID: 727452759104 | ||
| ECR_REPOSITORY: momentlit/auth | ||
| ECS_CLUSTER: default | ||
| ECS_SERVICE: momentlit-auth-service | ||
| IMAGE_TAG: latest | ||
|
|
||
| jobs: | ||
| deploy: | ||
| name: Build and Deploy Auth | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout source code | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Configure AWS credentials | ||
| uses: aws-actions/configure-aws-credentials@v5 | ||
| with: | ||
| aws-region: ${{ env.AWS_REGION }} | ||
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | ||
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | ||
|
|
||
| - name: Login to Amazon ECR | ||
| uses: aws-actions/amazon-ecr-login@v2 | ||
|
|
||
| - name: Set up Docker Buildx | ||
| uses: docker/setup-buildx-action@v3 | ||
|
|
||
| - name: Build and push Docker image | ||
| run: | | ||
| IMAGE_URI=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}:${IMAGE_TAG} | ||
|
|
||
| docker buildx build \ | ||
| --platform linux/amd64 \ | ||
| --provenance=false \ | ||
| -t $IMAGE_URI \ | ||
| . \ | ||
| --push | ||
|
|
||
| - name: Force new ECS deployment | ||
| run: | | ||
| aws ecs update-service \ | ||
| --cluster $ECS_CLUSTER \ | ||
| --service $ECS_SERVICE \ | ||
| --force-new-deployment \ | ||
| --region $AWS_REGION | ||
|
|
||
| - name: Wait for ECS service stable | ||
| run: | | ||
| aws ecs wait services-stable \ | ||
| --cluster $ECS_CLUSTER \ | ||
| --services $ECS_SERVICE \ | ||
| --region $AWS_REGION | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,54 @@ servers: | |
| tags: [] | ||
| paths: {} | ||
| components: | ||
| schemas: {} | ||
| responses: {} | ||
| schemas: | ||
| ErrorResponse: | ||
| type: object | ||
| properties: | ||
| message: | ||
| type: string | ||
| example: "[ERROR: Request/BadRequest] Refresh Token을 입력해주세요." | ||
| data: | ||
| nullable: true | ||
| example: null | ||
| required: | ||
| - message | ||
| - data | ||
|
Comment on lines
+13
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 에러 응답 스키마가 코딩 가이드라인을 위반합니다. 코딩 가이드라인은 "API error responses should have a consistent format with 'code' and 'message' fields"를 요구하지만, 현재 다음과 같이 구조화된 스키마로 변경하는 것을 권장합니다: ErrorResponse:
type: object
properties:
code:
type: string
example: "REQUEST_BAD_REQUEST"
message:
type: string
example: "Refresh Token을 입력해주세요."
data:
nullable: true
example: null
required:
- code
- message이 변경은 As per coding guidelines, "API error responses should have a consistent format with 'code' and 'message' fields." 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| responses: | ||
| BadRequestError: | ||
| description: "Invalid request input." | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: "#/components/schemas/ErrorResponse" | ||
| example: | ||
| message: "[ERROR: Request/BadRequest] Refresh Token을 입력해주세요." | ||
| data: null | ||
| UnauthorizedError: | ||
| description: "Authentication failed or token is invalid." | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: "#/components/schemas/ErrorResponse" | ||
| example: | ||
| message: "[ERROR: Auth/Unauthorized] 유효하지 않은 Refresh Token입니다." | ||
| data: null | ||
| GoogleOauthError: | ||
| description: "Google OAuth request failed." | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: "#/components/schemas/ErrorResponse" | ||
| example: | ||
| message: "[ERROR: Auth/Oauth/Google] Google Access Token을 발급받을 수 없습니다." | ||
| data: null | ||
| InternalServerError: | ||
| description: "Unexpected server error." | ||
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: "#/components/schemas/ErrorResponse" | ||
| example: | ||
| message: "[ERROR: ?/?] 서버 내부 오류가 발생했습니다." | ||
| data: null | ||
|
Comment on lines
+25
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift 에러 응답 컴포넌트의 예시가 구조화되지 않은 에러 코드를 포함합니다. 각 에러 응답 예시의 BadRequestError:
description: "Invalid request input."
content:
application/json:
schema:
$ref: "`#/components/schemas/ErrorResponse`"
example:
code: "REQUEST_BAD_REQUEST"
message: "Refresh Token을 입력해주세요."
data: null🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| securitySchemes: {} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,4 +3,8 @@ | |
| public record ApiResponse<T> ( | ||
| String message, | ||
| T data | ||
| ){} | ||
| ){ | ||
| public static <T> ApiResponse<T> fail(String message) { | ||
| return new ApiResponse<>(message, null); | ||
| } | ||
|
Comment on lines
3
to
+9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 에러 응답 포맷을 현재 변경 예시-public record ApiResponse<T> (
- String message,
- T data
-){
- public static <T> ApiResponse<T> fail(String message) {
- return new ApiResponse<>(message, null);
- }
+public record ApiResponse<T> (
+ String code,
+ String message,
+ T data
+){
+ public static <T> ApiResponse<T> fail(String code, String message) {
+ return new ApiResponse<>(code, message, null);
+ }
}As per coding guidelines, "API error responses should have a consistent format with 'code' and 'message' fields." 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.example.auth.global.exception; | ||
|
|
||
| public class AuthException extends RuntimeException { | ||
| public AuthException(String message) { | ||
| super(message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.example.auth.global.exception; | ||
|
|
||
| public class BadRequestException extends AuthException { | ||
| public BadRequestException(String message) { | ||
| super(message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package com.example.auth.global.exception; | ||
|
|
||
| import com.example.auth.global.dto.ApiResponse; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||
| import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
|
|
||
| @Slf4j | ||
| @RestControllerAdvice | ||
| public class GlobalExceptionHandler { | ||
|
|
||
| @ExceptionHandler(TokenNotFoundException.class) | ||
| public ResponseEntity<ApiResponse<String>> tokenNotFoundHandleException(TokenNotFoundException e) { | ||
| log.warn("TokenNotFoundException: {}", e.getMessage()); | ||
|
|
||
| return ResponseEntity.status(HttpStatus.UNAUTHORIZED) | ||
| .body(ApiResponse.fail("[ERROR: Auth/Token/NotFound] " + e.getMessage())); | ||
| } | ||
|
|
||
| @ExceptionHandler(UnauthorizedException.class) | ||
| public ResponseEntity<ApiResponse<String>> unauthorizedHandleException(UnauthorizedException e) { | ||
| log.warn("UnauthorizedException: {}", e.getMessage()); | ||
|
|
||
| return ResponseEntity.status(HttpStatus.UNAUTHORIZED) | ||
| .body(ApiResponse.fail("[ERROR: Auth/Unauthorized] " + e.getMessage())); | ||
| } | ||
|
|
||
| @ExceptionHandler(BadRequestException.class) | ||
| public ResponseEntity<ApiResponse<String>> badRequestHandleException(BadRequestException e) { | ||
| log.warn("BadRequestException: {}", e.getMessage()); | ||
|
|
||
| return ResponseEntity.status(HttpStatus.BAD_REQUEST) | ||
| .body(ApiResponse.fail("[ERROR: Request/BadRequest] " + e.getMessage())); | ||
| } | ||
|
|
||
| @ExceptionHandler(GoogleOauthException.class) | ||
| public ResponseEntity<ApiResponse<String>> googleOauthHandleException(GoogleOauthException e) { | ||
| log.error("GoogleOauthException", e); | ||
|
|
||
| return ResponseEntity.status(HttpStatus.BAD_GATEWAY) | ||
| .body(ApiResponse.fail("[ERROR: Auth/Oauth/Google] " + e.getMessage())); | ||
| } | ||
|
|
||
| @ExceptionHandler(AuthException.class) | ||
| public ResponseEntity<ApiResponse<String>> authHandleException(AuthException e) { | ||
| log.error("AuthException", e); | ||
|
|
||
| return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) | ||
| .body(ApiResponse.fail("[ERROR: Auth/?] " + e.getMessage())); | ||
| } | ||
|
|
||
| @ExceptionHandler(Exception.class) | ||
| public ResponseEntity<ApiResponse<String>> globalHandleException(Exception e) { | ||
| log.error("Unhandled exception", e); | ||
|
|
||
| return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) | ||
| .body(ApiResponse.fail("[ERROR: ?/?] 서버 내부 오류가 발생했습니다.")); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.example.auth.global.exception; | ||
|
|
||
| public class GoogleOauthException extends AuthException { | ||
| public GoogleOauthException(String message) { | ||
| super(message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.example.auth.global.exception; | ||
|
|
||
| public class TokenNotFoundException extends AuthException { | ||
| public TokenNotFoundException(String message) { | ||
| super(message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.example.auth.global.exception; | ||
|
|
||
| public class UnauthorizedException extends AuthException { | ||
| public UnauthorizedException(String message) { | ||
| super(message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,10 @@ | ||
| package com.example.auth.service; | ||
|
|
||
| import com.example.auth.global.security.JwtProvider; | ||
| import com.example.auth.global.exception.TokenNotFoundException; | ||
| import com.example.auth.global.exception.UnauthorizedException; | ||
| import com.example.auth.infra.RefreshTokenRepository; | ||
| import io.jsonwebtoken.JwtException; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
|
|
@@ -46,12 +49,16 @@ public long accessTokenExpiresInSeconds() { | |
| } | ||
|
|
||
| public String getValidRefreshTokenSubject(String refreshToken) { | ||
| jwtProvider.validateToken(refreshToken); | ||
| try { | ||
| jwtProvider.validateToken(refreshToken); | ||
| } catch (JwtException | IllegalArgumentException exception) { | ||
| throw new UnauthorizedException("유효하지 않은 Refresh Token입니다."); | ||
| } | ||
|
Comment on lines
+52
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# JwtProvider.validateToken() 메서드의 예외 선언 확인
rg -nP -A 10 'void\s+validateToken\s*\(' --type=java -g '*JwtProvider*'Repository: MomentLit/Auth Length of output: 40 🌐 Web query:
💡 Result: In JJWT 0.12.6, the JwtParser.parseSignedClaims(CharSequence) method is designed to parse and verify signed JWTs (JWS). Because this operation involves both parsing and cryptographic verification, it can throw various exceptions derived from the base io.jsonwebtoken.JwtException class [1][2]. Common exceptions include: 1. SignatureException: Thrown if the JWT signature verification fails [3]. 2. ExpiredJwtException: Thrown if the JWT has expired (e.g., the 'exp' claim is in the past) [4][3]. 3. UnsupportedJwtException: Thrown if the parsed JWT does not match the expected format or type (e.g., if you call parseSignedClaims on a token that is not a JWS) [1][2][3]. 4. MalformedJwtException: Thrown if the JWT string is not correctly formatted or is otherwise unparseable [1][2]. 5. MissingClaimException: Thrown if a required claim is missing. 6. PrematureJwtException: Thrown if the JWT is not yet valid (e.g., the 'nbf' claim is in the future). Since these exceptions all extend from JwtException, it is recommended to wrap the parseSignedClaims call in a try-catch block to handle authentication or validation failures [1][2][3]: try { Claims claims = Jwts.parser.verifyWith(secretKey).build.parseSignedClaims(token).getPayload; // Token is valid and trusted } catch (JwtException e) { // Token is invalid, expired, or failed signature verification } Note that in version 0.12.6, JwtParser is immutable, and you must use the Jwts.parser method to obtain a JwtParserBuilder, configure it, and call build before parsing [5][6][7]. Using the incorrect method for the token type (e.g., trying to parse an unsigned JWT with parseSignedClaims) will trigger an UnsupportedJwtException [1][2]. Citations:
🏁 Script executed: fd -type f -name "*.java" | head -20Repository: MomentLit/Auth Length of output: 229 🏁 Script executed: find . -type f -name "*JwtProvider*" 2>/dev/nullRepository: MomentLit/Auth Length of output: 124 🏁 Script executed: rg "class JwtProvider" --type=java -A 5Repository: MomentLit/Auth Length of output: 615 🏁 Script executed: cat -n src/main/java/com/example/auth/service/TokenService.java | head -80Repository: MomentLit/Auth Length of output: 2958 🏁 Script executed: cat -n src/main/java/com/example/auth/global/security/JwtProvider.javaRepository: MomentLit/Auth Length of output: 4740
🤖 Prompt for AI Agents |
||
|
|
||
| String subject = jwtProvider.getSubject(refreshToken); | ||
|
|
||
| if (!refreshTokenRepository.existsBySubjectAndToken(subject, refreshToken)) { | ||
| throw new IllegalArgumentException( | ||
| throw new TokenNotFoundException( | ||
| "저장된 Refresh Token과 일치하지 않습니다." | ||
| ); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
보안 강화:
permissions블록 추가 및persist-credentials: false설정 필요정적 분석에서 지적한 대로, 최소 권한 원칙을 위해 명시적 권한 블록을 추가하고, checkout 단계에서 자격 증명 지속을 비활성화해야 합니다.
🛡️ 권장 수정 사항
jobs: deploy: name: Build and Deploy Auth runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - name: Checkout source code uses: actions/checkout@v4 + with: + persist-credentials: false📝 Committable suggestion
🧰 Tools
🪛 zizmor (1.25.2)
[warning] 23-24: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[error] 24-24: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Source: Linters/SAST tools