이전 Spring Security + JWT 로그인 구현에서 마지막으로 Refresh Token 저장을 위한 Redis를 추가하고, 토큰 재발급 기능을 추가하여 로그인 기능을 완성하였습니다.
앞서 포스팅 한 Security + JWT 로그인 구현 과정입니다. 이어지는 내용이기 때문에 참고 부탁드리겠습니다.
Redis 사용법에 대한 내용입니다. 해당 로그인 기능에서는 RedisTemplate을 사용하여 기능을 구현하였습니다.
* 해당 두 포스팅은 내용 이해에 필요한 부분이라 생각하여 링크로 올려놨으며, GitHub 전체 코드 및 다른 추가적으로 참고할 수 있는 내용은 포스팅 맨 하단에 링크해두겠습니다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
Gradle 의존성 추가
Spring Boot 에서는 Spring Data Redis를 통해 Lettuce, Jedis라는 두 가지 오픈소스 라이브러리를 사용할 수 있습니다. 의존성을 추가할 때 별도의 설정을 하지 않으면 Lettuce가 기본으로 적용됩니다.
위 'Redis 사용법' 포스팅에 조금 더 자세한 내용이 담겨있지만, 해당 코드에서는 RedisTemplate을 이용하여 Redis를 사용합니다.
@RequiredArgsConstructor
@Configuration
@EnableRedisRepositories
public class RedisRepositoryConfig {
private final RedisProperties redisProperties;
// lettuce
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
}
RedisConnctionFactory 인터페이스를 통해 LettuceConnectionFactory를 생성하여 반환합니다.
setKeySerializer, setValueSerializer 설정을 함으로써 redis-cli을 통해 직접 데이터를 볼 수 있습니다.
# Redis
spring.redis.host=localhost
spring.redis.port=6379
RedisProperties를 통해서 properties에 저장한 host, port를 가지고 와서 연결합니다.
public ResponseEntity<?> login(UserRequestDto.Login login) {
if (usersRepository.findByEmail(login.getEmail()).orElse(null) == null) {
return response.fail("해당하는 유저가 존재하지 않습니다.", HttpStatus.BAD_REQUEST);
}
// 1. Login ID/PW 를 기반으로 Authentication 객체 생성
// 이때 authentication 는 인증 여부를 확인하는 authenticated 값이 false
UsernamePasswordAuthenticationToken authenticationToken = login.toAuthentication();
// 2. 실제 검증 (사용자 비밀번호 체크)이 이루어지는 부분
// authenticate 매서드가 실행될 때 CustomUserDetailsService 에서 만든 loadUserByUsername 메서드가 실행
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
// 3. 인증 정보를 기반으로 JWT 토큰 생성
UserResponseDto.TokenInfo tokenInfo = jwtTokenProvider.generateToken(authentication);
// 4. RefreshToken Redis 저장 (expirationTime 설정을 통해 자동 삭제 처리)
redisTemplate.opsForValue()
.set("RT:" + authentication.getName(), tokenInfo.getRefreshToken(), tokenInfo.getRefreshTokenExpirationTime(), TimeUnit.MILLISECONDS);
return response.success(tokenInfo, "로그인에 성공했습니다.", HttpStatus.OK);
}
Login 과정의 Service단 로직입니다.
나머지 코드는 기존에 구현된 부분이고 '4. RefreshToken Redis 저장' 부분이 해당 포스팅에서 추가된 부분인데요.
RefreshToken 저장을 위해 사용한 set 메서드는 key, value 값 외에 long type의 timeout, TimeUnit type의 unit을 인자로 받습니다.
해당 메서드의 용도는 'Set the value and expiration timeout for key.' refresh token의 유효시간 정보를 함께 저장해서 토큰이 만료되었을 때 value 값을 자동으로 삭제하는 기능을 위해 사용하였습니다.
이렇게 로그인이 성공했을 때 redis에 refresh token이 정상적으로 저장되는 것을 볼 수 있습니다. 또한 TTL (key) 명령어를 통해 해당 key 값의 유효시간도 정상적으로 저장된 것을 확인할 수 있습니다.
public ResponseEntity<?> reissue(UserRequestDto.Reissue reissue) {
// 1. Refresh Token 검증
if (!jwtTokenProvider.validateToken(reissue.getRefreshToken())) {
return response.fail("Refresh Token 정보가 유효하지 않습니다.", HttpStatus.BAD_REQUEST);
}
// 2. Access Token 에서 User email 를 가져옵니다.
Authentication authentication = jwtTokenProvider.getAuthentication(reissue.getAccessToken());
// 3. Redis 에서 User email 을 기반으로 저장된 Refresh Token 값을 가져옵니다.
String refreshToken = (String)redisTemplate.opsForValue().get("RT:" + authentication.getName());
if(!refreshToken.equals(reissue.getRefreshToken())) {
return response.fail("Refresh Token 정보가 일치하지 않습니다.", HttpStatus.BAD_REQUEST);
}
// 4. 새로운 토큰 생성
UserResponseDto.TokenInfo tokenInfo = jwtTokenProvider.generateToken(authentication);
// 5. RefreshToken Redis 업데이트
redisTemplate.opsForValue()
.set("RT:" + authentication.getName(), tokenInfo.getRefreshToken(), tokenInfo.getRefreshTokenExpirationTime(), TimeUnit.MILLISECONDS);
return response.success(tokenInfo, "Token 정보가 갱신되었습니다.", HttpStatus.OK);
}
Service단에 있는 토큰이 재발급 기능을 하는 reissue() 메서드입니다.
@Getter
@Setter
public static class Reissue {
@NotEmpty(message = "accessToken 을 입력해주세요.")
private String accessToken;
@NotEmpty(message = "refreshToken 을 입력해주세요.")
private String refreshToken;
}
토큰 갱신을 위해서는 accessToken, refreshToken 값이 필요합니다.
갱신 과정은
- Refresh Token이 유효한지 검증합니다.
- Access Token에서 Authentication 객체를 가지고 와서 저장된 name(email)을 가지고 옵니다.
- email을 가지고 Redis에 저장된 Refresh Token을 가지고 와서 입력받은 Refresh Token 값과 비교합니다.
- Authentication 객체를 가지고 새로운 토큰을 생성합니다.
- Redis에 새로 생성된 Refresh Token을 저장합니다.
- 클라이언트에게 새로 발급된 토큰 정보를 내려줍니다.
* 해당 포스팅은 JwtTokenProvider class의 메서드들과 같이 이전 포스팅에서 나왔던 부분의 자세한 설명을 하지 않아 이해가 어려울 수 있습니다. 아래 전체 코드와 이전 Secutiry + JWT 로그인 구현 포스팅의 내용을 참고하시면 이해에 도움이 될 것 같습니다.
GitHub 전체 코드
함께 보면 좋은 자료
참고 자료
'Programming > Spring Boot' 카테고리의 다른 글
RestTemplate 4xx, 5xx Code 처리하는 방법 (HttpClientException) (0) | 2021.10.04 |
---|---|
JWT + Redis Logout 로그아웃 구현하기 (13) | 2021.09.25 |
spring security + JWT 로그인 기능 파헤치기 - 2 (4) | 2021.09.22 |
spring security + JWT 로그인 기능 파헤치기 - 1 (9) | 2021.09.22 |
UsernameNotFoundException Not Working 이유 파헤치기 (0) | 2021.09.20 |