이전 [Docker] 도커를 사용해서 로컬에 mysql, redis 구성 게시글에서 도커에 Redis 컨테이너를 띄우는 것을 알아봤다.
그래서 이 Redis에 accessToken과 refreshToken을 저장하여 accessToken 만료, refreshToken 유효한 상태인 경우 Redis에서 refreshToken을 조회해서 자동 갱신해 주는 것을 해보려고 한다.
Redis 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
build.gradle 파일에 Redis 의존성을 추가한다.
RedisConfig 작성
# application.yml
redis:
url: localhost
port: 6379
application.yml에 Redis 서버 url과 port번호를 설정한다. 로컬 도커에 실행시켰기 때문에 서버 url은 localhost로 설정했다.
@Configuration
public class RedisConfig {
@Value("${redis.url}")
private String url;
@Value("${redis.port}")
private String port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(url);
redisStandaloneConfiguration.setPort(Integer.parseInt(port));
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration);
return lettuceConnectionFactory;
}
}
RedisConnectionFactory를 Bean으로 등록한다.
TokenInfo RedisHash추가
@RedisHash(value = "tokenInfo", timeToLive = 60*60*3) // TTL : 3시간
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TokenInfo {
@Id
private String accessToken;
private String refreshToken;
}
spring-data-jpa의 @Entity처럼 @RedisHash 어노테이션을 사용해서 Redis의 Hash를 만들 수 있다. TTL을 refreshToken의 만료시간과 동일한 3시간으로 설정해 만료되는 시점에 Redis의 key-value 데이터도 같이 사라진다.
accessToken을 Id로 갖고 accessToken을 통해 refreshToken을 조회해오기 위해 위와 같은 TokenInfo 클래스를 만들었다.
TokenRepository
public interface TokenRepository extends CrudRepository<TokenInfo, String> {
}
repository 또한 jpa와 동일하게 인터페이스를 정의하면 findById, findAll, ... 과 같은 메소드를 통해 Redis에 데이터를 조회하고 저장할 수 있다.
TokenService
@Service
@RequiredArgsConstructor
public class TokenInfoService {
private final TokenRepository tokenRepository;
public TokenDto getTokenInfoById(String accessToken) {
TokenInfo tokenInfo = tokenRepository.findById(accessToken)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.OK, ErrorCode.EMPTY_DATA.name()));
return TokenDto.builder()
.accessToken(tokenInfo.getAccessToken())
.refreshToken(tokenInfo.getRefreshToken())
.build();
}
public void saveTokenInfo(TokenInfo tokenInfo) {
tokenRepository.save(tokenInfo);
}
public void deleteTokenInfoById(String accessToken) {
tokenRepository.deleteById(accessToken);
}
}
TokenService에서 등록, 조회, 삭제 기능만 추가했다. 이제 해당 메소드들을 필요한 상황에서 호출하기만 하면 내 App에서 Redis를 사용할 수 있는 것이다.
로그인 성공
public TokenDto login(LoginDto loginDto, HttpServletResponse response) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDto.getUserId(), loginDto.getPassword());
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
TokenDto tokenDto = tokenProvider.generateToken(authentication);
CookieUtil.generateCookie(response, "accessToken", tokenDto.getAccessToken());
// Redis에 토큰 정보 저장
tokenInfoService.saveTokenInfo(
TokenInfo.builder()
.accessToken(tokenDto.getAccessToken())
.refreshToken(tokenDto.getRefreshToken())
.build()
);
return tokenDto;
}
로그인 메소드에 TokenProvider에서 생성한 accessToken과 refreshToken을 TokenInfo 객체로 만들어 Redis에 저장하는 로직을 추가했다.
로그인 성공 시 토큰이 잘 저장되고 TTL도 적용되어 있는 것을 확인할 수 있다.
토큰 자동 갱신
이제 포스트 제목대로 Jwt 필터로 들어온 요청이 만료된 액세스토큰을 가지고 온 경우 Redis의 refreshToken을 조회해 와서 재검증 후 만약 refreshToken이 유효하다면 토큰을 갱신해 줄 것이다.
JWT 필터의 doFilterInternal()메소드를 보자.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 쿠키 or 헤더에서 토큰 추출
String accessToken = CookieUtil.getAccessToken(request);
if (StringUtils.hasText(accessToken)) {
try {
// 액세스 토큰이 유효한 경우
if (tokenProvider.validateToken(accessToken)) {
Authentication authentication = tokenProvider.getAuthentication(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (ExpiredJwtException accessTokenEx) {
// 액세스 토큰이 만료된 경우 ExpiredJwtException 발생하여 catch문에서 재시도
try {
// Redis에서 refreshToken 조회
String refreshToken = tokenInfoService.getTokenInfoById(accessToken).getRefreshToken();
// refreshToken이 유효한 경우
if (tokenProvider.validateToken(refreshToken)) {
// Redis에 저장된 기존 토큰정보 삭제
tokenInfoService.deleteTokenInfoById(accessToken);
// 토큰 재발급
TokenDto tokenDto = tokenProvider.reissueToken(refreshToken);
// Redis에 재발급 된 토큰 정보 저장
tokenInfoService.saveTokenInfo(
TokenInfo.builder()
.accessToken(tokenDto.getAccessToken())
.refreshToken(tokenDto.getRefreshToken())
.build());
Authentication authentication = tokenProvider.getAuthentication(tokenDto.getAccessToken());
SecurityContextHolder.getContext().setAuthentication(authentication);
CookieUtil.generateCookie((HttpServletResponse) response, "accessToken", tokenDto.getAccessToken());
}
} catch (ExpiredJwtException | ResponseStatusException ex) {
log.info("Expired refreshToken");
}
}
}
filterChain.doFilter(request, response);
}
코드에 주석달아둔 것처럼 accessToken이 만료되면 Redis의 refreshToken을 조회해서 재검증, refreshToken도 만료되었다면 로그인 API를 다시 호출해야 한다.
끝
'Spring Boot' 카테고리의 다른 글
[Spring Boot] @AuthenticationPrincipal 사용해서 사용자 정보 조회 (0) | 2023.12.25 |
---|---|
[Spring Boot] 로그인 성공 시 쿠키에 accessToken 넣기 (0) | 2023.12.25 |
[Spring Boot] @Valid 어노테이션 사용하기 (0) | 2023.12.24 |