Spring Security - Refresh Token을 Redis에 저장하여 Access Token 재발급기능 구현하기

TopOfTheHead·2026년 1월 28일

Spring Security

목록 보기
21/21
post-thumbnail

전체 과정

클라이언트 요청
  ↓
Access Token 만료
  ↓
/auth/refresh 요청 (Refresh Token 전달)
  ↓
Refresh Token 검증
  ├─ 서명/만료 검증
  ├─ Redis 저장 여부 확인
  ├─ 사용자 일치 여부 확인
  ↓
새 AT + (선택) 새 RefreshToken 발급
  ↓
Redis RefreshToken 갱신

클라이언트로그인JWT Token을 발급하는 경우 서버RedisRefresh Token를 저장
Access Token 재발급 end point : /auth/refresh

  • 서버에서 Refresh TokenRedis에 저장하는 이유
    NoSQLKey - Value 자료구조로서 인메모리 DB 방식으로 빠르게 접근

    인메모리 DB 서버이므로 브라우저에 비해 서버 사이드에 있으므로 탈취가능성이 낮음

    Refresh Token비밀번호처럼 영구히 남는 데이터가 아니므로 인메모리 DB를 통해 설사 데이터만료되더라도 큰 문제가 발생하지 않음.

사전 설정

  • redis 설치
docker run --name redis-container -d -p 6379:6379 redis
  • Spring 설정
implementation 'org.redisson:redisson-spring-boot-starter:3.32.0'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
spring:
  data:
    redis:
      host: ${redis.host:localhost}
      port: ${redis.host:6379}
  datasource:
    hikari:
      maximum-pool-size: 100

spring: datasource: hikari: maximum-pool-size 는 최대 DB Connection 수를 지정
▶ 약 100개의 스레드로 통한 DB 트랜잭션을 각각 수행하므로 100으로 지정

spring: data: redis의 설정정보는 RedisConfiguration에서 RedisProperties를 통해 의존성주입되어 URI 설정 시 활용됨
환경변수로 값 설정

  • RedisConfiguration file 생성
    RedissonClient를 활용하기위해 Spring Bean으로 등록

    RedissonClient에 설정될 URI 정보는 RedisProperties 객체를 통해 의존성 주입application.ymlspring: data: redis에서 작성한 host, port 정보를 읽어와서 URI로 작성 후 입력
    redis://localhost:6379
    RedisConnectionFactory 도 동일
@Configuration
@RequiredArgsConstructor
public class RedisConfiguration {
	// application.yml 에서 redis 관련 설정정보 가져오는 객체
	private final RedisProperties redisProperties;
	// RedissonClient 를 Spring Bean으로 등록
	// 이후 분산락에서 사용
	@Bean
	public RedissonClient redisClient() {
		Config config = new Config();
		// URI 설정
		String uri = String.format("redis://%s:%s", redisProperties.getHost(), redisProperties.getPort());
		config.useSingleServer().setAddress(uri);
		return Redisson.create(config);
	}
	// RedisConnectionFactory 를 return
	@Bean
	public RedisConnectionFactory redisConnectionFactory() {
		return new LettuceConnectionFactory(redisProperties.getHost(),redisProperties.getPort());
	}
}

RedisJava ClientLettuceJedis가 존재하는데 Lettuce를 사용.

Redis발급Refresh Token 저장할 Entity를 선언
。선언된 Entity Class 구조대로 Redis에 저장됨.

import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.annotation.Id;
@RedisHash(value = "refreshToken", timeToLive = 43200) // 12시간
@NoArgsConstructor
@Getter
@ToString
public class RefreshToken {
	@Id
	private String refreshToken;
	private UUID id;
	private String userId;
	public RefreshToken(String refreshToken, UUID id, String userId) {
		this.refreshToken = refreshToken;
		this.id = id;
		this.userId = userId;
	}
}
  • @RedisHash
    Redis Lettuce를사용하여 Redis에 저장할 자료구조임을 지시하는 어노테이션

    value = "키" : Redis에서 해당 자료Key namespace가 된다.
    Redis DB에 저장 시 Key : {value값}:{@Id필드값}으로 지정됨.

  • @Id
    。선언된 필드Redis Key 값으로 설정됨
    null인 경우 랜덤값으로 설정

    @Id 필드RefreshToken로 설정
    ▶ 이후 재발급 시 클라이언트에서 전송한 쿠키에 포함된 RefreshToken를 통해 해당 RefreshToken을 찾아야하므로.

    JPA@Id가 아닌 import org.springframework.data.annotation.Id;@Id를 선언해야한다.

    @RedisHash(value = "keynamespace")@Id Long id필드값"id"인 경우
    Redis 자료구조에서는 keynamespace:id키값으로 저장

RefreshToken Repository 생성
CrudRepository<>를 확장하여 생성
JpaRepository<>를 선언 시 JPARedis 가 동시에 해당 Entity를 참조하려 하면서 오류 발생하므로

@Repository
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
	default RefreshToken findByRefreshTokenByIdOrThrow(String refreshToken) {
		return findById(refreshToken).orElseThrow(()->new CustomException(ErrorCode.TOKEN_NOT_FOUND));
	}
}

클라이언트에서 전송한 RefreshToken을 기반으로 Redis에서 저장된 RefreshToken을 찾는 메서드 작성
RefreshToken 클래스@Id 필드 = RefreshToken인 이유

JWT 발급은 위해 로그인을 하는 단계에서 다음 RefreshToken Repository를 통해 Redis에 저장하는 코드 추가

private final RefreshTokenRepository refreshTokenRepository;
	@Override
	public Pair<String,String> LoginAccount(AccountRequest.Login request) {
		Account foundedAccount = accountRepository.findByEmailOrThrow(request.email());
//
		PreConditions.validate(
		//	passwordEncoder.matches(request.password(),foundedAccount.getPassword()),
			ErrorCode.FAIL_LOGIN
		);
//
		PreConditions.validate(
		//	foundedAccount.getStatus().equals(AccountStatus.ACTIVATED),
			ErrorCode.REMOVED_ACCOUNT
		);
//
		String accessToken = jwtService.issue(
			foundedAccount.getId(),
		//	pojoJwtProperties.getJwt().accessTokenExpiration()
		);
//
		String refreshToken = jwtService.issue(
			foundedAccount.getId(),
		//	pojoJwtProperties.getJwt().refreshTokenExpiration()
		);
//
		refreshTokenRepository.save(
			new RefreshToken(
				refreshToken,
				foundedAccount.getId(),
				foundedAccount.getEmail()
				)
		);
		return Pair.of(accessToken, refreshToken);
	}

。이후 로그인하여 JWT 토큰 생성 및 Redis에서 조회 시 다음처럼 저장됨을 확인가능.


refreshToken:wjdtn747@naver.com :
refreshToken : @RedisHash(value="refreshToken")접두사
wjdtn747@naver.com : RefreshToken 클래스에서 @Id 필드에 해당

Refresh TokenString이 아니므로 출력되지 않음.
HGETALL 키명 입력 시 출력

ACCESS TOKEN 재발급
클라이언트만료Access Token을 전송 시 401 UnAthorize를 반환하며, 이에 대응하여 클라이언트/api/auth/refresh를 호출

JWT 로그인 로직 작성에서 구현한 로직에 따라 Cookie에 포함된 Refresh Token을 가져오는 로직 사용

  • 서비스 레이어에서 Refresh Token을 추출하는 메서드 작성
    Cookie에서 찾은 RefreshToken를 기반으로 Redis DB에 저장된 RefreshToken을 검색 후 존재 시 Access Token을 재발급 후 컨트롤러로 반환
	@Override
	public String generateAccessToken(String refreshToken) {
		PreConditions.validate(
			Strings.isNotBlank(refreshToken),
			ErrorCode.REFRESH_TOKEN_NOT_FOUND
		);
		// Client에서 전송한 RefreshToken으로 Redis DB에 저장된 RefreshToken 조회
		RefreshToken foundedRefreshToken = refreshTokenRepository
			.findByRefreshTokenByIdOrThrow(refreshToken);
		//
		return jwtService.issue(
			foundedRefreshToken.getId(),
			pojoJwtProperties.getJwt().accessTokenExpiration()
		);
	}
  • 컨트롤러 레이어에서 HttpRequest를 받아서 서비스레이어에서 생성한 Access Token을 반환하는 로직 작성
    HttpRequest에서 Cookie를 추출하는 메서드 작성
    JWT 로그인 로직에서 설정한 Cookie명을 활용하여 해당 Refresh Token을 찾는다.
    private static final String cookieName = "RT";
	@PostMapping("/refresh")
	public ResponseEntity<ApiResultResponse<AuthResponse.AccessToken>> refresh(
		HttpServletRequest request
	){
		String refreshToken = resolveToken(request);
		String accessToken = authService.generateAccessToken(refreshToken);
		return ApiResultResponse.data(
			SuccessCode.ACCESS_TOKEN_REFRESH_SUCCESS,
			new AuthResponse.AccessToken(accessToken)
		);
	}
	public String resolveRefreshToken(HttpServletRequest request) {
		Cookie[] cookies = request.getCookies();
		if( cookies != null ){
			for(Cookie cookie : cookies){
				if(cookieName.equals(cookie.getName())){
					return cookie.getValue();
				}
			}
		}
		return null;
	}

클라이언트Access Token이 만료되더라도 브라우저에서 RefreshToken을 가진 유효한 Cookie를 포함하는 경우, 해당 API를 호출하여 RefreshToken을 발급받을 수 있다.

  • JWT 필터에서 해당 /api/auth/refresh를 거치지않도록 작성하기
	@Override
	protected void doFilterInternal(
		HttpServletRequest request,
		HttpServletResponse response,
		FilterChain chain
	) throws ServletException, IOException{
		String uri = request.getRequestURI();
		//
		if (uri.startsWith("/api/auth/")) {
			chain.doFilter(request, response);
			return;
		}


▶ 이후 RefreshToken을 포함한 쿠키를 기반으로 Access Token이 새로 생성됨을 확인 가능

profile
공부기록 블로그

0개의 댓글