Skip to content

[FEAT] jwt 토큰 인증 추가#5

Merged
taemin3 merged 1 commit into
mainfrom
SPM-506
Nov 11, 2025
Merged

[FEAT] jwt 토큰 인증 추가#5
taemin3 merged 1 commit into
mainfrom
SPM-506

Conversation

@taemin3
Copy link
Copy Markdown
Contributor

@taemin3 taemin3 commented Nov 11, 2025

📝 Summary

  • [FEAT] jwt 토큰 인증 추가

🙏 Question & PR point

📬 Reference

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • JWT 기반 인증 시스템 추가
    • 역할 기반 접근 제어 및 권한 계층 구조 구현
    • 조직(워크스페이스) 지원 추가
    • 맞춤형 보안 오류 처리 및 응답 추가
  • 버그 수정

    • 패키지 이름 오타 수정

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 11, 2025

작업 요약

JWT 기반 인증 및 보안 설정을 구현하는 변경사항입니다. 필요한 라이브러리 의존성을 추가하고, JWT 토큰 검증 및 필터링을 위한 컴포넌트, Spring Security 설정, 새로운 엔티티 타입(역할, 작업 공간), 그리고 관련 예외 처리 및 오류 상태 코드를 도입했습니다.

변경 사항

코호트 / 파일(들) 변경 요약
빌드 및 의존성
build.gradle
JWT 토큰 처리를 위해 jjwt-api, jjwt-impl, jjwt-jackson 및 spring-boot-starter-security 의존성 추가
패키지명 오타 수정
src/main/java/com/sampoom/purchase/api/purchase/entity/PurchaseOrder.java, src/main/java/com/sampoom/purchase/common/entity/BaseTimeEntity.java, src/main/java/com/sampoom/purchase/common/entity/SoftDeleteEntity.java
패키지 명 com.sampoom.purchase.common.entitiy에서 com.sampoom.purchase.common.entity로 수정
JWT 인증 제공자
src/main/java/com/sampoom/purchase/common/config/jwt/JwtProvider.java
JWT 토큰 파싱 및 검증, RSA 공개키 로드, Authorization 헤더/쿠키에서 액세스 토큰 추출 기능 제공
JWT 요청 필터
src/main/java/com/sampoom/purchase/common/config/jwt/JwtAuthFilter.java
OncePerRequestFilter 확장하여 JWT 기반 인증 처리, 토큰 타입별 검증 및 권한 매핑 수행
보안 설정
src/main/java/com/sampoom/purchase/common/config/security/SecurityConfig.java
Stateless JWT 기반 보안 설정: CSRF 비활성화, 경로별 권한 설정, JwtAuthFilter 및 커스텀 예외 처리기 등록
역할 계층 설정
src/main/java/com/sampoom/purchase/common/config/security/RoleHierarchyConfig.java
ROLE_ADMIN 및 서브 역할의 계층 구조 정의
커스텀 인증 진입점
src/main/java/com/sampoom/purchase/common/config/security/CustomAuthEntryPoint.java
AuthenticationEntryPoint 구현하여 인증 실패 시 구조화된 JSON 응답 반환
커스텀 접근 거부 처리
src/main/java/com/sampoom/purchase/common/config/security/CustomAccessDeniedHandler.java
AccessDeniedHandler 구현하여 권한 없음(403) 응답 처리
엔티티 타입
src/main/java/com/sampoom/purchase/common/entity/Role.java, src/main/java/com/sampoom/purchase/common/entity/Workspace.java
ADMIN, USER 역할 및 MD, SALES, INVENTORY 등 조직 타입을 나타내는 열거형 추가
커스텀 예외
src/main/java/com/sampoom/purchase/common/exception/CustomAuthenticationException.java
ErrorStatus를 포함하는 AuthenticationException 서브클래스 추가
오류 상태 확장
src/main/java/com/sampoom/purchase/common/response/ErrorStatus.java
JWT 토큰 검증 및 권한 관련 오류 상태 15개 추가 (토큰 만료, 유효하지 않은 토큰, 접근 거부 등)

시퀀스 다이어그램

sequenceDiagram
    participant Client
    participant JwtAuthFilter
    participant JwtProvider
    participant SecurityContext
    participant CustomAuthEntryPoint

    Client->>JwtAuthFilter: HTTP 요청
    JwtAuthFilter->>JwtProvider: 토큰 추출 (쿠키/헤더)
    
    alt 토큰 없음
        JwtProvider-->>JwtAuthFilter: null/blank
        JwtAuthFilter->>Client: 요청 계속 진행
    else 토큰 존재
        JwtProvider->>JwtProvider: JWT 파싱 및 검증
        
        alt 토큰 유효함
            JwtProvider-->>JwtAuthFilter: Claims
            JwtAuthFilter->>JwtAuthFilter: 토큰 타입 확인
            
            alt Service 토큰
                JwtAuthFilter->>SecurityContext: SVC_AUTH 권한 설정
            else Refresh 토큰
                JwtAuthFilter->>CustomAuthEntryPoint: NOT_ACCESS_TOKEN 오류
            else 일반 토큰
                JwtAuthFilter->>SecurityContext: 사용자 정보 및 권한 설정
            end
            
            JwtAuthFilter->>Client: 요청 계속 진행
        else 토큰 유효하지 않음
            JwtProvider-->>JwtAuthFilter: 예외 발생
            JwtAuthFilter->>CustomAuthEntryPoint: CustomAuthenticationException
            CustomAuthEntryPoint->>Client: 401/400 오류 응답
        end
    end
Loading

예상 코드 리뷰 소요 시간

🎯 3 (중간) | ⏱️ ~25분

추가 검토 필요 영역:

  • JwtProvider.java: RSA 공개키 로드 및 검증 로직, 토큰 파싱 오류 처리 흐름
  • JwtAuthFilter.java: 토큰 타입별 권한 매핑 및 SecurityContext 설정 로직의 정확성
  • SecurityConfig.java: 경로별 권한 설정이 의도대로 적용되었는지 확인 필요 (공개/인증 경로 구분)
  • ErrorStatus.java: 새로운 오류 코드들의 HTTP 상태 코드 매핑이 적절한지 검증

관련된 PR

  • PR #1: PurchaseOrder 엔티티의 SoftDeleteEntity 임포트 경로 오류를 본 PR에서 수정하였으므로 직접적인 연관이 있습니다.

제안된 레이블

ready-to-merge

제안된 리뷰어

  • yangjiseonn
  • vivivim
  • CHOOSLA
  • Lee-Jong-Jin

시 🐰

토큰을 검증하며 보안의 문을 열고,
JWT의 마법으로 사용자를 맞이하네.
필터와 설정이 춤을 추고,
역할과 권한이 제 자리를 찾아
새로운 인증 시스템의 탄생! 🔐✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 PR의 주요 변경 사항을 명확하게 요약합니다. JWT 토큰 인증 기능 추가라는 핵심 변경 내용을 정확히 반영하고 있습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch SPM-506

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@Lee-Jong-Jin Lee-Jong-Jin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제대로 넣었군

public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthFilter, CustomAuthEntryPoint customAuthEntryPoint, CustomAccessDeniedHandler customAccessDeniedHandler) throws Exception {
http
// CodeQL [java/spring-disabled-csrf-protection]: suppress - Stateless JWT API라 CSRF 불필요
.csrf(csrf -> csrf.disable())

Check failure

Code scanning / CodeQL

Disabled Spring CSRF protection High

CSRF vulnerability due to protection being disabled.

Copilot Autofix

AI 7 months ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
src/main/java/com/sampoom/purchase/common/config/jwt/JwtProvider.java (3)

24-36: 생성자의 예외 처리를 단순화할 수 있습니다.

Lines 30-31에서 BadRequestException을 다시 던지는 것은 불필요합니다. loadPublicKey 메서드에서 이미 BadRequestException을 던지고 있으므로, catch 블록을 단순화할 수 있습니다.

     public JwtProvider(@Value("${jwt.public-key-base64}") String publicKeyBase64) {
         if (publicKeyBase64 == null || publicKeyBase64.isBlank()) {
             throw new BadRequestException(ErrorStatus.INVALID_PUBLIC_KEY);
         }
         try {
             this.publicKey = loadPublicKey(publicKeyBase64);
-        } catch (BadRequestException e) {
-            throw e;
         } catch (Exception e) {
             throw new BadRequestException(ErrorStatus.INVALID_PUBLIC_KEY);
         }
     }

38-54: 예외 처리를 단순화할 수 있습니다.

Lines 49-51에서 BadRequestException을 다시 던지는 것은 불필요합니다. 이미 던져진 예외를 그대로 전파하면 됩니다.

     private PublicKey loadPublicKey(String base64) throws Exception {
         try {
             byte[] keyBytes = Base64.getDecoder().decode(base64);
             PublicKey key = KeyFactory.getInstance("RSA")
                     .generatePublic(new X509EncodedKeySpec(keyBytes));
             if (key instanceof RSAPublicKey rsaKey) {
                 if (rsaKey.getModulus().bitLength() < 2048) {
                     throw new BadRequestException(ErrorStatus.SHORT_PUBLIC_KEY);
                 }
             }
             return key;
-        } catch (BadRequestException e) {
-            throw e;
         } catch (Exception e) {
             throw new BadRequestException(ErrorStatus.INVALID_PUBLIC_KEY);
         }
     }

73-88: 쿠키 이름을 상수로 추출하는 것을 고려하세요.

Line 77에서 "ACCESS_TOKEN" 문자열이 하드코딩되어 있습니다. 이를 클래스 상수로 추출하면 유지보수성과 일관성이 향상됩니다.

+    private static final String ACCESS_TOKEN_COOKIE_NAME = "ACCESS_TOKEN";
+
     public String resolveAccessToken(HttpServletRequest request) {
         // 쿠키에서 ACCESS_TOKEN 찾기
         if (request.getCookies() != null) {
             for (Cookie cookie : request.getCookies()) {
-                if ("ACCESS_TOKEN".equals(cookie.getName())) {
+                if (ACCESS_TOKEN_COOKIE_NAME.equals(cookie.getName())) {
                     return cookie.getValue();
                 }
             }
         }
         // Bearer 방식일 때
         String header = request.getHeader("Authorization");
         if (header == null) return null;
         if (!header.startsWith("Bearer "))
             throw new UnauthorizedException(ErrorStatus.INVALID_TOKEN);
         return header.substring(7); // "Bearer " 제거
     }
src/main/java/com/sampoom/purchase/common/config/security/CustomAuthEntryPoint.java (1)

20-44: 공통 ObjectMapper 빈을 주입해 주세요

전역에서 커스터마이징된 ObjectMapper(예: JavaTimeModule, naming 전략 등)를 사용하고 있는데, 여기서 new ObjectMapper()를 직접 생성하면 해당 설정이 적용되지 않아 보안 응답 직렬화가 깨질 수 있습니다. 스프링 컨테이너에 등록된 ObjectMapper 빈을 주입받도록 변경하는 편이 안전합니다.

@@
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.sampoom.purchase.common.exception.CustomAuthenticationException;
-import com.sampoom.purchase.common.response.ApiResponse;
-import com.sampoom.purchase.common.response.ErrorStatus;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import lombok.extern.slf4j.Slf4j;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sampoom.purchase.common.exception.CustomAuthenticationException;
+import com.sampoom.purchase.common.response.ApiResponse;
+import com.sampoom.purchase.common.response.ErrorStatus;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
@@
-@Slf4j
-@Component
-public class CustomAuthEntryPoint implements AuthenticationEntryPoint {
-
-    private final ObjectMapper objectMapper = new ObjectMapper();
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class CustomAuthEntryPoint implements AuthenticationEntryPoint {
+
+    private final ObjectMapper objectMapper;
src/main/java/com/sampoom/purchase/common/config/security/CustomAccessDeniedHandler.java (1)

20-37: 전역 ObjectMapper 설정을 재사용해 주세요

이곳에서도 new ObjectMapper()로 별도 인스턴스를 만들면 글로벌 모듈/설정이 반영되지 않아 직렬화 형태가 다른 API와 달라지거나 실패할 수 있습니다. 빈으로 등록된 ObjectMapper를 주입해서 동일한 설정을 적용해 주세요.

@@
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.sampoom.purchase.common.response.ApiResponse;
-import com.sampoom.purchase.common.response.ErrorStatus;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import lombok.extern.slf4j.Slf4j;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sampoom.purchase.common.response.ApiResponse;
+import com.sampoom.purchase.common.response.ErrorStatus;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
@@
-@Slf4j
-@Component
-public class CustomAccessDeniedHandler implements AccessDeniedHandler {
-
-    private final ObjectMapper objectMapper = new ObjectMapper();
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class CustomAccessDeniedHandler implements AccessDeniedHandler {
+
+    private final ObjectMapper objectMapper;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ff72fe5 and 69a2714.

📒 Files selected for processing (14)
  • build.gradle (1 hunks)
  • src/main/java/com/sampoom/purchase/api/purchase/entity/PurchaseOrder.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/config/jwt/JwtAuthFilter.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/config/jwt/JwtProvider.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/config/security/CustomAccessDeniedHandler.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/config/security/CustomAuthEntryPoint.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/config/security/RoleHierarchyConfig.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/config/security/SecurityConfig.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/entity/BaseTimeEntity.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/entity/Role.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/entity/SoftDeleteEntity.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/entity/Workspace.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/exception/CustomAuthenticationException.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/response/ErrorStatus.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
src/main/java/com/sampoom/purchase/common/config/jwt/JwtProvider.java (3)
src/main/java/com/sampoom/purchase/common/exception/BadRequestException.java (1)
  • BadRequestException (6-19)
src/main/java/com/sampoom/purchase/common/exception/CustomAuthenticationException.java (1)
  • CustomAuthenticationException (6-17)
src/main/java/com/sampoom/purchase/common/exception/UnauthorizedException.java (1)
  • UnauthorizedException (6-18)
src/main/java/com/sampoom/purchase/common/config/security/RoleHierarchyConfig.java (1)
src/main/java/com/sampoom/purchase/common/config/security/SecurityConfig.java (1)
  • Configuration (20-84)
src/main/java/com/sampoom/purchase/common/config/jwt/JwtAuthFilter.java (3)
src/main/java/com/sampoom/purchase/common/exception/CustomAuthenticationException.java (1)
  • CustomAuthenticationException (6-17)
src/main/java/com/sampoom/purchase/common/config/security/CustomAuthEntryPoint.java (1)
  • Slf4j (16-47)
src/main/java/com/sampoom/purchase/common/config/jwt/JwtProvider.java (1)
  • Component (19-89)
src/main/java/com/sampoom/purchase/common/config/security/CustomAccessDeniedHandler.java (3)
src/main/java/com/sampoom/purchase/common/config/jwt/JwtAuthFilter.java (1)
  • Slf4j (26-135)
src/main/java/com/sampoom/purchase/common/config/security/CustomAuthEntryPoint.java (1)
  • Slf4j (16-47)
src/main/java/com/sampoom/purchase/common/config/jwt/JwtProvider.java (1)
  • Component (19-89)
src/main/java/com/sampoom/purchase/common/config/security/SecurityConfig.java (1)
src/main/java/com/sampoom/purchase/common/config/security/RoleHierarchyConfig.java (1)
  • Configuration (8-29)
src/main/java/com/sampoom/purchase/common/config/security/CustomAuthEntryPoint.java (4)
src/main/java/com/sampoom/purchase/common/exception/CustomAuthenticationException.java (1)
  • CustomAuthenticationException (6-17)
src/main/java/com/sampoom/purchase/common/config/jwt/JwtAuthFilter.java (1)
  • Slf4j (26-135)
src/main/java/com/sampoom/purchase/common/config/security/CustomAccessDeniedHandler.java (1)
  • Slf4j (16-41)
src/main/java/com/sampoom/purchase/common/config/jwt/JwtProvider.java (1)
  • Component (19-89)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (java-kotlin)
🔇 Additional comments (6)
src/main/java/com/sampoom/purchase/common/entity/Workspace.java (1)

3-13: LGTM!

워크스페이스 타입을 명확하게 정의한 enum입니다. 주석으로 각 타입의 의미가 잘 설명되어 있습니다.

src/main/java/com/sampoom/purchase/common/entity/BaseTimeEntity.java (1)

1-1: 패키지 명 오타 수정 확인.

entitiy에서 entity로 올바르게 수정되었습니다.

src/main/java/com/sampoom/purchase/common/response/ErrorStatus.java (1)

27-50: LGTM!

JWT 인증 및 권한 관리를 위한 에러 상태 코드들이 잘 정의되어 있습니다. HTTP 상태 코드별로 논리적으로 그룹화되어 있고, 한글 메시지도 명확합니다.

src/main/java/com/sampoom/purchase/common/entity/SoftDeleteEntity.java (1)

1-1: 패키지 명 오타 수정 확인.

entitiy에서 entity로 올바르게 수정되었습니다.

src/main/java/com/sampoom/purchase/api/purchase/entity/PurchaseOrder.java (1)

3-3: import 경로 수정 확인.

SoftDeleteEntity의 패키지 명 수정에 따라 import 경로가 올바르게 업데이트되었습니다.

src/main/java/com/sampoom/purchase/common/entity/Role.java (1)

3-6: LGTM!

역할(Role)을 명확하게 정의한 enum입니다. 타입 안전성을 제공하며 간결합니다.

Comment thread build.gradle
@taemin3 taemin3 merged commit 578ffee into main Nov 11, 2025
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants