최종프로젝트는 일자별로 일일이 기록하기에는 프로젝트에 투자할 시간이 모자를것 같아서 이제부터는 큰 트러블 슈팅위주로 포스팅을 할 계획이다.
그래서 오늘 쓸 주제는 Redis에 관한 이슈이다.
우리 서비스에서 로그인을 하면 인증에 필요한 jwt토큰이 발급되고 이건 30분동안 유효하다. 30분이 지나면 토큰이 만료되어 재발급이 필요하다. 재발급을 위해서는 refresh토큰의 인증절차를 거친 뒤 인증되면 새 토큰을 발급하는 구조로 코드를 작성하기로 했다.
이 때 refresh토큰을 인증하는 절차를 만들려면 이 refresh토큰을 서버에서 기억하고 있어야 한다. 그렇다고 dbms에 저장하면 새 토큰을 발급할 때 마다 트랜젝션이 수행될 거고 이게 누적되면 서버에 적잖은 부담이 가해질 것이다. 다시 말해 리프레쉬 토큰 발급을 위해 적잖은 자원을 소모해야 한다.
그래서 서버의 부담을 줄이기 위해 redis를 사용하려고 한다. redis는 dbms에서 데이터를 가져오는 것 보다는 자원을 덜 소모하는 장점이 있다. 인메모리 db이기 때문에 보조기억장치에 저장되는 mysql같은 rdbms보다는 훨씬 빠른 속도로 데이터를 가져올 수 있다.
아래는 redistemplate를 빈으로 등록하는 redisconfig의 코드 일부이다.
@Bean
public RedisTemplate<String,String> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
아래는 redis에 refresh토큰을 저장하고 가져오는등 redis와 상호작용하는 코드를 모아둔 redisdao이다.
@Component
@RequiredArgsConstructor
public class UserRedisDao {
private final RedisTemplate<String, String> redisTemplate;
public void setRefreshToken(String key, String refreshToken, long refreshTokenTime) {
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(refreshToken.getClass()));
redisTemplate.opsForValue().set(key, refreshToken, refreshTokenTime, TimeUnit.MILLISECONDS);
}
public String getRefreshToken(String key) {
return redisTemplate.opsForValue().get(key);
}
public void deleteRefreshToken(String key) {
redisTemplate.delete(key);
}
public boolean hasKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
public void setBlackList(String accessToken, String msg, Long milliseconds) {
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(msg.getClass()));
redisTemplate.opsForValue().set(accessToken, msg, milliseconds, TimeUnit.MILLISECONDS);
}
public String getBlackList(String key) {
return redisTemplate.opsForValue().get(key);
}
public boolean deleteBlackList(String key) {
return Boolean.TRUE.equals(redisTemplate.delete(key));
}
public void flushAll(){
redisTemplate.getConnectionFactory().getConnection().serverCommands().flushAll();
}
}
아래는 토큰을 재발급하는 서비스단 코드이다.
@CachePut(cacheNames = CacheNames.USERBYEMAIL, key = "'login' + #p1")
public AuthResponseDto reissueToken(String refreshToken, String email) {
String substringToken = jwtUtil.substringToken(refreshToken);
Users user = usersRepository.findByEmail(email).orElseThrow(() ->
new UsernameNotFoundException("Not Found " + email));
String storedRefreshToken = redisDao.getRefreshToken(email);
if(redisDao.getRefreshToken(email).contains("\"")) {
storedRefreshToken = storedRefreshToken.replace("\"", "");
}
if (!storedRefreshToken.equals(refreshToken)) {
throw new CustomException(ErrorCode.TOKEN_MISMATCH);
}
String newAccessToken = jwtUtil.createAccessToken(user);
String newRefreshToken = jwtUtil.createRefreshToken(user);
redisDao.setRefreshToken(email, newRefreshToken, jwtUtil.getREFRESHTOKEN_TIME());
return new AuthResponseDto(newAccessToken, newRefreshToken);
}
구현이 완료된 후 실행을 해보니 토큰도 redis에서 잘 가져오고 잘 실행이 된다.
문제는 이 상태에서 서버가 재시작할 때다.
redis에 데이터가 저장된 채로 서버를 재시작하면 redis에 저장된 값에 큰따옴표가 붙은 채로 나와서 토큰이 일치하지 않다고 토큰을 재발급 안해준다.
RedisTemplate<String,Object> redisTemplate()
구글에 검색해보니 이렇게 String, Object제네릭을 사용해서 redistemplate를 생성해야 한다는 이야기도 있고
redisTemplate.setValueSerializer(new StringRedisSerializer());
이 코드가 빠져서 그렇다는 이야기도 나오고 나와 비슷한 이슈를 겪은 여러 글들을 찾을 수 있었다.
, https://sunghs.tistory.com/159)
여러 글들을 참고해보고 코드를 수정해서 다시 재발급 기능을 실행해 봤다.
@Bean
public RedisTemplate<String,Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
redistemplate를 String, String제네릭에서 String, Object제네릭으로 바꿨다.
@Component
@RequiredArgsConstructor
public class UserRedisDao {
private final RedisTemplate<String, Object> redisTemplate;
public void setRefreshToken(String key, String refreshToken, long refreshTokenTime) {
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(refreshToken.getClass()));
redisTemplate.opsForValue().set(key, refreshToken, refreshTokenTime, TimeUnit.MILLISECONDS);
}
public Object getRefreshToken(String key) {
return redisTemplate.opsForValue().get(key);
}
public void deleteRefreshToken(String key) {
redisTemplate.delete(key);
}
public boolean hasKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
public void setBlackList(String accessToken, String msg, Long milliseconds) {
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(msg.getClass()));
redisTemplate.opsForValue().set(accessToken, msg, milliseconds, TimeUnit.MILLISECONDS);
}
public Object getBlackList(String key) {
return redisTemplate.opsForValue().get(key);
}
public boolean deleteBlackList(String key) {
return Boolean.TRUE.equals(redisTemplate.delete(key));
}
public void flushAll(){
redisTemplate.getConnectionFactory().getConnection().serverCommands().flushAll();
}
}
redistemplate의 타입이 변경되었기 때문에 반환하는 value의 타입도 string에서 object로 변경해줬다.
그래도 결과는 같았다.
코드를 바꿔줘도 왜 따옴표가 붙는지 모르겠다. redis를 사용해서 기능을 구현한 다른 팀원은 저렇게 object로 바꿔주니까 해결됐다는데 왜 난 해결이 안될까?
일단 어떻게든 기능을 구현해야 했기에 서비스단에 따옴표가 붙으면 따옴표를 제거하는 코드를 추가해줬다.
if(redisDao.getRefreshToken(email).contains("\"")) {
storedRefreshToken = storedRefreshToken.replace("\"", "");
}
@CachePut(cacheNames = CacheNames.USERBYEMAIL, key = "'login' + #p1")
public AuthResponseDto reissueToken(String refreshToken, String email) {
String substringToken = jwtUtil.substringToken(refreshToken);
Users user = usersRepository.findByEmail(email).orElseThrow(() ->
new UsernameNotFoundException("Not Found " + email));
String storedRefreshToken = redisDao.getRefreshToken(email);
if(redisDao.getRefreshToken(email).contains("\"")) {
storedRefreshToken = storedRefreshToken.replace("\"", "");
}
if (!storedRefreshToken.equals(refreshToken)) {
throw new CustomException(ErrorCode.TOKEN_MISMATCH);
}
String newAccessToken = jwtUtil.createAccessToken(user);
String newRefreshToken = jwtUtil.createRefreshToken(user);
redisDao.setRefreshToken(email, newRefreshToken, jwtUtil.getREFRESHTOKEN_TIME());
return new AuthResponseDto(newAccessToken, newRefreshToken);
}
이렇게 추가해주니 잘 작동하기는 한다.
그런데 여전히 찜찜하다. 해결할 방법이 있을 것 같은데...(댓글로 해결책 달아주시면 감사하겠습니다.)
'내일배움캠프' 카테고리의 다른 글
[최종프로젝트]Embedded Redis 이슈 (1) | 2024.08.27 |
---|---|
[최종프로젝트] 프로필 페이지 런타임 에러 (0) | 2024.08.20 |
public class Dev {}(최종프로젝트) 2일차 (1) | 2024.07.22 |
Trello프로젝트(심화 프로젝트) 5일차(KPT 회고) (2) | 2024.07.22 |
Trello프로젝트(심화 프로젝트) 4일차 (6) | 2024.07.20 |