리프래쉬 토큰을 redis에서 관리하려고 구현하는 과정에서 발생한 문제다.
redis에서 value(rtk)값으로 해당하는 데이터를 가져오는게 안된다.
분명히 아래와 같이 redis에 저장되어 있고 디버깅해보면 저장된 value값 그대로 대입되는걸 여러번 확인했는데 오류가 반복된다.
127.0.0.1:6379> HGETALL refreshToken:eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0MTFAZ21haWwuY29tIiwiaWF0IjoxNjg0MzQ5MjAxLCJleHAiOjE2ODQzNzQ0MDF9.R_kJJ_Wqh36nYahBCP0DNCh2meQcJeupSuP8WgIuF0A
1) "refreshToken"
2) "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0MTFAZ21haWwuY29tIiwiaWF0IjoxNjg0MzQ5MjAxLCJleHAiOjE2ODQzNzQ0MDF9.R_kJJ_Wqh36nYahBCP0DNCh2meQcJeupSuP8WgIuF0A"
3) "_class"
4) "com.codestates.edusync.security.auth.refresh.RefreshToken"
5) "memberId"
6) "6"
127.0.0.1:6379>
리프래쉬 토큰을 이용해 엑세스 토큰을 재발급 하려고 하면
refreshTokenRepository.findByRtk(refreshToken);
메서드를 통해 redis에서 올바른 객체를 가져오지를 못하고 계속 null을 가져와서 Invalid refresh token [redis]
가 출력된다.
@RestController
@RequestMapping("/refresh")
@RequiredArgsConstructor
public class RefreshController {
private final JwtTokenizer jwtTokenizer;
private final MemberRepository memberRepository;
private final TokenService tokenService;
private final RefreshTokenRepository refreshTokenRepository;
@PostMapping
public ResponseEntity<String> refreshAccessToken(HttpServletRequest request) { // 리프레쉬 토큰 받으면 엑세스 토큰 재발급
String refreshTokenHeader = request.getHeader("Refresh");
if (refreshTokenHeader != null && refreshTokenHeader.startsWith("Bearer ")) {
String refreshToken = refreshTokenHeader.substring(7);
try {
------------------------------------------오류 발생------------------------------------------
RefreshToken refreshTokenObj = refreshTokenRepository.findByRtk(refreshToken);
------------------------------------------오류 발생------------------------------------------
if (refreshTokenObj == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token [redis]");
}
Jws<Claims> claims = jwtTokenizer.getClaims(refreshToken, jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()));
String email = claims.getBody().getSubject();
Optional<Member> optionalMember = memberRepository.findByEmail(email);
if (optionalMember.isPresent()) {
Member member = optionalMember.get();
String accessToken = tokenService.delegateAccessToken(member);
return ResponseEntity.ok().header("Authorization", "Bearer " + accessToken).body("Access token refreshed");
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid member email");
}
} catch (JwtException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token");
}
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Missing refresh token");
}
}
}
@Repository
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
RefreshToken findByRtk(String refreshToken);
}
@RedisHash(value = "refreshToken", timeToLive = 420)
public class RefreshToken {
@Id
private String rtk;
private Long memberId;
public RefreshToken(final String rtk, final Long memberId) {
this.rtk = rtk;
this.memberId = memberId;
}
public String getRefreshToken() {
return rtk;
}
public Long getMemberId() {
return memberId;
}
}
@Service
@RequiredArgsConstructor
public class TokenService {
private final RefreshTokenRepository refreshTokenRepository;
private final JwtTokenizer jwtTokenizer;
private final MemberUtils memberUtils;
// Access Token을 생성하는 구체적인 로직
public String delegateAccessToken(Member member) {
String email = member.getEmail();
Map<String, Object> claims = new HashMap<>();
claims.put("email", email);
claims.put("roles", member.getRoles());
claims.put("nickName", member.getNickName());
Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes());
String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());
String accessToken = jwtTokenizer.generateAccessToken(claims, email, expiration, base64EncodedSecretKey);
return "Bearer " + accessToken;
}
// Refresh Token을 생성하는 구체적인 로직
public String delegateRefreshToken(Member member) {
String subject = member.getEmail();
Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes());
String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());
String refreshToken = jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey);
/*
redis 설치가 귀찮으시다면 아래 두줄 주석처리하시면 됩니다.
*/
RefreshToken rtk = new RefreshToken(refreshToken, member.getId());
refreshTokenRepository.save(rtk);
return "Bearer " + refreshToken;
}
}
2가지 문제점이 있었다.
아래와 같이 RefreshTokenRepository
인터페이스에 @Repository
애너테이션을 삭제해 해결할 수 있었다.
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
Optional<RefreshToken> findByRtk(String refreshToken);
}
일반적으로 필드값 기준으로 한다면 기존의 findByRtk()
가 맞지만 rtk필드는 @ID 애너테이션 붙어있으므로 findById()
로 작성하는게 맞다.
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
Optional<RefreshToken> findById(String refreshToken);
}
정상작동!!!
이것저것 테스트하는 과정에서 리프래쉬 토큰의 길이가 너무 길어 아래와 같이 간단하게 바꿔서 테스트 했다.
@RestController
@RequestMapping("/redis")
@RequiredArgsConstructor
public class RedisTest {
private final RefreshTokenRepository refreshTokenRepository;
@PostMapping
public ResponseEntity<String> redisTest(HttpServletRequest request) {
RefreshToken rtk = new RefreshToken("111", 111L);
refreshTokenRepository.save(rtk);
Optional<RefreshToken> refreshTokenObj = refreshTokenRepository.findByRtk("111");
return ResponseEntity.ok().body(refreshTokenObj.get().getRefreshToken());
}
}
데이터 저장은 항상 아래와 같이 2가지 방법으로 확인했고 저장까지는 항상 문제 없었다.
따라서 redis에서 데이터를 꺼내오는 작업에서 발생하는 문제라고 범위를 줄일 수 있었고 오류를 해결하는데 걸리는 시간을 줄일 수 있었다.
127.0.0.1:6379> HGETALL refreshToken:111
1) "_class"
2) "com.codestates.edusync.security.auth.refresh.RefreshToken"
3) "memberId"
4) "111"
5) "rtk"
6) "111"
127.0.0.1:6379>