구현
현재 프로젝트는 Spring Security를 사용하고 jwt 토큰으로 인증/인가를 구현했다. 기존의 방식은 액세스 토큰을 헤더에 넣고, 리프레쉬 토큰을 쿠키에 넣은 후 mysql에 refresh 토큰 정보를 넣어서 사용했다. mysql에 넣을 시 안사용하는 토큰들을 주기적으로 정리해줘야 하는 불편함이 있어서 이 참에 Redis로 변경하기로 했다.
인증이 성공했을 때 위와 같이 로그인 정보를 프론트에 보내주고, 레디스에는 리프레쉬 토큰을 저장해 주고 TTL을 리프레쉬 토큰 유효시간 동안 걸어주었다.
public ResponseDto<?> reissue(String refreshToken) {
if(refreshToken == null) {
return ResponseDto.of(HttpStatus.BAD_REQUEST, "재발급하려면 리프레쉬 토큰이 필요합니다.", null);
}
// 프론트에서 붙여준 Bearer prefix 제거
try{
refreshToken = jwtUtil.substringToken(refreshToken);
} catch (NullPointerException e) {
return ResponseDto.of(HttpStatus.BAD_REQUEST, "잘못된 토큰 형식 입니다.", null);
}
// 리프레쉬 토큰인지 검사
String category = jwtUtil.getCategory(refreshToken);
if (!category.equals(TokenType.REFRESH.name())) {
return ResponseDto.of(HttpStatus.BAD_REQUEST, "리프레쉬 토큰이 아닙니다.");
}
// 토큰 만료 검사
try{
jwtUtil.isExpired(refreshToken);
} catch (ExpiredJwtException e) {
return ResponseDto.of(HttpStatus.UNAUTHORIZED, "만료된 리프레쉬 토큰입니다.", null);
}
String username = jwtUtil.getUsername(refreshToken);
// 레디스에서 리프레쉬 토큰을 가져온다.
refreshToken = (String) redisTemplate.opsForValue().get(username);
if (refreshToken == null) {
return ResponseDto.of(HttpStatus.UNAUTHORIZED, "만료된 리프레쉬 토큰입니다.", null);
}
// 검증이 통과되었다면 refresh 토큰으로 액세스 토큰을 발행해준다.
String role = jwtUtil.getRole(refreshToken);
// 새 토큰 발급
String newAccessToken = jwtUtil.createAccessToken(username, UserRole.valueOf(role), false);
String newRefreshToken = jwtUtil.createRefreshToken(username, UserRole.valueOf(role), false);
// TTL 새로해서
Long ttl = redisTemplate.getExpire(username);
redisTemplate.opsForValue().set(username, newAccessToken);
if(ttl != null && ttl > 0) {
redisTemplate.expire(username, ttl, TimeUnit.MILLISECONDS);
} else {
return ResponseDto.of(HttpStatus.UNAUTHORIZED, "만료된 리프레쉬 토큰입니다.", null);
}
Reissue reissue = new Reissue(newAccessToken, newRefreshToken);
return ResponseDto.of(HttpStatus.OK, "", reissue);
}
재발행해주는 부분은 refresh토큰을 검증해 주고 기존 리프레쉬 토큰을 삭제 후 다시 발급해서 redis에 저장 후 프런트에게 보내주었다.
액세스 토큰이나 리프레쉬 토큰을 탈취당했을 때 처리를 위해 블랙리스트 처리도 나중에 한 번 고민해봐야겠다. 그리고 리프레쉬 토큰을 프런트엔드에게 보내는 게 맞는 것인지 다시 한번 생각해 봐야겠다.
트러블 슈팅
테스트를 위해 requestMatchers("/**"). permitAll()을 설정했는데, 예상과 달리 인가 필터에서 accessToken이 없다는 이유로 요청이 차단되는 문제가 발생했다. 이는 requestMatchers("/**"). permitAll()의 의미를 제대로 이해하지 못해 생긴 문제였다. 스프링에서는 permitAll()을 사용하더라도 필터는 그대로 동작하며, 해당 매처에 포함된 URL로 요청을 보낼 때 인증 처리를 생략하는 것이지 필터 자체를 건너뛰는 것은 아니었다.
이 문제를 해결하기 위해 찾아본 결과, 필터 부분에 shouldNotFilter라는 오버라이드 가능한 메서드가 있었다. 이 부분에서 해당 요청으로 오면 필터를 건너뛸 수 있도록 바꿔주었다.
'사이드 프로젝트 > 쿠키톡' 카테고리의 다른 글
쿠키 CRUD 구현 (5) | 2024.10.08 |
---|---|
기존의 업로드 방식 리팩토링 (0) | 2024.09.29 |
HLS 변환 (0) | 2024.09.15 |
사이드 프로젝트 시작 (0) | 2024.09.15 |