Skip to content

Conversation

@yeokyeong
Copy link
Collaborator

요구사항

프로젝트 마일스톤

  • JWT 기반 인증 / 인가

주요 변경 사항

프로젝트 버전이 변경되었습니다. v2.1-M10

  • 세부 사항

    # build.gradle
    ...
    version = '2.0-M9'version = '2.1-M10'
    ...
    
    • 2.1api-doc 버전을 따릅니다.
    • M10: 미션 10을 의미합니다.

프론트엔드가 변경되었습니다.

1. JWT 컴포넌트 구현

2. 리팩토링 - 로그인

  • 미션 9와 마찬가지로 Spring Security의 formLogin + 미션 9의 인증 흐름은 그대로 유지하면서 필요한 부분만 대체합니다.

  • 세션 생성 정책을 STATELESS로 변경하고, sessionConcurrency 설정을 삭제하세요.

    http
        .sessionManagement(session -> session
            ...
            .sessionCreationPolicy(...)
        )
  • AuthenticationSuccessHandler 컴포넌트를 대체하세요.

    • 기존 구현체는 LoginSuccessHandler입니다.

    • JwtLoginSuccessHandler를 정의하고 대체하세요.

      @Component
      public class LoginSuccessHandler implements AuthenticationSuccessHandler {
          ...
          @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
            ...
        }
      }
    • 설정에 추가하세요.

      http
          .formLogin(login -> login
              ...
              .successHandler(jwtLoginSuccessHandler)
          )

    3. JWT 인증 필터 구현

    • 엑세스 토큰을 통해 인증하는 필터(JwtAuthenticationFilter)를 구현하세요.

      public class JwtAuthenticationFilter extends OncePerRequestFilter {
      
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {...}
      • 요청 당 한번만 실행되도록 OncePerRequestFilter를 상속하세요.

      • 요청 헤더(Authorization)에 Bearer 토큰이 포함된 경우에만 인증을 시도하세요.

      • JwtProvider를 통해 엑세스 토큰의 유효성을 검사하세요.

      • 유효한 토큰인 경우 UsernamePasswordAuthenticationToken 객체를 활용해 인증 완료 처리하세요.

        UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                    );
        SecurityContextHolder.getContext().setAuthentication(authentication);

    4. 리프레시 토큰을 활용한 엑세스 토큰 재발급

    • 리프레시 토큰을 활용해 엑세스 토큰을 재발급하는 API를 구현하세요.
      • API 스펙
        • 엔드포인트: POST /api/auth/refresh
        • 요청: Header Cookie: REFRESH_TOKEN=…
        • 응답
          • 리프레시 토큰이 유효한 경우: 200 JwtDto
          • 리프레시 토큰이 유효하지 않은 경우: 401 ErrorResponse
      • permitAll 설정에 포함하세요.
        • 이 API는 엑세스 토큰이 없거나 만료된 상태에서 호출하게 됩니다.
    • 리프레시 토큰 Rotation을 통해 보안을 강화하세요.
    • 토큰 재발급 API로 대체할 수 있는 컴포넌트를 모두 삭제하세요.
      • Me API (GET /auth/me)
      • 프론트엔드 2.0.x과 마찬가지로 2.1.x에서는 사용자 정보와 엑세스 토큰 정보를 브라우저의 메모리에서 관리합니다.
      • 따라서 새로고침 시 쿠키에 저장된 리프레시 토큰을 통해 엑세스 토큰을 갱신합니다.
    • RememberMe
      • 쿠키에 저장된 리프레시 토큰이 RememberMe의 기능을 대체할 수 있습니다.

    5. 리팩토링 - 로그아웃

    • 쿠키에 저장된 리프레시 토큰을 삭제하는 LogoutHandler를 구현하세요.

      public class JwtLogoutHandler implements LogoutHandler {
      
        @Override
        public void logout(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) {...}
    • 구현한 핸들러를 추가하세요.

      http
        .logout(logout -> logout
            ...
            .addLogoutHandler(jwtLogoutHandler)
        )

    6. 심화) 리팩토링 - 토큰 상태 관리

    • 토큰 기반 인증 방식은 세션 기반 인증 방식과 달리 무상태(stateless)이기 때문에 사용자의 로그인 상태를 제어하기 어렵습니다.

    • 따라서 SessionRegistry를 통해 세션의 상태를 관리했던 것처럼, JWT의 상태를 관리할 수 있는 컴포넌트를 추가해야합니다.

    • 토큰의 상태를 관리하는 JwtRegistry를 구현하세요.

      [9da9kvl8y-image.png](https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=14442&version=1&directory=/9da9kvl8y-image.png&name=9da9kvl8y-image.png)

      • JwtRegistry
        • registerJwtInformation
          • 로그인 성공 시 JwtInformation을 등록합니다.
          • 최대 동시 로그인 수(1)를 제어합니다.
        • invalidateJwtInformationByUserId: UserId로 해당 유저의 모든 JwtInformation 정보를 삭제합니다.
        • hasActiveJwtInformationBy*JwtInformation이 Registry에 존재하는지 확인합니다.
          • ByUserId: 사용자의 로그인 상태를 판단할 때 활용합니다.
          • ByAccessToken: 필터에서 유효한 토큰인지 확인할 때 활용합니다.
          • ByRefreshToken: 토큰 재발급 시 유효한 토큰인지 확인할 때 활용합니다.
        • rotateJwtInformation: 토큰 재발급 시 토큰 로테이션을 수행합니다.
        • clearExpiredJwtInformation: 만료된 JwtInformation을 삭제합니다.
      • InMemoryJwtRegistry
        • 메모리에 JwtInformation을 저장하는 JwtRegistry 구현체입니다.

        • 동시성 처리를 위해 다음과 같이 구성하세요. 동시성에 대해서는 다음 미션에서 학습합니다.

          public class InMemoryJwtRegistry implements JwtRegistry {
          
            // <userId, Queue<JwtInformation>>
            private final Map<UUID, Queue<JwtInformation>> origin = new ConcurrentHashMap<>();
            private final int maxActiveJwtCount;
              ...
          }
    • JwtAuthenticationFilter에서 JwtRegistry를 활용해 토큰의 상태를 검사하는 로직을 추가하세요.

    • JwtRegistry를 활용해 동시 로그인 제한 기능을 리팩토링하세요.

      • 동일한 계정으로 로그인 시 기존 로그인 세션을 무효화합니다.
    • JwtRegistry를 활용해 권한이 변경된 사용자가 로그인 상태라면 강제로 로그아웃되도록 하세요.

    • JwtRegistry를 활용해 사용자의 로그인 여부를 판단하도록 리팩토링하세요.

    • JwtLogoutHandler에서 JwtRegistry를 활용해 로그아웃 시 토큰을 무효화하세요.

        @Override
        public void logout(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) {
          ...
          Arrays.stream(request.getCookies())
              .filter(cookie -> cookie.getName().equals(JwtTokenProvider.REFRESH_TOKEN_COOKIE_NAME))
              .findFirst()
              .ifPresent(cookie -> {...});
              ...
        }
      • 로그아웃 API는 인증이 필요없기 때문에 Authentication 정보가 없을 수 있습니다.
      • 따라서 요청 쿠키의 리프레시 토큰을 활용해 토큰을 무효화합니다.
    • 주기적으로 만료된 토큰 정보를 레지스트리에서 삭제하세요.

      • @EnableScheduling를 추가하세요.

            @Configuration
            @EnableJpaAuditing
            @EnableSchedulingpublic class AppConfig {
        
            }
      • @Scheduled를 활용해서 5분마다 만료된 토큰을 삭제하세요.

          @Scheduled(fixedDelay = 1000 * 60 * 5)@Override
          public void clearExpiredJwtInformation() {...}

멘토에게

  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.
  • 늦어서 죄송합니다 🥹

@yeokyeong yeokyeong self-assigned this Aug 28, 2025
@yeokyeong yeokyeong added 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. 지각제출⏰ 제출일 이후에 늦게 제출한 PR입니다. labels Aug 28, 2025
@yeokyeong yeokyeong requested a review from azjaehyun August 28, 2025 07:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. 지각제출⏰ 제출일 이후에 늦게 제출한 PR입니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant