REDBOX(헌혈증 기부 시스템) - JWT 정리

2025. 1. 16. 18:07·Spring Security/JWT

1. JWT 기반 인증/인가 시스템 구축

JWTFilter

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
    throws ServletException, IOException {
    String accessToken = request.getHeader("access");

    if (accessToken == null) {
        filterChain.doFilter(request, response);
        return;
    }

    try {
        jwtUtil.isExpired(accessToken);

        String category = jwtUtil.getCategory(accessToken);
        if (!"access".equals(category)) {
            throw new InvalidTokenException();
        }

        Long userId = jwtUtil.getUserId(accessToken);
        String email = jwtUtil.getEmail(accessToken);
        String role = jwtUtil.getRole(accessToken).replace("ROLE_", "");

        User user = User.builder()
            .id(userId)
            .email(email)
            .roleType(RoleType.valueOf(role))
            .build();

        CustomUserDetails customUserDetails = new CustomUserDetails(user);

        Authentication authToken = new UsernamePasswordAuthenticationToken(
            customUserDetails, null, customUserDetails.getAuthorities()
        );
        SecurityContextHolder.getContext().setAuthentication(authToken);

        filterChain.doFilter(request, response);
    } catch (ExpiredJwtException e) {
        ErrorResponseUtil.handleException(response, ErrorCode.EXPIRED_TOKEN);
    } catch (Exception e) {
        ErrorResponseUtil.handleException(response, ErrorCode.INTERNAL_SERVER_ERROR);
    }
}

 

  • JWTFilter는 Access Token의 유효성을 검증, 토큰에서 사용자 정보를 추출하여 인증 객체를 생성.
  • 토큰의 "카테고리" 필드를 확인하여 access 토큰만 처리, 잘못된 토큰은 InvalidTokenException을 통해 예외 처리.
  • 유효한 인증 정보는 SecurityContextHolder에 저장되어 이후 요청에 대해 인증 상태를 유지.

2. 리프레시 토큰 기반 재발급 시스템 (Token Reissue)

ReissueService

public String reissueAccessToken(String refreshToken) {
    validateRefreshToken(refreshToken);

    Long userId = jwtUtil.getUserId(refreshToken);
    String email = jwtUtil.getEmail(refreshToken);
    String role = jwtUtil.getRole(refreshToken);

    return jwtUtil.createJwt("access", userId, email, role, 600_000L); // 10분
}

private void validateRefreshToken(String refreshToken) {
    if (refreshToken == null) {
        throw new RefreshTokenNotFoundException();
    }

    if (jwtUtil.isExpired(refreshToken)) {
        throw new ExpiredRefreshTokenException();
    }

    if (!"refresh".equals(jwtUtil.getCategory(refreshToken))) {
        throw new IllegalArgumentException("Invalid refresh token category");
    }

    if (!refreshTokenService.existsByRefreshToken(refreshToken)) {
        throw new RefreshTokenNotFoundException();
    }
}
  • reissueAccessToken 메서드는 Refresh Token의 유효성을 확인하고 새로운 Access Token을 발급.
  • validateRefreshToken 메서드를 통해 토큰의 만료 여부, 카테고리, Redis에서의 존재 여부 등을 확인하여 불필요한 토큰 발급을 방지.
  • jwtUtil.createJwt를 사용해 새 Access Token을 생성, 만료 시간은 10분으로 설정.

3. Redis와의 통합

RefreshTokenService

public void saveRefreshToken(String email, String refreshToken, long ttl) {
    redisTemplate.opsForValue().set(refreshToken, email, ttl, TimeUnit.MILLISECONDS);
}

public boolean existsByRefreshToken(String refreshToken) {
    return redisTemplate.hasKey(refreshToken);
}

public void deleteRefreshToken(String refreshToken) {
    redisTemplate.delete(refreshToken);
}

public String getEmailByRefreshToken(String refreshToken) {
    return redisTemplate.opsForValue().get(refreshToken);
}
  • saveRefreshToken 메서드를 통해 Redis에 Refresh Token을 저장, 만료 시간(TTL)을 설정.
  • existsByRefreshToken 메서드로 Redis에서 Refresh Token의 존재 여부를 확인, 로그아웃 시 deleteRefreshToken으로 Redis에서 해당 토큰을 삭제.
  • Redis는 중앙에서 토큰을 관리, Refresh Token의 강제 만료나 세션 관리를 가능하게 함.

4. Custom 필터로 인증/인가 처리

LoginFilter

@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                        FilterChain chain, Authentication authentication) {
    CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
    String email = userDetails.getUsername();
    Long userId = userDetails.getUserId();

    Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
    String role = authorities.iterator().next().getAuthority();

    String access = jwtUtil.createJwt("access", userId, email, role, 600000L); // 10분
    String refresh = jwtUtil.createJwt("refresh", userId, email, role, 86400000L); // 1일

    refreshTokenService.saveRefreshToken(email, refresh, 86400000L); // 저장

    response.setHeader("access", access);
    response.addCookie(createCookie("refresh", refresh));
    response.setStatus(HttpStatus.OK.value());
}
  • 로그인 성공 시 Access Token과 Refresh Token을 생성하여 응답 헤더와 쿠키에 각각 추가.
  • Refresh Token은 Redis에 저장하여 중앙에서 관리되며, 이후 재발급 시 사용됨.
  • 생성된 쿠키는 HttpOnly로 설정하여 클라이언트에서 직접 접근할 수 없게 보안을 강화.

5. 로그아웃 처리

CustomLogoutFilter

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
    throws IOException, ServletException {
    String requestUri = request.getRequestURI();
    String requestMethod = request.getMethod();

    if (!requestUri.matches("^\\/auth\\/logout$") || !requestMethod.equals("POST")) {
        filterChain.doFilter(request, response);
        return;
    }

    String refresh = extractRefreshToken(request);

    jwtUtil.isExpired(refresh);
    String category = jwtUtil.getCategory(refresh);
    if (!"refresh".equals(category)) {
        throw new InvalidRefreshTokenException();
    }

    String email = refreshTokenService.getEmailByRefreshToken(refresh);
    refreshTokenService.deleteRefreshToken(email);
    invalidateCookie(response, "refresh");
    response.setStatus(HttpServletResponse.SC_OK);
}
  • 로그아웃 요청(/auth/logout) 시 Refresh Token을 검증하고 Redis에서 삭제.
  • invalidateCookie 메서드를 통해 Refresh Token 쿠키를 무효화하여 세션을 완전히 종료함.
  • Refresh Token이 Redis에 없거나 만료된 경우 예외를 발생시켜 로그아웃 요청의 적합성을 검증.

6. JWT 유틸리티 구현

JWTUtil

public String createJwt(String category, Long userId, String email, String role, long ttl) {
    return Jwts.builder()
        .setSubject(email)
        .claim("id", userId)
        .claim("role", role)
        .claim("category", category)
        .setExpiration(new Date(System.currentTimeMillis() + ttl))
        .signWith(SignatureAlgorithm.HS512, secretKey)
        .compact();
}

public boolean isExpired(String token) {
    try {
        Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
        return false;
    } catch (ExpiredJwtException e) {
        return true;
    }
}

 

  • createJwt 메서드는 JWT 토큰 생성 로직을 구현하며, 사용자 ID, 이메일, 역할, 카테고리(access/refresh)와 만료 시간을 포함함.
  • isExpired 메서드는 토큰의 만료 여부를 확인하여 만료된 경우 예외를 던짐.

 

'Spring Security > JWT' 카테고리의 다른 글

스프링 JWT 심화 9 : 로그아웃  (0) 2024.12.22
스프링 JWT 심화 8 : Refresh 토큰 서버 측 저장  (1) 2024.12.22
스프링 JWT 심화 7 : Refresh Rotate  (0) 2024.12.22
스프링 JWT 심화 6 : Refresh로 Access 토큰 재발급  (0) 2024.12.22
스프링 JWT 심화 5 : Access 토큰 필터 : JWTFilter  (0) 2024.12.22
'Spring Security/JWT' 카테고리의 다른 글
  • 스프링 JWT 심화 9 : 로그아웃
  • 스프링 JWT 심화 8 : Refresh 토큰 서버 측 저장
  • 스프링 JWT 심화 7 : Refresh Rotate
  • 스프링 JWT 심화 6 : Refresh로 Access 토큰 재발급
jhyngu
jhyngu
취업하자.
    티스토리 홈
    |
  • jhyngu
    jhyngu
    jhyngu
  • 글쓰기 관리
  • 전체
    오늘
    어제
    • Dev (151)
      • Java (2)
      • Spring (51)
      • Spring Security (39)
        • JWT (22)
        • OAuth2 (17)
      • Kotlin (2)
      • React (6)
      • Coding Test (28)
      • DB (0)
      • Git (5)
      • Linux (14)
      • docker (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    백준
    JWT
    JavaScript
    Spring
    api client
    OAuth2
    Spring Security
    MVC
    Postman
    JDBC
    git
    mybatis
    Linux
    Spring Boot
    react
    알고리즘
    MariaDB
    Spring Framework
    oauth2
    spring web
  • hELLO· Designed By정상우.v4.10.3
jhyngu
REDBOX(헌혈증 기부 시스템) - JWT 정리
상단으로

티스토리툴바