기능들을 구현하면서 토큰에 대한 작업이 반복되는 것을 느꼈다
그래서 토큰 검증, 삭제, 저장 같은 메서드들을 한 곳에서 관리해서 중복을 좀 없애고 싶었다
TokenService를 만들어서 토큰을 관리해보자
@Service
@RequiredArgsConstructor
public class TokenService {
private final RefreshTokenRepository refreshTokenRepository;
private final JwtUtil jwtUtil;
// DB에 Refresh 토큰 저장
@Transactional
public void addRefreshToken(User user, String refresh) {
RefreshToken refreshToken = RefreshToken.builder()
.user(user)
.tokenValue(refresh)
.build();
refreshTokenRepository.save(refreshToken);
}
// 쿠키 생성
public Cookie createCookie(String key, String value) {
Cookie cookie = new Cookie(key, value);
cookie.setMaxAge(24 * 60 * 60);
cookie.setHttpOnly(true);
return cookie;
}
// DB에 Refresh 토큰이 존재하는지 확인
public Boolean existsByTokenValue(String tokenValue) {
return refreshTokenRepository.existsByTokenValue(tokenValue);
}
// DB에서 Refresh 토큰을 삭제
@Transactional
public void deleteByTokenValue(String tokenValue) {
refreshTokenRepository.deleteByTokenValue(tokenValue);
}
// 리프레시 토큰 검증
public void validateRefreshToken(String token) {
if (token == null) {
throw new InvalidTokenException("리프레시 토큰이 없습니다.");
}
try {
jwtUtil.isExpired(token);
} catch (ExpiredJwtException e) {
throw new InvalidTokenException("만료된 리프레시 토큰입니다.");
}
String category = jwtUtil.getCategory(token);
if (!category.equals("refresh")) {
throw new InvalidTokenException("유효하지 않은 카테고리의 토큰입니다.");
}
if (!existsByTokenValue(token)) {
throw new InvalidTokenException("DB에 존재하지 않는 리프레시 토큰입니다.");
}
}
// 액세스 토큰 검증
public void validateAccessToken(String token) {
if (token == null) {
throw new InvalidTokenException("액세스 토큰이 없습니다.");
}
try {
jwtUtil.isExpired(token);
} catch (ExpiredJwtException e) {
throw new InvalidTokenException("만료된 액세스 토큰입니다.");
}
String category = jwtUtil.getCategory(token);
if (!category.equals("access")) {
throw new InvalidTokenException("유효하지 않은 카테고리의 토큰입니다.");
}
}
}
이제 토큰과 관련된 작업들은 TokenService를 사용하도록 바꿔주었다
Refresh 토큰이 있는 이유가 이제 나타난다
Access 토큰만 구현이 되어 있다면 생명 주기가 짧기 때문에 만료시 다시 로그인해야 하는 상황이 벌어진다
/api/auth/reissue)를 구성하여 Refresh 토큰을 검증하고 Access 토큰 응답@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/signup")
public ResponseEntity<String> signup(@RequestBody SignupDto signupDto) {
authService.signup(signupDto);
return ResponseEntity.ok("회원가입 성공");
}
// 토큰 재발급
@PostMapping("/reissue")
public ResponseEntity<?> reissue(HttpServletRequest request, HttpServletResponse response) {
return authService.reissue(request, response);
}
@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody LoginDto loginDto, HttpServletResponse response) {
authService.login(loginDto, response);
return ResponseEntity.ok("로그인 성공");
}
}
우선 재발급 요청이 온다면 Access 토큰이 헤더에 담겨 왔는지 검증을 해야 한다
Access 토큰이 유효하다면 Refresh 토큰 또한 검증한다
Access 토큰이 만료되었기 때문에 만료된 토큰에서 username을 꺼내는 메서드를 사용해서 username을 꺼낸다
username으로 DB에서 회원을 찾아 새로운 Access/Refresh 토큰을 발급해준다
기존 Refresh 토큰은 DB에서 삭제 후 새로운 토큰을 DB에 저장한다
헤더와 쿠키에 새 토큰을 세팅한다
@Slf4j
@Service
@RequiredArgsConstructor
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
private final TokenService tokenService;
private final AuthenticationManager authenticationManager;
// ... 이전 코드 ... //
// 토큰 재발급
public ResponseEntity<?> reissue(HttpServletRequest request, HttpServletResponse response) {
String authorization = request.getHeader("Authorization");
// Access Token 검증
if (authorization == null || !authorization.startsWith("Bearer ")) {
throw new InvalidTokenException("액세스 토큰이 없습니다.");
}
String accessToken = authorization.split(" ")[1];
// Refresh 토큰 검증
String refresh = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("refresh")) {
refresh = cookie.getValue();
break;
}
}
}
tokenService.validateRefreshToken(refresh);
// --- 검증 통과 --- //
// 기존 토큰에서 username 꺼냄
String username = jwtUtil.getUsernameFromExpirationToken(accessToken);
User foundUser = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("회원을 찾을 수 없습니다."));
// 새로운 access/refresh 토큰 생성
String newAccess = jwtUtil.generateAccessToken("access", username, foundUser.getRoleName());
String newRefresh = jwtUtil.generateRefreshToken("refresh");
// 기존 Refresh 토큰 DB에서 삭제 후 새 Refresh 토큰 저장
tokenService.deleteByTokenValue(refresh);
tokenService.addRefreshToken(foundUser, newRefresh);
// 응답 설정
response.setHeader("Authorization", "Bearer " + newAccess);
response.addCookie(tokenService.createCookie("refresh", newRefresh));
return new ResponseEntity<>(HttpStatus.OK);
}
}
Access 토큰이 만료됐다고 가정하고 /api/auth/reissue로 요청을 해보자
Postman에서 헤더에 Authorization 값에 기존 Access 토큰 값을 담아서 요청하면
새로운 토큰으로 발급해준다

Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJST0xFX1VTRVIiLCJjYXRlZ29yeSI6ImFjY2VzcyIsImlhdCI6MTc2NTA5NDMwNCwiZXhwIjoxNzY1MDk2MTA0fQ.TglDXiwybiJKLreQAOdvlEN9jjpg-L_vFvMDu6jW7e8Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJST0xFX1VTRVIiLCJjYXRlZ29yeSI6ImFjY2VzcyIsImlhdCI6MTc2NTA5Njg2OCwiZXhwIjoxNzY1MDk4NjY4fQ.CrehsQH6AojJOhMaVYLO4j53-ojjlk_kitQBmIleEfQRefresh 토큰 또한 바뀌는 것을 확인했다
다음은 로그아웃을 구현하겠다