JWT(Json Web Token) | JeongKeepsCalm

JWT(Json Web Token)

JWT 사용 이유

  • 모바일 앱
    • 상태 유지 어려움
      모바일 앱은 네트워크 연결이 불안정할 수 있어, 세션 상태를 유지하는 것이 어렵다.
    • 확장성 문제
      세션 방식은 서버에 상태를 저장하므로, 서버의 부하가 증가할 수 있다.
      모바일 앱은 많은 사용자가 동시에 접속할 수 있어, 서버 확장성이 중요한데 세션 방식은 이를 어렵게 한다.
  • 장시간 로그인과 세션
    장기간 동안 로그인 상태를 유지하려고 세션 설정을 하면 서버 측 부하가 많이 가기 때문에 JWT 방식을 이용

토큰 사용 추적

  1. 로그인 성공 JWT 발급 : 서버측 → 클라이언트로 JWT 발급
  2. 권한이 필요한 모든 요청 : 클라이언트 → 서버측 JWT 전송

클라이언트 측에서 XSS를 이용하거나 HTTP 통신을 가로채서 토큰을 훔칠 수 있기 때문에 탈취 대비 로직 필요


다중 토큰: Access/Refresh

  1. 로그인 성공 시 생명주기와 활용도가 다른 토큰 2개 발급: Access/Refresh
    • Access 토큰: 권한이 필요한 모든 요청 헤더에 사용될 JWT로 탈취 위험을 낮추기 위해 약 10분 정도의 짧은 생명주기를 가진다.
    • Refresh 토큰: Access 토큰이 만료되었을 때 재발급 받기 위한 용도로만 사용되며 약 24시간 이상의 긴 생명주기를 가진다.
  2. 권한이 필요한 모든 요청: Access 토큰을 통해 요청
  3. 토큰이 만료된 경우 Refresh 토큰으로 Access 토큰 발급
    • Refresh 토큰을 가지고 서버의 특정 경로(Refresh 토큰을 받는 경로)에 요청을 보내어 Access 토큰을 재발급 받는다.

다중 토큰 구현

  • 로그인 완료 → successHandler에서 Access/Refresh 토큰 2개를 발급해 응답
    각 토큰은 각기 다른 생명주기, payload 정보를 가진다.
  • Access 토큰 요청을 검증하는 JWTFilter에서 Access 토큰이 만료된 경우는 프론트 개발자와 협의된 상태 코드와 메시지를 응답
  • 프론트측 API 클라이언트 (axios, fetch) 요청시 Access 토큰 만료 요청이 오면 예외문을 통해 Refresh 토큰을 서버측으로 전송하고 Access 토큰을 발급 받는 로직을 수행한다. (기존 Access는 제거)
  • 서버측에서는 Refresh 토큰을 받을 엔드포인트 (컨트롤러)를 구성하여 Refresh를 검증하고 Access를 응답한다.

Access/Refresh 토큰 저장 위치

  • Access Token: 로컬 스토리지(XSS 공격에 취약)
    • 탈취에서 사용까지 기간이 매우 짧은 accessToken을 로컬스토리지에 저장한다.
    • 권한이 필요한 모든 경로에 사용되기 때문에 CSRF 공격의 위험보다는 XSS 공격을 받는 게 더 나은 선택일 수 있다.
  • Refresh Token: httpOnly 쿠키(CSRF 공격에 취약)
    • 쿠키는 XSS 공격을 받을 수 있지만 httpOnly를 설정하면 완벽히 방어할 수 있다.
    • CSRF는 accessToken이 접근하는 CRUD에 취약하지만 토큰 재발급 경로에서는 크게 피해를 입힐 만한 로직이 없기 때문에 쿠키에 저장한다.

로그아웃과 Refresh 토큰 주도권

  • 문제
    로그아웃 시 프론트에 존재하는 Access/Refresh 토큰을 제거한다. 프론트 측에서는 요청을 보낼 JWT가 없기 때문에 로그아웃 되었다고 생각하지만 이전에 해커가 JWT를 복제했다면 요청이 수행된다.
  • 해결방안
    생명주기가 긴 Refresh 토큰은 발급과 함께 서버측 저장소에도 저장하여 요청이 올때마다 저장소에 존재하는지 확인하는 방법으로 서버측에서 주도권을 가져온다. 만약 로그아웃을 진행하거나 탈취에 의해 피해가 진행되는 경우 서버측 저장소에서 해당 JWT를 삭제하여 피해를 방어할 수 있다.(Refresh 토큰 블랙리스팅)

Refresh Rotate

  • Reissue 엔드포인트에서 Refresh 토큰을 받아 Access 토큰 갱신 시 Refresh 토큰도 함께 갱신하는 방법
  • 발급했던 Refresh 토큰을 모두 기억한 뒤, Rotate 이전의 Refresh 토큰은 사용하지 못하도록 해야한다.
  • 장점
    • Refresh 토큰 교체로 보안성 강화
    • 로그인 지속시간 길어짐

JWT 구현 요약

  • SecurityConfig
    • formLogin 비활성화 -> UsernamePasswordAuthenticationFilter 필터 비활성화
    • Session: stateless 설정
  • JwtFilter
    • accessToken 검증
      • Header 검증
        • accessToken 여부 확인
        • accessToken 만료 확인
        • 해당 토큰이 accessToken인지 확인
          • 해당 토큰이 아니거나, 토큰 만료 시 프론트 측과 협의된 응답코드 세팅하여 응답
      • DB 검증(Redis로 변경 필요)
        • DB에 accessToken 존재 여부 확인
        • accessToken 값으로 DB에 저장된 IP와 현재 요청 들어온 IP를 비교
          • 같지 않다면 로그아웃 처리 후, 재로그인 유도
      • 모든 검증에 통과되면 매 요청마다 header 내 저장된 토큰 정보를 가지고 세션에 새로 저장
  • LoginFilter(UsernamePasswordAuthenticationFilter 구현체)
    • SecurityContextHolder를 통해 사용자의 인증 상태를 확인되면 UsernamePasswordAuthenticationFilter 구현체가 더이상 호출되지 않는다.
    • attemptAuthentication
      • AuthenticationManager를 사용하여 사용자 인증 수행
      • AuthenticationManager는 UserDetailsService를 사용하여 사용자 정보를 로드하고, 비밀번호를 검증
    • successfulAuthentication / unsuccessfulAuthentication
      • successfulAuthentication
        • accessToken, refreshToken 생성
        • accessToken, refreshToken DB 저장
        • accessToken: 헤더 응답
        • refreshToken: 쿠키 설정
  • ReissueRestController
    • refreshToken 검증
      • 쿠키 검증
        • 쿠키에 저장된 refreshToken 가져온다.
        • refreshToken 존재 여부 확인
        • refreshToken 만료 여부 확인
        • 토큰이 refreshToken 인지 확인(페이로드 정보)
          • refreshToken 검증에 실패하면 모두 badRequest 응답
      • DB 검증(Redis로 변경 필요)
        • DB에 refreshToken이 저장되어 있는지 확인
      • 모든 검증 통과 시
        • accessToken 재발급
        • refreshToken 재발급(refresh rotate)
        • DB에 refreshToken 삭제 후 저장
        • DB에서 accessToken은 삭제 불가능
          • 애초에 /reissue 요청으로 넘어왔다는 건 accessToken 이 만료되었던 것이고, 클라이언트에서 요청 보낼 때, 헤더에 만료된 accessToken 을 보내지 않는다.
        • accessToken: 헤더 응답
        • refreshToken: 쿠키 설정
    • CustomLogoutFilter
      • LogoutFilter 의 부모 GenericFilterBean 상속
      • 검증
        • 모든 요청시 필터를 거치기때문에, “/logout” url 검증, method 검증(post method 확인)
        • 쿠키
          • refreshToken 존재 여부 확인
          • refreshToken 만료 여부 확인
        • DB(Redis로 변경 필요)
          • DB에 refreshToken이 저장되어 있는지 확인
      • DB에 저장된 accessToken, refreshToken 삭제
      • 쿠키에 저장된 refreshToken 삭제
    • JwtCleanupScheduler
      • 만료된 accessToken, refreshToken 을 삭제한다.
  • CORS(교차 출처(다른 포트번호) 리소스 공유되도록 설정)
    • SecurityConfig 내, WebMvcConfigurer 구현체에 코드 추가 필요