Skip to content

Conversation

@m-a-king
Copy link
Collaborator

@m-a-king m-a-king commented Nov 26, 2024

resolved :

📌 과제 설명

JWT를 활용한 인증, 인가를 위한 토큰 관리 기능을 구현

👩‍💻 요구 사항과 구현 내용

1. JWT 생성 및 관리

  • JwtGenerator: JTI를 포함한 토큰 생성 로직 추가. 동일 시점의 토큰을 고유하게 구분.
  • JwtParser: 파싱 시 발생할 수 있는 예외 처리 로직 추가.
  • JwtValidator: boolean 반환을 예외 처리로 변경하여 명확한 검증 로직 구현.
  • JwtRefresher: 프론트엔드 협의를 통해 JWT 갱신 로직을 완성. RefreshToken 유효성 확인 후 AccessToken 갱신.

2. 인증 및 필터링

  • JwtAuthenticationToken: 사용자 인증 정보를 커스텀 인증 토큰으로 관리.
  • JwtAuthFilter: JWT를 기반으로 한 인증 필터 구현. SecurityContext에 사용자 정보 설정.
  • JwtExceptionFilter: JWT 예외를 처리하는 전용 필터 추가. (스프링 시큐리티의 필터 계층)
  • SecurityConfig: JwtAuthFilter, JwtExceptionFilter를 Spring Security Filter Chain에 추가.

3. 예외 처리

  • JwtException: JWT 처리 과정에서 발생하는 예외를 명확히 구분하도록 메시지(ENUM) 및 클래스를 추가.
  • RefreshTokenManager: RefreshToken 조회 및 예외 메시지 개선.

4. 테스트

  • JwtServiceTest:
    • JWT 생성, 검증, 갱신, 예외 처리 등 다양한 시나리오 테스트 작성.
    • 만료된 AccessToken 갱신, RefreshToken 유효성 검증, 쿠키 저장 테스트 추가.
    • 잘못된 JWT 토큰 및 만료된 토큰의 예외 발생 테스트.

✅ PR 포인트 & 궁금한 점

  1. 프론트엔드와 협의한 토큰 갱신 과정에 집중해 주세요! 리프레시 토큰이 있는 한, 엑세스 토큰이 만료된 것을 그들은 느낄 수 없습니다...

  2. Security 관련 어노테이션 사용 방법

@PreAuthorize("hasRole('CENTER')")
public void adminCENTER() {
    // CENTER 역할만 접근 가능
}
@GetMapping("/me")
public String getCurrentUser(@AuthenticationPrincipal String userId) {
    return userId; // SecurityContext에 저장된 userId 반환
}
@PreAuthorize("hasAuthority('VOLUNTEER')")
@GetMapping("/volunteer")
public String userAccess(@AuthenticationPrincipal String userId) {
    return "현재 사용자 ID: " + userId;
}

- verifyToken 메서드 제거 후 processAccessToken 메서드로 통합.
  - 만료된 AccessToken을 처리하고 갱신된 토큰을 Set-Cookie 헤더에 추가하도록 구현.
  - HttpServletResponse를 활용해 갱신된 AccessToken 설정 로직 추가.

- 만료된 AccessToken에 대한 예외 처리 로직(handleJwtExpiredException) 추가.
  - JwtException을 확인하고 만료된 토큰만 갱신 처리.
  - SetCookieUseCase 의존성 주입을 통해 클라이언트에 새로운 AccessToken 전달.

- JwtRefresher와 SetCookieUseCase를 활용하여 클라이언트 응답에 새로운 AccessToken을 설정하는 책임 분리.
- JwtValidator와 JwtParser를 활용한 검증 및 클레임 추출 구조 간소화.
- 사용자 정보를 담는 principal(userId)과 credentials(accessToken) 필드 추가
- Spring Security의 AbstractAuthenticationToken을 상속받아 SecurityContext와의 호환성 유지
- setAuthenticated(true)를 통해 인증 상태 설정 (생선 전에 검증함)
- 이후 SecurityContextHolder에서 principal 및 권한 정보를 추출하여 사용 가능
- HTTP 요청의 Authorization 헤더에서 JWT 추출
- 추출한 JWT를 검증하고 만료 시 리프레시 처리 (JwtUseCase 활용)
- JWT Claims에서 사용자 ID와 권한을 추출하여 JwtAuthenticationToken 생성
- SecurityContextHolder에 Authentication 객체 등록하여 인증 상태 관리
- shouldNotFilter 메서드로 특정 요청 URI에 대해 필터링 제외 가능 (현재는 개발 중 모든 요청 허용)
- JWT 처리 중 발생하는 JwtException을 전용 필터에서 처리 (스프링 컨텍스트가 아니므로)
- HTTP 상태 코드 401(Unauthorized)와 함께 ProblemDetail 형식의 JSON 응답 생성
- ProblemDetail 응답에 오류 제목, 유형, 상세 메시지, 타임스탬프 포함
- JwtAuthFilter: 요청 헤더에서 JWT를 검증하고, 인증 정보를 SecurityContext에 설정
- JwtExceptionFilter: JWT 처리 중 발생한 예외를 캡처하고 ProblemDetail 형식으로 응답 반환
- JWT 토큰 생성 및 검증 로직 테스트:
  - 토큰이 올바르게 생성되고, 예상한 클레임(`id`, `role`, `expiration`)을 포함하는지 확인.
  - 동일 사용자에 대해 여러 토큰 생성 시 서로 다른 값이어야 함을 검증.

- JWT 만료 및 갱신 로직 테스트:
  - 만료된 AccessToken이 RefreshToken이 유효한 경우 갱신되는지 확인.
  - RefreshToken이 유효하지 않거나 존재하지 않을 때 예외 발생을 검증.

- RefreshToken 관련 테스트:
  - 기존 RefreshToken이 올바르게 갱신되는지 확인.
  - RefreshToken이 없거나 잘못된 경우 예외 발생을 검증.

- JWT 예외 처리 테스트:
  - 잘못된 토큰 및 만료된 토큰이 적절한 `JwtException`을 던지는지 확인.

- RefreshToken 저장 및 유효성 확인 테스트:
  - RefreshToken 저장 로직 검증.
  - 갱신된 AccessToken이 쿠키에 올바르게 설정되는지 확인.
@m-a-king m-a-king self-assigned this Nov 26, 2024
@m-a-king m-a-king linked an issue Nov 26, 2024 that may be closed by this pull request
2 tasks
- principal 필드를 직렬화 가능하도록 수정
- credentials 필드는 인증 이후에 사용하지 않으니 직렬화 제외
- 세션 저장 및 분산 서버 환경에서의 호환성 확보
@7zrv
Copy link
Collaborator

7zrv commented Nov 26, 2024

고생하셨습니다 저는 문제되는 부분 못찾았습니다!

Copy link
Collaborator

@leebs0521 leebs0521 left a comment

Choose a reason for hiding this comment

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

테스트 코드까지 고생많으셨습니다.!!

}

private EncodedToken getAccessToken(HttpServletRequest request) {
String accessToken = request.getHeader("Authorization");
Copy link
Collaborator

Choose a reason for hiding this comment

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

getAccessToken() 메서드가 createAuthenticationToken() 보다 먼저오는게 좋아보여요

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

안목이 있으십니다.

public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private final Serializable principal;
private final transient Object credentials;

Copy link
Collaborator

Choose a reason for hiding this comment

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

transient 신기하네요

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

사실 저도 존재만 알고 있었고, 처음 사용해봤습니다 ㅎㅎ

ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, e.getMessage());
problemDetail.setTitle("Authentication Error");
problemDetail.setType(URI.create("http://프론트엔드주소/errors/unauthorized"));
problemDetail.setProperty("timestamp", System.currentTimeMillis());
Copy link
Collaborator

Choose a reason for hiding this comment

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

problemDetail.setType(URI.create("http://프론트엔드주소/errors/unauthorized")); 일부로 하드코딩 하신건가요?

Copy link
Collaborator Author

@m-a-king m-a-king Nov 26, 2024

Choose a reason for hiding this comment

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

네. 이 부분에 대해서 얘기해 봐야 할 것 같아요.

Claims claims = buildClaims(userId, role);
Instant now = Instant.now();
Instant expiration = now.plusMillis(tokenType.getPeriod());
String uniqueId = UUID.randomUUID().toString(); // JTI
Copy link
Collaborator

Choose a reason for hiding this comment

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

// JTI 이게 어떤 의미의 주석인가요???

Copy link
Collaborator Author

@m-a-king m-a-king Nov 26, 2024

Choose a reason for hiding this comment

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

https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7

여기 확인해 보시면, jwt에 유니크한 값을 삽입해서 동일 시점, 동일 클레임으로 생성된 토큰 간의 차이점을 만드는 것이라고 확인하실 수 있습니다. 테스트하면서 발견해서 추가했습니다!

Copy link
Collaborator

@ayoung-dev ayoung-dev left a comment

Choose a reason for hiding this comment

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

수고하셨습니다~

Comment on lines +182 to +183
// when
// then
Copy link
Collaborator

Choose a reason for hiding this comment

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

// when & then 으로 통일하면 좋을 거 같아요

Comment on lines +230 to +231
// when
// then
Copy link
Collaborator

Choose a reason for hiding this comment

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

여기도요!

Comment on lines +200 to +201
// when
// then
Copy link
Collaborator

Choose a reason for hiding this comment

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

여기도요

Comment on lines +215 to +216
// when
// then
Copy link
Collaborator

Choose a reason for hiding this comment

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

여기도..!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

수정...했습니다!

@sonarqubecloud
Copy link

@m-a-king m-a-king merged commit 493d688 into main Nov 26, 2024
2 checks passed
@m-a-king m-a-king deleted the feature/42-auth-jwtfilter branch November 26, 2024 07:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] JwtFilter 인가 기능

5 participants