개발 일지 (redis로 토큰 관리하기)
1. redis로 refresh token 관리하기
로그인 시에, userId와 세션 아이디를 기반으로 refresh_token을 만들어서, 레디스에 저장을 한다.
세션 아이디를 같이 받는 이유? -> 다중 로그인을 가능하게 하기 위함
이렇게 하지 않으면, user 김씨가 기기 A와 기기 B에 로그인을 한 상태에서, 기기 B에서 로그아웃을 하게 되면 기기 A에서도 로그아웃이 되어버린다.
그걸 방지하기 위해서, 레디스에 refresh token을 redis에 저장을 하고, access_token의 갱신이 필요할 때마다 redis에서 유저 id를 기반와 세션 id 기반으로 찾아낸다.
최종적으로, refresh_token은 다음과 같이 저장이 된다.
key = refresh:1:abc-123
value = eyJhbGciOiJIUzI1NiJ9...
TTL = refresh token 만료 시간
코드 뜯어보기
로그인
String sessionId = UUID.randomUUID().toString();
String refreshToken = jwtTokenProvider.createRefreshToken(user.getId(), user.getUserName(), sessionId);
refreshTokenRepository.save(
user.getId(),
sessionId,
refreshToken,
jwtTokenProvider.getRefreshTokenExpirationMillis()
);
세션 ID는 랜덤으로 만들어서, jwt 토큰을 생성한다.
그리고 이 토큰을 레디스에 저장을 한다.
로그아웃
jwtTokenProvider.validateRefreshToken(refreshToken);
Long userId = jwtTokenProvider.getUserId(refreshToken);
String sessionId = jwtTokenProvider.getSessionId(refreshToken);
String savedRefreshToken = refreshTokenRepository.find(userId, sessionId);
if (savedRefreshToken != null && savedRefreshToken.equals(refreshToken)) {
refreshTokenRepository.delete(userId, sessionId);
}
리프레시 토큰에서 userId와 sessionId를 파싱해와서 그걸 기반으로, 제거할 refresh_token을 찾아서 삭제한다.
2. redis로 black list 관리하기
black list가 무엇인가
black list의 존재 이유는, 로그아웃에 있다.
사용자가 로그아웃을 한다면, 그때 가지고 있던 access_token은 더이상 효력이 없어져야 한다.
효력이 없어지지 않는다면, 로그아웃을 했음에도 불구하고, 이 access_token을 이용해서 자유롭게 인증이 필요한 api를 요청을 할 수 있게 된다.
근데 지금 가지고 있는 access_token을 어떻게 무력화할까?
access_token은 알다시피, 서버쪽에 저장되지 않는 값이다. 로그인을 하고 나면, 클라이언트쪽에서 저장을 해서 인증이 필요한 api를 호출할 때마다 사용하는 방식이다.
그렇다면 이렇게 서버쪽에서 관리하지 않는 access_token을 무력화하기 위해선 어떤 방법을 사용해야할까?
redis에다가, 로그아웃 처리된 access_token을 저장하면 해결된다.
access_token은 30분 정도의 유효기간을 가지고 있기 때문에, 이 access_token이 유효한 기간동안 redis에서 이 access_token을 black list처리를 하면, 로그아웃이 됐을 때 이 토큰을 가지고 인증을 할 수가 없게 된다.
jwtAuthenticationFilter에서, 서버로 인증이 필요한 요청이 들어올 때, authentication 헤더에 토큰이 있다면, 이 토큰이 black list에 등록되어있는지 redis에서 찾은 다음에, black list에 등록이 되어있는 access_token이라면 인증 실패 처리를 한다.
코드 뜯어보기
로그아웃
@Transactional
public void logout(HttpServletRequest request) {
// access_token이 헤더에 있는지 확인
String accessToken = resolveAccessToken(request);
if (accessToken != null) {
try {
jwtTokenProvider.validateAccessToken(accessToken);
long remainingExpirationMillis =
jwtTokenProvider.getRemainingExpirationMillis(accessToken);
accessTokenBlacklistRepository.save(
accessToken,
remainingExpirationMillis
);
} catch (BusinessException e) {
// 이미 만료되었거나 유효하지 않은 access token은 blacklist에 넣을 필요가 없으므로 무시
}
}
String refreshToken = getRefreshToken(request)
.orElse(null);
if (refreshToken == null) {
return;
}
try {
jwtTokenProvider.validateRefreshToken(refreshToken);
Long userId = jwtTokenProvider.getUserId(refreshToken);
String sessionId = jwtTokenProvider.getSessionId(refreshToken);
String savedRefreshToken = refreshTokenRepository.find(userId, sessionId);
if (savedRefreshToken != null && savedRefreshToken.equals(refreshToken)) {
refreshTokenRepository.delete(userId, sessionId);
}
} catch (BusinessException e) {
// 이미 만료되었거나 유효하지 않은 refresh token이면
// Redis에서 삭제할 수 없으므로 무시하고 로그아웃 응답은 정상 처리
}
}
- access_token
- access_token이 남아있다면 토큰을 검증해서 유효기간을 구해온다.
- 유효기간 만큼 레디스에 black list로 등록해놓는다.
- refresh_token
- refresh_token은 이미 레디스에서 관리하고 있다.
- refresh_token은 그냥 레디스에서 삭제만 하면 된다.
black list 명단에 access_token 저장 및 확인
- access_token을 black list 명단에 저장
public void save(String accessToken, long expirationMillis) {
if (accessToken == null || accessToken.isBlank()) {
return;
}
if (expirationMillis <= 0) {
return;
}
redisTemplate.opsForValue().set(
key(accessToken),
"logout",
Duration.ofMillis(expirationMillis)
);
}
요청이 들어왔을 때, black list에 속하는 access_token인지 확인
public boolean exists(String accessToken) {
if (accessToken == null || accessToken.isBlank()) {
return false;
}
return Boolean.TRUE.equals(redisTemplate.hasKey(key(accessToken)));
}
private String key(String accessToken) {
return PREFIX + accessToken;
}
'개인 프로젝트 > modu' 카테고리의 다른 글
| [daon] 8일차 개발 회고록 (0) | 2026.06.10 |
|---|---|
| [daon] 7일차 개발 회고록 (0) | 2026.06.07 |
| [daon] 5일차 개발 회고록 (1) | 2026.05.25 |
| [daon] 3일차 개발 회고록 (1) | 2026.05.12 |
| [daon] 2일차 개발 회고록 (0) | 2026.05.06 |