이번 포스팅에선 데이터베이스에 저장하던 Refresh Token을 인 메모리 데이터베이스인 Redis에 저장하고, Refresh Token을 사용하여 Access Token을 발급하는 과정을 살펴보겠습니다.
dependencies {
...
// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
spring:
redis:
data:
host: localhost
port: 6379
@Getter
@AllArgsConstructor
@RedisHash(value = "refreshToken", timeToLive = 60 * 60 * 24 * 7)
public class RefreshToken {
@Id
private String refreshToken;
private Long memberId;
}
Redis에 저장할 Refresh Token을 정의합니다.
Refresh Token은 Redis에 저장하여 JPA 의존성이 필요하지 않기 때문에, @Id 어노테이션은 java.persistence.id가 아닌 opg.springframework.data.annotation.Id를 import 합니다.
Redis 데이터의 유효시간은, timetoLive 옵션으로 refresh Token과 같은 시간인 7일로 지정하였습니다.
@Repository
public interface RefreshTokenRepository extends CrudRepository<RefreshToken,String> {
}
Repository 또한 JPA 의존성이 필요하지 않기 때문에 JpaRepository가 아닌 CrudRepository를 상속 받습니다.
@Configuration
public class RedisConfig {
@Value("${spring.redis.data.host}")
private String host;
@Value("${spring.redis.data.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
}
@Service
@RequiredArgsConstructor
public class AuthService {
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final TokenProvider tokenProvider;
private final RefreshTokenRepository refreshTokenRepository;
@Transactional
public MemberResponseDTO signup(MemberRequestDTO memberRequestDTO) {
if (memberRepository.existsByUsername(memberRequestDTO.getUsername())) {
throw new CustomException(ErrorCode.DUPLICATE_USER_ID);
}
Member member = memberRequestDTO.toMember(passwordEncoder);
return MemberResponseDTO.of(memberRepository.save(member));
}
@Transactional
public TokenDTO login(MemberRequestDTO memberRequestDTO) {
// 1. Login ID/PW 를 기반으로 AuthenticationToken 생성
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(memberRequestDto.getUsername(), memberRequestDto.getPassword());
// 2. 실제로 검증 (사용자 비밀번호 체크) 이 이루어지는 부분
// authenticate 메서드가 실행이 될 때 CustomUserDetailsService 에서 만들었던 loadUserByUsername 메서드가 실행됨
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
Member member = memberRepository.findByUsername(memberRequestDTO.getUsername())
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
// 3. 인증 정보를 기반으로 JWT 토큰 생성
String accessToken = tokenProvider.generateAccessToken(authentication);
String refreshToken = tokenProvider.generateRefreshToken(authentication);
// 4. Redis에 RefreshToken 저장
refreshTokenRepository.save(
new RefreshToken(refreshToken, member.getId())
);
// 5. 토큰 발급
return TokenDTO.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.memberId(member.getId())
.build();
}
@Transactional
public TokenDTO reissue(RequestTokenDTO requestTokenDTO) {
// 1. Redis에 Refresh Token이 저장되어 있는지 확인
RefreshToken foundTokenInfo = refreshTokenRepository.findById(requestTokenDTO.getRefreshToken())
.orElseThrow(() -> new CustomException(ErrorCode.TOKEN_NOT_FOUND));
Member member = memberRepository.findById(foundTokenInfo.getMemberId())
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
String refreshToken = foundTokenInfo.getRefreshToken();
tokenProvider.validateToken(refreshToken);
// 2. Refresh Token으로 부터 인증 정보를 꺼냄
Authentication authentication = tokenProvider.getAuthentication(refreshToken);
// 3. 새로운 Access Token 생성
String accessToken = tokenProvider.generateAccessToken(authentication);
// Token 재발급
return TokenDTO.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.memberId(member.getId())
.build();
}
}
login()
사용자의 로그인 요청을 처리하고, 입력된 ID와 비밀번호로 UsernamePasswordAuthenticationToken을 생성하여 Spring Security의 AuthenticationManager를 통해 실제로 검증합니다.
authenticate 메서드가 실행이 될 때 CustomUserDetailsService에서 만들었던 loadUserByUsername 메서드가 실행되므로 검증이 성공하면 해당 인증 정보를 기반으로 JWT 토큰을 생성하고, Refresh Token을 생성하여 Redis에 저장합니다.
reissue()
Access Token을 사용하여 Redis에 저장된 Refresh Token의 유효성 검사 후, Access Token을 재발급하는 메서드입니다.
먼저 Redis에 Refresh Token이 저장되어 있는지 확인하고, Refresh Token으로 부터 인증 정보를 꺼내 새로운 Access Token을 발급하고 Redis에도 업데이트를 해줍니다.
설치
$ docker pull redis
실행
$ docker run --name redis -p 6379:6379 -d redis
Redis-cli 접속
$ docker exec -it redis redis-cli
Redis를 실행하기 위해 다음과 같은 Docker 명령어를 입력해 줍니다.
로그인을 진행하였을 때, Access Token과 Refresh Token이 정상적으로 발급된 것을 확인할 수 있습니다.
또한 Redis에도 Token이 저장된 것을 확인할 수 있습니다.
Access Token이 만료되었을 경우, Refresh Token을 사용하여 재발급 또한 정상적으로 이루어지는 것을 확인할 수 있습니다.