개발/TIL

로그인 시 refresh token 을 사용하는 이유와 frontend - backend에서 주의할 점

ebang 2025. 2. 23. 23:31

로그인 관련하여 사용자 경험, 보안을 모두 고려해서 refresh token 을 설계하는 것은 매우 기본적이면서도 중요한 문제이다. 

이번에는 frontend- backend 각각에서 refresh token 처리를 위한 논리 중 기본적인 뼈대를 정리해보았다. 

각각의 구현 방법과 장/단점도 포함되어있다. 

 

  1. 사용자가 로그인하면
    • 서버는 Access TokenRefresh Token을 발급한다.
    • Access Token메모리(React의 State, Redux, Vuex 등)에 저장한다. 
    • Refresh Token은 보안을 위해 HttpOnly Secure Cookie에 저장하거나, 로컬 스토리지에 저장(이 방식은 위험한 편)한다.
  2. API 요청 시
    • 프론트엔드는 Access Token을 Authorization: Bearer {accessToken} 헤더에 추가해 API 요청을 보낸다. 
  3. Access Token이 만료되면
    • 서버에서 401 Unauthorized 응답을 반환한다. 
    • 프론트엔드는 401 응답을 감지하고, 저장된 Refresh Token을 사용해 새로운 Access Token을 요청한다. 
  4. Refresh Token을 이용해 Access Token 재발급 요청
    • 프론트엔드는 /auth/refresh 와 같은 엔드포인트로 요청을 보낸디. 
    • 이때 Refresh Token은:
      • HttpOnly Cookie에 저장된 경우 → 자동으로 요청에 포함됨
      • 로컬 스토리지에 저장된 경우 → 요청 헤더에 추가 (Authorization: Bearer {refreshToken})
  5. 서버의 응답 처리
    • 서버는 Refresh Token의 유효성을 검증한다. 
    • 유효하면 새로운 Access Token을 발급하여 응답한다. 
    • 프론트엔드는 이 Access Token을 다시 메모리(Redux 등)에 저장하여 이후 요청에 사용한다. 
  6. Refresh Token도 만료되었을 경우
    • 사용자는 다시 로그인해야 한다. 

 프론트엔드 코드 예시 (JavaScript/React)


// Axios 인스턴스 생성 (Access Token을 자동으로 추가)
import axios from 'axios';

// 기본 Axios 인스턴스
const api = axios.create({
  baseURL: '<http://localhost:8080/api>',
  withCredentials: true // HttpOnly Cookie 사용 시 필요
});

// Access Token을 요청 헤더에 추가
api.interceptors.request.use((config) => {
  const accessToken = localStorage.getItem('accessToken');
  if (accessToken) {
    config.headers.Authorization = `Bearer ${accessToken}`;
  }
  return config;
});

// 응답 인터셉터 (Access Token이 만료되면 Refresh Token으로 새로 요청)
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      try {
        const refreshResponse = await axios.post(
          '<http://localhost:8080/auth/refresh>',
          {},
          { withCredentials: true }
        );

        const newAccessToken = refreshResponse.data.accessToken;
        localStorage.setItem('accessToken', newAccessToken);

        // 원래 요청 재시도
        error.config.headers.Authorization = `Bearer ${newAccessToken}`;
        return api(error.config);
      } catch (refreshError) {
        // Refresh Token도 만료된 경우 로그아웃 처리
        console.error('Session expired, please login again');
        window.location.href = '/login';
      }
    }
    return Promise.reject(error);
  }
);

export default api;

 

 

일반적인 요청 흐름 -  backend

 

  • 로그인 시 Refresh Token 발급 및 저장
    • 사용자가 로그인하면 Access TokenRefresh Token을 발급한다. 
    • Access TokenJWT 형식으로 프론트엔드에 전달한다. 
    • Refresh Token서버에 저장하거나 HttpOnly Secure Cookie를 통해 클라이언트에 전달한다. 
  • Access Token이 만료되었을 때
    • 프론트엔드는 저장된 Refresh Token을 사용하여 새로운 Access Token을 요청한다. 
    • 백엔드는 저장된 Refresh Token의 유효성을 검증한 후 새 Access Token을 발급한다. 
  • Refresh Token 사용 후 처리
    • 보안을 위해 사용한 Refresh Token즉시 폐기하고 새로 발급한다. 이를 통해 도난된 Refresh Token이 재사용되는 것을 방지한다. 
  • 로그아웃 시 Refresh Token 제거
    • 사용자가 로그아웃하면 해당 사용자의 Refresh Token을 서버에서 삭제한다. 이를 통해 더 이상 해당 토큰으로 Access Token을 발급받을 수 없게 한다. 

 

 Refresh Token의 저장 방법 - backend

1.  Redis에 저장

  • 빠른 속도만료 시 자동 삭제 기능 제공
  • 대규모 시스템에서도 성능에 영향이 적음
  • TTL 설정으로 Refresh Token이 만료되면 자동 삭제

Redis에 저장하는 방식 예시

  • Key: refresh_token:{userId}
  • Value: Refresh Token 문자열
  • Expiration: Refresh Token의 만료 시간 (예: 7일)

 Redis에 Refresh Token 저장 예시 (Spring Boot 코드)


@Service
public class RefreshTokenService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private final long REFRESH_TOKEN_EXPIRATION = 7 * 24 * 60 * 60; // 7일 (단위: 초)

    // Refresh Token 저장
    public void saveRefreshToken(String userId, String refreshToken) {
        redisTemplate.opsForValue().set("refresh_token:" + userId, refreshToken, REFRESH_TOKEN_EXPIRATION, TimeUnit.SECONDS);
    }

    // Refresh Token 조회
    public String getRefreshToken(String userId) {
        return redisTemplate.opsForValue().get("refresh_token:" + userId);
    }

    // Refresh Token 삭제 (로그아웃 시 사용)
    public void deleteRefreshToken(String userId) {
        redisTemplate.delete("refresh_token:" + userId);
    }
}

 


Redis에 저장된 Refresh Token의 예시


Key: refresh_token:12345
Value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Expiration: 604800초 (7일)

 

2: 데이터베이스에 저장 (MySQL, PostgreSQL, MongoDB)

  • 데이터가 영구적으로 저장되므로 로그 및 감사에 적합
  • 사용자가 로그아웃하거나 토큰이 만료되면 수동으로 삭제 필요

Refresh Token 저장 및 검증 로직

@Service
public class RefreshTokenService {

    @Autowired
    private RefreshTokenRepository refreshTokenRepository;

    private final long REFRESH_TOKEN_EXPIRATION = 7 * 24 * 60 * 60 * 1000; // 7일 (단위: 밀리초)

    // Refresh Token 저장
    public void saveRefreshToken(String userId, String refreshToken) {
        RefreshToken token = new RefreshToken();
        token.setUserId(userId);
        token.setToken(refreshToken);
        token.setExpiryDate(Instant.now().plusMillis(REFRESH_TOKEN_EXPIRATION));
        refreshTokenRepository.save(token);
    }

    // Refresh Token 검증 및 조회
    public Optional<RefreshToken> validateRefreshToken(String refreshToken) {
        return refreshTokenRepository.findByToken(refreshToken)
                .filter(token -> !token.getExpiryDate().isBefore(Instant.now()));
    }

    // Refresh Token 삭제 (로그아웃 시 사용)
    public void deleteRefreshToken(String refreshToken) {
        refreshTokenRepository.deleteByToken(refreshToken);
    }
}

3: 클라이언트(HttpOnly Cookie)에만 저장 (서버에 저장하지 않음)

  • Stateless 방식으로 서버의 부하가 최소화됨
  • 서버가 상태를 유지할 필요 없음 (특히 마이크로서비스 환경에 유리함)
  • 단점:
    • Refresh Token탈취될 경우 이를 무효화할 방법이 없음.
    • 보안이 중요한 서비스에서는 권장되지 않음
  • Refresh TokenHttpOnly Secure Cookie프론트엔드에만 저장하는 방식으로 구현 가능. 
  • Access Token이 만료되면 프론트엔드가 Cookie에 저장된 Refresh Token을 사용해 Access Token을 재발급 요청

 

보안상 주의할 점

1. Refresh Token의 만료 기간 설정

  • 너무 긴 수명은 보안에 위험하므로 7일 ~ 30일 정도로 해야한다. 
  • 보안이 더 중요한 경우 단기 Refresh Token(예: 7일)과 장기 Refresh Token(예: 30일)을 조합하여 사용하기도 한다. 

2. Refresh Token 재발급 원칙 (One-Time Use)

  • Refresh Token은 사용 시마다 새로운 Refresh Token을 발급하여 기존 토큰을 즉시 폐기해야한다. 
  • 만약 유출되었을 경우 바로 사용될 가능성을 차단하게 됨. 

3. Refresh Token 사용 제한

  • 사용자의 IP 주소기기 정보를 기록해서, 인증 요청이 동일한 환경에서만 허용되도록 제한하는 것도 중요하다. 

4. Refresh Token 재사용 방지 (Replay Attack 방지)

  • Redis를 사용할 때 Refresh Token의 사용 여부를 표시한다. (flag 등)
  • 또는 한 번 사용된 Refresh Token은 즉시 삭제하여 재사용 공격을 방지한다. 

5. 로그아웃 시 Refresh Token 삭제

  • 기본 중의 기본.
  • 사용자가 로그아웃하면 서버에서 해당 사용자의 Refresh Token즉시 삭제한다. 
  • 프론트엔드에서도 HttpOnly Cookie를 제거하거나 로컬 스토리지에서 삭제한다.