Notice
Recent Posts
Recent Comments
Link
«   2025/06   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

kym8821 님의 블로그

Access Token와 Refresh Token을 활용한 인증과 인가 본문

Spring/기초 개념

Access Token와 Refresh Token을 활용한 인증과 인가

kym8821 2025. 2. 7. 02:49

단일 JWT 토큰을 활용한 로그인의 문제점

단일 JWT 토큰을 사용한 로그인 방식은 간편하지만 아래와 같은 문제점이 있었다.

  • 토큰이 탈취되었을 때, 계정 해킹에 취약해짐
  • 토큰 관리를 사용자가 하므로 토큰이 탈취되어도 할 수 있는 것이 많지 않음

이러한 문제를 어느정도 해결하기 위해 access token와 refresh token을 활용한다.


Access Token와 Refresh Token

일단 각 토큰의 역할부터 알아보도록 하자.

Access Token

Access Token은 사용자 인증 및 인가를 위해 사용하는 토큰이다.

Access Token은 짧은 유효기간을 갖도록 설계된다.

따라서 탈취자가 운좋게 토큰을 탈취해도 다시 탈취를 시도해야 한다.

Refresh Token

Refresh Token은 Access Token을 발급하기 위해 사용하는 토큰이다.

Refresh Token은 Access Token와 달리 긴 유효기간을 갖고 있다

Access Token와 Refresh Token을 활용한 인증 절차

아래는 일반적인 상황에서의 인증 절차이다.

  1. 로그인 성공시 Access Token와 Refresh Token을 클라이언트에게 전달
  2. 클라이언트는 요청시 Access Token을 헤더에 포함하여 전달
  3. 서버는 클라이언트의 요청에서 Access Token을 추출하여 요청의 유효성을 판단

아래는 Access Token 혹은 Refresh Token이 만료되었을 때의 절차이다.

  1. 클라이언트가 Access Token을 헤더에 포함하여 요청을 전달
  2. Access Token이 만료되었으므로 서버는 클라이언트에게 Access Token이 만료되었음을 알림(401)
  3. 클라이언트는 Refresh Token을 활용하여 토큰을 재요청
  4. 서버는 클라이언트에게 새로운 Access Token와 Refresh Token을 발급함

Refresh Token이 만료되었을 때도 토큰 재요청시 Access Token와 Refresh Token을 발급받을 수 있다.

 

토큰 탈취 상황과 Refresh Token Rotation

토큰 탈취 시나리오

아래는 Access Token이 탈취되었을 때의 시나리오이다.

  1. 공격자가 Access Token을 탈취
  2. 토큰 만료 후 공격자는 토큰을 다시 탈취해야함

짧은 만료시간을 갖는 Access Token이기에 탈취자는 또다시 탈취를 시도해야 할 것이다.

 

아래는 Refresh Token이 탈취되었을 때의 시나리오이다.

  1. 공격자가 Refresh Token을 탈취
  2. Refresh Token으로 Access Token와 Refresh Token을 재발급
  3. Access Token 만료시 Refresh Token을 활용하여 토큰을 재발급

refresh token을 활용한 통신은 자주 발생하는 것이 아니기에 탈취가 어렵다.

하지만 위처럼 탈취 되었을 때의 위험이 크기도 하다.

이러한 이유로 Refresh Token Rotation을 활용하여 문제를 보완할 수 있다.

Refresh Token Rotation

Refresh Token Rotation은 Access Token이 만료되었을 때, Refresh Token도 함께 교체해준다.

즉 Refresh Token의 재사용이 불가능하도록 만들기 위한 방법이다.

 

Refresh Token Rotation을 활용했을 때, 어떤 방식으로 문제가 보완되는지 확인해보자.

  1. 공격자가 RT1 탈취 후 토큰 재발급
  2. 서버는 RT1을 만료시키고, 새로운 토큰인 AT2와 RT2를 전달함
  3. 기존 사용자의 AT1가 만료된 후 RT1으로 토큰 재발급 시도
  4. RT1은 만료된 토큰이므로 모든 RT를 무효화함

즉, 만료된 refresh token으로 토큰 재발급을 시도하면, 서버는 이를 공격으로 인식하고 토큰을 무효화한다.

이를 통해 공격자가 갖고 있는 토큰을 무효화하여 사용자를 보호할 수 있다.

Spring Boot를 활용한 Refresh Token Rotation 구현

마지막으로 간단한 예제를 통해 어떤 방식으로 Refresh Tokne Rotation을 구현하는지 확인하자.

아래 예제들은 토큰에 이메일과 역할(role)을 담는다.

로그인 기능 구현

아래는 간단한 로그인 서비스 코드(signInUser)이다.

/**
 * save tokens on redis and return user auth response
 * @param email user's email
 * @param role user's role
* */
UserAuthResponseDto generateUserAuthResponseDto(String email, UserRole role){
    UserAuthInfoDto userAuthInfoDto = UserAuthInfoDto
            .builder()
            .email(email)
            .role(role)
            .build();
    String accessToken = jwtUtils.generateAccessToken(userAuthInfoDto);
    String refreshToken = jwtUtils.generateRefreshToken(userAuthInfoDto);
    authTokenService.saveAccessTokenAndRefreshToken(email, accessToken, refreshToken);
    return UserAuthResponseDto
            .builder()
            .accessToken(jwtUtils.generateAccessToken(userAuthInfoDto))
            .refreshToken(jwtUtils.generateRefreshToken(userAuthInfoDto))
            .email(email)
            .build();
}

/**
 * Check user's id and password and return auth info
 * @param userSignInDto UserSignInDto, not-null
 * @return UserAuthResponseDto - not null
 * @throws RuntimeException If user info is not valid
* */
public UserAuthResponseDto signInUser(UserSignInDto userSignInDto) {
    if(!verifyUserSignInInfo(userSignInDto)){
        throw new RuntimeException("invalid user info : email or password invalid");
    }
    return generateUserAuthResponseDto(userSignInDto.getEmail(), UserRole.ROLE_USER);
}

 

코드 자체보다는 이 코드의 동작 방식을 이해해보자.

  1. 사용자 email로 DB에 저장된 사용자 조회 (existUser)
  2. 사용자가 존재하고 해당 사용자의 비밀번호가 유효하다면 토큰 발급
  3. 발급한 토큰은 서버 저장소에 저장 ( saveAccessTokenAndRefreshToken )
  4. 토큰들을 포함한 사용자 정보 반환

토큰을 서버 저장소에 저장하는 이유는 두 가지이다.

  • Refresh Token을 저장하여, 현재 특정 사용자에게 유효한 Refresh Token 정보를 보관해둠
  • 특정 사용자에 대한 공격을 감지했을 때, 해당 사용자의 Refresh Token을 만료시킴

토큰 재발급 기능 구현

해당 코드에서 Refresh Token Rotation을 사용할 예정이다.

 

코드는 아래와 같다.

/**
 * save tokens on redis and return user auth response
 * @param email user's email
 * @param role user's role
* */
UserAuthResponseDto generateUserAuthResponseDto(String email, UserRole role){
    UserAuthInfoDto userAuthInfoDto = UserAuthInfoDto
            .builder()
            .email(email)
            .role(role)
            .build();
    String accessToken = jwtUtils.generateAccessToken(userAuthInfoDto);
    String refreshToken = jwtUtils.generateRefreshToken(userAuthInfoDto);
    authTokenService.saveAccessTokenAndRefreshToken(email, accessToken, refreshToken);
    return UserAuthResponseDto
            .builder()
            .accessToken(jwtUtils.generateAccessToken(userAuthInfoDto))
            .refreshToken(jwtUtils.generateRefreshToken(userAuthInfoDto))
            .email(email)
            .build();
}

/**
 * regenerate access token and refresh token
 * @param refreshToken - user's refresh token, String
 * @return UserAuthResponseDto - UserAuthResponseDto, not-null
 * @throws RuntimeException - if prev version token or invalid token is sent
* */
@Transactional
public UserAuthResponseDto doRefreshTokenRotation(String refreshToken){
    if(!jwtUtils.validateToken(refreshToken)){
        throw new RuntimeException("refresh token invalid : using invalid or expired token");
    }
    String email = jwtUtils.getUserEmail(refreshToken);
    String savedRefreshToken = authTokenService.getRefreshToken(email);
    if(!savedRefreshToken.equals(refreshToken)){
        authTokenService.deleteAccessTokenAndRefreshToken(email);
        throw new RuntimeException("refresh token invalid : using prev version token");
    }
    return generateUserAuthResponseDto(email, UserRole.ROLE_USER);
}

이 코드도 마찬가지로 이해보다는 동작 방식에 집중하자.

  1. refresh token이 유효한지 검증함.
  2. 유효한 refresh token에서 사용자 email을 추출함.
  3. email을 활용하여 서버에 저장된 refresh token을 조회함
  4. 만약 요청으로 받은 토큰과 저장되어 있던 토큰이 일치하지 않는다면, 토큰을 제거하여 만료시킴
  5. 만약 두 refresh token이 일치한다면, 새로운 토큰을 재발급함.

정리하자면, 공격을 감지하면 사용자의 refresh token을 제거한다.

 

마무리

 

기존 단일 JWT 토큰 인증 방식을 넘어 access token와 refresh token을 활용한 인증 및 인가에 대해서 알아보았다.

 

위 코드에서 구상해볼 수 있는 문제점은 refresh token이 만료되어도 access token은 사용할 수 있다는 것이다.

따라서 access token의 만료기간이 길수록 사용자는 계정 해킹에 취약해질 수밖에 없다.

 

이러한 문제 해결을 위한 방안은 아래와 같다.

  • Access Token의 만료기간을 짧게 설정( ~15분 )
  • 유효하지 않은 refresh token에 대한 access token이 있을 때, 해당 access token을 블랙리스트에 등록
    • 이 때는 redis와 같은 중앙 저장소를 사용하는 것이 좋음