서버 측 주도권
단순하게 JWT를 발급하여 클라이언트 측으로 전송하면 인증 / 인가 에 대한 주도권 자체가 클라이언트 측에 맡겨진다.
JWT를 탈취하여 서버 측으로 접근할 경우 JWT가 만료되기 까지 서버 측에서는 그것을 막을 수 없으며, 프론트 측에서 토큰을 삭제하는 로그아웃을 구현해도 이미 복제가 되었다면 피해를 입을 수 있다.
이런 문제를 해결하기 위해 생명주기가 긴 Refresh 토큰은 발급 시 서버 측 저장소에 기억한 후 기억되어 있는 Refresh 토큰만 사용할 수 있도록 서버 측에서 주도권을 가질 수 있다.
구현 방법
- 발급 시
- Refresh 토큰을 서버 측 저장소에 저장
- 갱신 시 (Refresh Rotate)
- 기존 Refresh 토큰을 삭제하고 새로 발급한 Refresh 토큰을 저장
토큰 저장소 구현
- 토큰 저장소
RDB 또는 Redis와 같은 데이터베이스를 통해 Refresh 토큰을 저장한다. 이때 Redis의 경우 TTL 설정을 통해 생명주기가 끝이 난 토큰은 자동으로 삭제할 수 있는 장점이 있다.
- RefreshEntity
@Entity
@Getter
@Setter
public class RefreshEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String refresh;
private String expiration;
}
- RefreshRepository
public interface RefreshRepository extends JpaRepository<RefreshEntity, Long> {
Boolean existsByRefresh(String refresh);
@Transactional
void deleteByRefresh(String refresh);
}
로그인 시 : LoginSuccessHandler
- RefreshRepository 의존성 주입
- successfulAuthentication() 일부 추가
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
//유저 정보
String username = authentication.getName();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();
String role = auth.getAuthority();
//토큰 생성
String access = jwtUtil.createJwt("access", username, role, 600000L);
String refresh = jwtUtil.createJwt("refresh", username, role, 86400000L);
//Refresh 토큰 저장
addRefreshEntity(username, refresh, 86400000L);
//응답 설정
response.setHeader("access", access);
response.addCookie(createCookie("refresh", refresh));
response.setStatus(HttpStatus.OK.value());
}
- addRefrehEntity() 메소드 구현
private void addRefreshEntity(String username, String refresh, Long expiredMs) {
Date date = new Date(System.currentTimeMillis() + expiredMs);
RefreshEntity refreshEntity = new RefreshEntity();
refreshEntity.setUsername(username);
refreshEntity.setRefresh(refresh);
refreshEntity.setExpiration(date.toString());
refreshRepository.save(refreshEntity);
}
SecurityConfig 설정
- RefreshRepository 의존성 주입
- LoginFileter 등록시 refreshRepository 의존성 주입
http
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshRepository), UsernamePasswordAuthenticationFilter.class);
Reissue 시 : ReissueController
- RefrehRepository 의존성 주입
- PostMapping("/reissue") 경로 메소드
@PostMapping("/reissue")
public ResponseEntity<?> reissue(HttpServletRequest request, HttpServletResponse response) {
//get refresh token
String refresh = null;
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if (cookie.getName().equals("refresh")) {
refresh = cookie.getValue();
}
}
if (refresh == null) {
//response status code
return new ResponseEntity<>("refresh token null", HttpStatus.BAD_REQUEST);
}
//expired check
try {
jwtUtil.isExpired(refresh);
} catch (ExpiredJwtException e) {
//response status code
return new ResponseEntity<>("refresh token expired", HttpStatus.BAD_REQUEST);
}
// 토큰이 refresh인지 확인 (발급시 페이로드에 명시)
String category = jwtUtil.getCategory(refresh);
if (!category.equals("refresh")) {
//response status code
return new ResponseEntity<>("invalid refresh token", HttpStatus.BAD_REQUEST);
}
//DB에 저장되어 있는지 확인
Boolean isExist = refreshRepository.existsByRefresh(refresh);
if (!isExist) {
//response body
return new ResponseEntity<>("invalid refresh token", HttpStatus.BAD_REQUEST);
}
String username = jwtUtil.getUsername(refresh);
String role = jwtUtil.getRole(refresh);
//make new JWT
String newAccess = jwtUtil.createJwt("access", username, role, 600000L);
String newRefresh = jwtUtil.createJwt("refresh", username, role, 86400000L);
//Refresh 토큰 저장 DB에 기존의 Refresh 토큰 삭제 후 새 Refresh 토큰 저장
refreshRepository.deleteByRefresh(refresh);
addRefreshEntity(username, newRefresh, 86400000L);
//response
response.setHeader("access", newAccess);
response.addCookie(createCookie("refresh", newRefresh));
return new ResponseEntity<>(HttpStatus.OK);
}
- addRefreshEntity() 메소드 구현
private void addRefreshEntity(String username, String refresh, Long expiredMs) {
Date date = new Date(System.currentTimeMillis() + expiredMs);
RefreshEntity refreshEntity = new RefreshEntity();
refreshEntity.setUsername(username);
refreshEntity.setRefresh(refresh);
refreshEntity.setExpiration(date.toString());
refreshRepository.save(refreshEntity);
}
Refresh 토큰 저장소에서 기한이 지난 토큰 삭제
TTL 설정(Redis)을 통해 자동으로 Refresh 토큰이 삭제되면 무방하지만 계속해서 토큰이 쌓일 경우 용량 문제가 발생할 수 있다.
따라서 스케줄 작업을 통해 만료시간이 지난 토큰은 주기적으로 삭제하는 것이 올바르다.
'Spring > Spring Security - JWT' 카테고리의 다른 글
스프링 JWT 심화 9 : 로그아웃 (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 |
스프링 JWT 심화 4 : 다중 토큰 발급 (Refresh) (0) | 2024.12.22 |