[전명훈] sprint10 - #208
[전명훈] sprint10#208coje1111 wants to merge 28 commits into
Conversation
There was a problem hiding this comment.
전체 요약
Spring Security 기반 JWT 인증 체계(액세스/리프레시 토큰, 토큰 로테이션, InMemoryJwtRegistry)를 완성도 높게 구현했습니다. 테스트 커버리지도 충실하며 전체적으로 잘 설계됐네요. 다만 레이스 컨디션과 보안 설정에 수정이 필요한 부분이 있습니다.
P4: JwtTokenProvider - refreshAccessToken 미사용 메서드
PR 내 어디에서도 호출되지 않는 것으로 보입니다. AuthController의 /refresh 엔드포인트는 generateAccessToken을 직접 사용합니다. 사용되지 않는 public API를 제거하거나, 실제로 사용할 계획이라면 AuthController에서 이 메서드를 활용하도록 통일하는 것을 제안합니다.
| jwtInformations.removeIf(jwtInformation -> | ||
| jwtInformation.refreshTokenExpiresAt().isBefore(now)); | ||
| } | ||
| if (jwtInformations.isEmpty()) { |
There was a problem hiding this comment.
P2: clearExpiredJwtInformation 레이스 컨디션
isEmpty() 체크와 origin.remove() 사이에 다른 스레드가 registerJwtInformation()으로 새 항목을 추가하면, 해당 큐가 origin에서 통째로 삭제됩니다. 이 경우 방금 로그인한 사용자의 JWT 정보가 레지스트리에서 사라져 즉시 401 응답을 받게 됩니다.
synchronized 블록 안에서 empty 체크와 remove를 함께 처리해야 합니다:
| if (jwtInformations.isEmpty()) { | |
| synchronized (jwtInformations) { | |
| jwtInformations.removeIf(jwtInformation -> | |
| jwtInformation.refreshTokenExpiresAt().isBefore(now)); | |
| if (jwtInformations.isEmpty()) { | |
| origin.remove(userId, jwtInformations); | |
| } | |
| } |
| private final long refreshTokenValiditySeconds; | ||
|
|
||
| public JwtTokenProvider( | ||
| @Value("${discodeit.jwt.secret:discodeit-mission-10-default-jwt-secret-key}") String secret, |
There was a problem hiding this comment.
P2: JWT 기본 시크릿 소스코드 하드코딩
기본 시크릿이 소스코드에 노출되어 있습니다. 환경변수를 설정하지 않으면 저장소를 볼 수 있는 누구나 유효한 JWT를 직접 생성할 수 있습니다. 기본값을 제거하고 시크릿 미설정 시 애플리케이션 시작을 실패시키는 것이 안전합니다.
| @Value("${discodeit.jwt.secret:discodeit-mission-10-default-jwt-secret-key}") String secret, | |
| @Value("${discodeit.jwt.secret}") String secret, |
| Cookie refreshTokenCookie = new Cookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken); | ||
| refreshTokenCookie.setHttpOnly(true); | ||
| refreshTokenCookie.setPath("/"); | ||
| response.addCookie(refreshTokenCookie); |
There was a problem hiding this comment.
P3: 리프레시 토큰 쿠키 Secure 플래그 누락
Secure 플래그가 없으면 HTTP 요청에서도 쿠키가 전송되어 중간자 공격에 노출될 수 있습니다. AuthController.createRefreshTokenCookie()도 동일하게 수정해 주세요.
| response.addCookie(refreshTokenCookie); | |
| Cookie refreshTokenCookie = new Cookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken); | |
| refreshTokenCookie.setHttpOnly(true); | |
| refreshTokenCookie.setSecure(true); | |
| refreshTokenCookie.setPath("/"); | |
| response.addCookie(refreshTokenCookie); |
|
|
||
| @Override | ||
| public boolean hasActiveJwtInformationByUserId(UUID userId) { | ||
| return origin.getOrDefault(userId, new ArrayDeque<>()).stream() |
There was a problem hiding this comment.
P4: 불필요한 ArrayDeque 생성
userId가 존재하지 않을 때마다 빈 ArrayDeque를 새로 생성합니다. get()으로 null 체크하는 방식이 더 효율적입니다.
| return origin.getOrDefault(userId, new ArrayDeque<>()).stream() | |
| Queue<JwtInformation> jwtInformations = origin.get(userId); | |
| if (jwtInformations == null) { | |
| return false; | |
| } | |
| return jwtInformations.stream().anyMatch(this::isActive); |
sprint 10
기본 요구사항
1. JWT 컴포넌트 구현
implementation 'com.nimbusds:nimbus-jose-jwt:10.3'JwtTokenProvider)를 구현하세요.2. 리팩토링 - 로그인
STATELESS로 변경하고,sessionConcurrency설정을 삭제하세요.AuthenticationSuccessHandler컴포넌트를 대체하세요.LoginSuccessHandler입니다.JwtLoginSuccessHandler를 정의하고 대체하세요.JwtProvider를 활용해 토큰을 발급하세요.REFRESH_TOKEN)에 저장하세요.200 JwtDto로 응답합니다.3. JWT 인증 필터 구현
JwtAuthenticationFilter)를 구현하세요.OncePerRequestFilter를 상속하세요.Authorization)에 Bearer 토큰이 포함된 경우에만 인증을 시도하세요.JwtProvider를 통해 엑세스 토큰의 유효성을 검사하세요.UsernamePasswordAuthenticationToken객체를 활용해 인증 완료 처리하세요.4. 리프레시 토큰을 활용한 엑세스 토큰 재발급
POST /api/auth/refreshHeader Cookie: REFRESH_TOKEN=…200 JwtDto401 ErrorResponsepermitAll설정에 포함하세요.GET /auth/me)2.0.x과 마찬가지로2.1.x에서는 사용자 정보와 엑세스 토큰 정보를 브라우저의 메모리에서 관리합니다.5. 리팩토링 - 로그아웃
LogoutHandler를 구현하세요.심화 요구사항
리팩토링 - 토큰 상태 관리
SessionRegistry를 통해 세션의 상태를 관리했던 것처럼, JWT의 상태를 관리할 수 있는 컴포넌트를 추가해야합니다.JwtRegistry를 구현하세요.JwtRegistryregisterJwtInformationJwtInformation을 등록합니다.1)를 제어합니다.invalidateJwtInformationByUserId: UserId로 해당 유저의 모든JwtInformation정보를 삭제합니다.hasActiveJwtInformationBy*:JwtInformation이 Registry에 존재하는지 확인합니다.ByUserId: 사용자의 로그인 상태를 판단할 때 활용합니다.ByAccessToken: 필터에서 유효한 토큰인지 확인할 때 활용합니다.ByRefreshToken: 토큰 재발급 시 유효한 토큰인지 확인할 때 활용합니다.rotateJwtInformation: 토큰 재발급 시 토큰 로테이션을 수행합니다.clearExpiredJwtInformation: 만료된JwtInformation을 삭제합니다.InMemoryJwtRegistryJwtInformation을 저장하는JwtRegistry구현체입니다.JwtAuthenticationFilter에서JwtRegistry를 활용해 토큰의 상태를 검사하는 로직을 추가하세요.JwtRegistry를 활용해 동시 로그인 제한 기능을 리팩토링하세요.JwtRegistry를 활용해 권한이 변경된 사용자가 로그인 상태라면 강제로 로그아웃되도록 하세요.JwtRegistry를 활용해 사용자의 로그인 여부를 판단하도록 리팩토링하세요.JwtLogoutHandler에서JwtRegistry를 활용해 로그아웃 시 토큰을 무효화하세요.Authentication정보가 없을 수 있습니다.@EnableScheduling를 추가하세요.@Scheduled를 활용해서 5분마다 만료된 토큰을 삭제하세요.멘토에게