
이번 단계에서는 기존 DB 기반 Refresh Token 구조를 제거하고, Redis를 활용한 인증 구조로 전환하였다. 단순한 저장소 변경이 아니라, 인증 보안 수준을 한 단계 끌어올리는 작업이었다.
기존에는 Refresh Token을 DB에 저장하는 방식이었다.
이 구조의 문제는 다음과 같다.
Refresh Token 저장소를 DB → Redis로 변경하였다.
Redis를 선택한 이유는 다음과 같다.
양방향 조회를 위해 2개의 key 구조를 설계하였다.
이 구조를 통해 다음이 가능해진다.
이 로직은 Backend 인증 세션 관리 핵심이다.
@Service
@RequiredArgsConstructor
public class RefreshTokenService {
private static final String REFRESH_TOKEN_KEY_PREFIX = "auth:refresh:";
private static final String USER_REFRESH_KEY_PREFIX = "auth:user-refresh:";
private final StringRedisTemplate redisTemplate;
@Value("${coreerp.auth.jwt.refresh-expiration}")
private long refreshExpiration;
public RefreshTokenInfo create(Long userId) {
revokeByUserId(userId);
String token = UUID.randomUUID().toString();
Duration ttl = Duration.ofMillis(refreshExpiration);
redisTemplate.opsForValue().set(
REFRESH_TOKEN_KEY_PREFIX + token,
String.valueOf(userId),
ttl
);
redisTemplate.opsForValue().set(
USER_REFRESH_KEY_PREFIX + userId,
token,
ttl
);
return new RefreshTokenInfo(
userId,
token,
LocalDateTime.now().plusSeconds(refreshExpiration / 1000),
false,
LocalDateTime.now()
);
}
public RefreshTokenInfo verify(String token) {
String userIdValue = redisTemplate.opsForValue()
.get(REFRESH_TOKEN_KEY_PREFIX + token);
if (userIdValue == null) {
throw new IllegalArgumentException("리프레시 토큰이 유효하지 않거나 만료되었습니다.");
}
Long userId = Long.valueOf(userIdValue);
String savedToken = redisTemplate.opsForValue()
.get(USER_REFRESH_KEY_PREFIX + userId);
if (!token.equals(savedToken)) {
throw new IllegalStateException("이미 교체된 토큰입니다.");
}
return new RefreshTokenInfo(userId, token, null, false, null);
}
public void revoke(String token) {
String userIdValue = redisTemplate.opsForValue()
.get(REFRESH_TOKEN_KEY_PREFIX + token);
if (userIdValue == null) return;
Long userId = Long.valueOf(userIdValue);
redisTemplate.delete(REFRESH_TOKEN_KEY_PREFIX + token);
redisTemplate.delete(USER_REFRESH_KEY_PREFIX + userId);
}
public RefreshTokenInfo rotate(String oldToken) {
RefreshTokenInfo verified = verify(oldToken);
revoke(oldToken);
return create(verified.userId());
}
}
기존에는 refresh 요청 시 동일 토큰을 유지했지만, 이제는 요청할 때마다 새로운 refresh token을 발급한다.
public AuthTokenResponse refresh(RefreshRequest req) {
RefreshTokenInfo newRefreshToken =
refreshTokenService.rotate(req.refreshToken());
User user = userService.findById(newRefreshToken.userId());
String accessToken = jwtTokenProvider.createToken(
user.getId(),
user.getLoginId(),
user.getRole().name()
);
return new AuthTokenResponse(
accessToken,
newRefreshToken.token(),
user.getId(),
user.getLoginId(),
user.getName(),
user.getRole().name()
);
}
핵심 변화는 다음과 같다.
변경 전
변경 후
즉 refresh token은 1회용으로 동작하게 된다.
Redis CLI 결과
keys *
(empty array)
TTL 확인
ttl auth:user-refresh:5
(integer) -2
→ key 완전 삭제 상태 정상
현재 CoreERP 인증 구조는 다음과 같이 분리된다.
Frontend → Backend → Redis → DB 구조로 완전히 분리되었다.
이번 작업은 단순한 기능 추가가 아니라, 인증 시스템을 실무 수준으로 끌어올린 단계이다.
이번 단계로 CoreERP 인증 구조는 단순 JWT 인증을 넘어 실제 서비스 수준의 세션 관리 구조로 발전하였다.
다음 단계는 인프라 구성 및 배포를 통해 실제 서비스 환경으로 확장하는 것이다.