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 토큰 서버 측 저장 (0) | 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 |