지난 포스팅에서 Docker를 이용해 Spring Boot와 Redis를 연동하였다.
[Spring][Redis] 스프링 부트와 Redis 연동 (+ Refresh Token)
이번 포스팅에서는 스프링 부트와 Redis를 연동하는 방법을 알아보고, 이후 Redis를 통해 기존 스프링 부트 프로젝트에서 간단하게 Refresh Token을 구현한 과정을 적어보려 한다. ✅ Redis란 무엇인가 R
zapzook.tistory.com
이제 본격적으로 Redis를 활용하여 Refresh Token을 구현해 볼 차례이다.
✅ Redis Repository 생성
@Repository
public class RefreshTokenRedisRepository {
private static final long EXPIRE_TIME = 6 * 60 * 60; // 6시간
@Resource(name = "redisTemplate")
private ValueOperations<String, String> valueOperations;
public void save(String key, String value) {
valueOperations.set(key, value, EXPIRE_TIME, TimeUnit.SECONDS);
}
public String findByKey(String key) {
return valueOperations.get(key);
}
public Boolean existsByKey(String key) {
return valueOperations.getOperations().hasKey(key);
}
public void delete(String key) {
valueOperations.getOperations().delete(key);
}
}
먼저 Refresh Token 데이터를 담을 Redis Repository를 생성해준다.
이전 포스팅에서 언급했듯이 Redis 데이터 저장소에는 키-벨류 쌍 데이터가 저장된다.
위 Repository에는 Username, Refresh Token이 각각 키,벨류로 저장될 것이다.
Refresh Token 데이터의 만료 시간(EXPIRE_TIME)은 6시간으로 설정했으며,
@Resource 애너테이션을 통해 redisTemplate을 빈으로 주입받아 ValueOperations를 설정하였다.
여기서 ValueOperations는 Redis에서 데이터를 조작하는데 사용되며, 데이터의 타입은 String으로 설정하였다.
이후 레포지토리에서 기본적으로 사용될 기능 메서드들을 정의하였다.(save, find 등)
여기서 데이터를 save 할 때, 키-벨류 값과 함께 데이터의 만료시간을 설정 가능하다.
그렇게 되면 save 된 데이터가 만료 시간이 지나면 자연히 저장소에서 휘발되게 된다.
(Refresh Token의 수명이 다 되면 사라지게 됨)
✅ Refresh Token 관련 로직 추가
Repository를 설정하였으니, 이제 인증 및 인가 로직에 Refresh Token의 검증 및 생성에 관한
코드를 추가해주면 된다.
인증 인가 로직을 구현하는 방식은 사람마다 다를테니, 이 부분은 그냥 참고만 하길 바란다.
본 프로젝트에서는 Spring Security와 JWT를 사용해 필터단에서 인증 인가 로직을 구현하였다.
📌 Refresh Token 생성 메서드 추가
@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil {
// Token 식별자
public static final String BEARER_PREFIX = "Bearer ";
// Refresh 토큰 만료시간
private final long REFRESH_TOKEN_TIME = 6 * 60 * 60 * 1000L; // 6시간
@Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
...
public String createRefreshToken(String username) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + REFRESH_TOKEN_TIME);
return Jwts.builder()
.setSubject(username)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(key, signatureAlgorithm)
.compact();
}
}
JWT와 관련된 유틸 메서드들을 담는 JwtUtil에 Refresh Token의 생성에 필요한 필드 변수,
생성 메서드를 추가하였다. Access Token과 다르게 Refresh Token은 토큰을 재발급하는 용도일 뿐,
유저의 인증에 사용되지 않으므로 토큰 생성시에 username을 제외한 불필요한 유저 정보는 제외시켰다.
📌 로그인 시, Refresh Token 생성 및 저장
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException{
UserDetailsImpl userDetails = ((UserDetailsImpl) authResult.getPrincipal());
User user = userDetails.getUser();
String accessToken = jwtUtil.createToken(user.getUsername(), user.getId(), user.getEmail());
String refreshToken = jwtUtil.createRefreshToken(user.getUsername());
response.addHeader(JwtUtil.AUTHORIZATION_HEADER, accessToken);
refreshTokenRedisRepository.save(user.getUsername(), refreshToken);
util.authResult(response, "로그인 성공! Header에 JWT 토큰을 반환합니다.", 200);
}
로그인에 성공했을 경우 실행되는 메서드, 즉 successfulAuthentication() 메서드에
RefreshToken을 생성 및 저장하는 코드를 추가하였다.
이제 특정 유저가 로그인을 하면 Access Token을 발급함과 동시에 Refresh Token을 생성하고,
해당 유저네임과 토큰값이 Redis Repository에 저장될 것이다.
📌 엑세스 토큰이 만료되었을 경우 재발급 로직 추가
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {
String tokenValue = jwtUtil.getJwtFromHeader(req);
if (StringUtils.hasText(tokenValue)) {
int tokenStatus = jwtUtil.validateToken(tokenValue);
if (tokenStatus == 1) { // 유효하지 않은 토큰
util.authResult(res, "JWT 토큰이 유효하지 않습니다.", 400);
return;
} else if (tokenStatus == 2) { // 만료된 토큰
try {
Claims info = jwtUtil.getExpiredTokenClaims(tokenValue);
String username = info.getSubject();
Long userId = info.get("userId", Long.class);
String email = info.get("email", String.class);
if(refreshTokenRedisRepository.existsByKey(username)) {
String newToken = jwtUtil.createToken(username, userId, email);
res.addHeader(JwtUtil.AUTHORIZATION_HEADER, newToken);
util.authResult(res, "Access 토큰이 만료되었습니다. 새로운 토큰을 헤더에 발급합니다.", 200);
return;
} else {
util.authResult(res, "Access 토큰과 Refresh 토큰이 모두 만료되었습니다. 다시 로그인 해주세요.", 401);
return;
}
} catch (Exception e) {
log.error(e.getMessage());
return;
}
} else { // 유효한 토큰
Claims info = jwtUtil.getUserInfoFromToken(tokenValue);
setAuthentication(info.getSubject(), info.get("userId", Long.class), info.get("email", String.class));
}
}
filterChain.doFilter(req, res);
}
(코드가 다소 지저분한 점 양해바람..)
JWT 검증 로직에 Access Token이 만료되었을 경우, Refresh Token을 통해 새로운 토큰을 발급하는
로직을 추가하였다.
Access Token에서 추출한 유저네임이 Refresh Token을 저장중인 Redis Repository내에 존재한다면
새로운 토큰을 생성하여 발급하는 코드이다.
✅ 마무리
테스트 결과 기능은 문제없이 잘 동작하였다.
본 프로젝트에선 가장 간단하고 기본적인 방식으로 Refresh Token을 구현하였다.
자료들을 찾다보니 알게된 건데, Refresh Token을 구현하는 방식은 여러 종류가 있는 듯 하다.
더 많은 내용이 궁금하다면 구글링을 해보길 바란다.
'Study > Spring' 카테고리의 다른 글
[Spring][Redis] 동시성 이슈와 분산락을 이용한 동시성 제어 (0) | 2024.03.27 |
---|---|
[Spring] AOP를 활용한 기능 모듈화 (0) | 2024.03.25 |
[Spring][Redis] 스프링 부트와 Redis 연동 (+ Refresh Token) (0) | 2024.03.13 |
1+N 문제 (0) | 2024.03.04 |
Entity 연관 관계 (지연 로딩, 영속성 전이, 고아 Entity 삭제) (0) | 2024.02.08 |