Pink Spider/jwt 관련 Tip

Created Thu, 27 Mar 2025 11:36:45 +0900 Modified Mon, 08 Dec 2025 08:41:47 +0900
2341 Words 11 min

jwt 관련 Tip

Q. access token 이 만료되면, refresh token을 통해 재발행 한다. refresh token은 만료 기간이 없을까?

A. Refresh Token반드시 만료 기간이 있어야 합니다.

  • 일반적으로는 Access Token보다 훨씬 긴 만료 기간을 설정할 뿐이에요.

1. 일반적인 만료 시간 설정

  • Access Token: 보통 15분 ~ 1시간
  • Refresh Token: 보통 7일 ~ 30일 또는 90일

2. 왜 Refresh Token도 만료 기간이 필요한가?

  • Refresh Token이 유출되었을 경우, 무제한으로 Access Token을 재발급받을 수 있다면 보안에 치명적입니다.
  • 일정 기간이 지나면 로그인 세션을 재확인하거나 재로그인을 유도하는 것이 보안상 더 안전합니다.

3. 저장 위치는?

  • Refresh Token은 보통 서버 저장 방식 (e.g., Redis)에 저장하고, 사용자에게는 식별자만 전달하거나 클라이언트에 저장하되 보안적으로 안전한 저장소 (e.g., Secure Cookie, LocalStorage X) 를 사용하는 것이 권장됩니다.

Q. Spring Boot에서의 예시

A. Spring Boot에서 JWT 기반으로 Access Token과 Refresh Token을 발급 및 재발급하는 기본적인 구조를 예시


1. 토큰 생성 클래스 (JwtProvider)

@Component
public class JwtProvider {

    private final String secretKey = "yourSecretKey"; // 적절히 암호화 필요
    private final long accessTokenValidity = 1000L * 60 * 15;  // 15분
    private final long refreshTokenValidity = 1000L * 60 * 60 * 24 * 14; // 14일

    public String createAccessToken(String userId) {
        return createToken(userId, accessTokenValidity);
    }

    public String createRefreshToken(String userId) {
        return createToken(userId, refreshTokenValidity);
    }

    private String createToken(String userId, long validity) {
        Claims claims = Jwts.claims().setSubject(userId);
        Date now = new Date();
        Date expiry = new Date(now.getTime() + validity);

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(expiry)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    public String getUserIdFromToken(String token) {
        return Jwts.parser().setSigningKey(secretKey)
                .parseClaimsJws(token).getBody().getSubject();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return true;
        } catch (ExpiredJwtException e) {
            return false;
        }
    }
}

2. 로그인 시 토큰 발급

@PostMapping("/login")
public ResponseEntity<TokenResponse> login(@RequestBody LoginRequest request) {
    // 인증 처리 (생략)
    String userId = "user-id";

    String accessToken = jwtProvider.createAccessToken(userId);
    String refreshToken = jwtProvider.createRefreshToken(userId);

    // Refresh Token을 DB나 Redis에 저장
    refreshTokenService.save(userId, refreshToken);

    return ResponseEntity.ok(new TokenResponse(accessToken, refreshToken));
}

3. Access Token 만료 시 Refresh Token으로 재발급

@PostMapping("/token/refresh")
public ResponseEntity<TokenResponse> refreshToken(@RequestBody TokenRequest request) {
    String refreshToken = request.getRefreshToken();

    if (!jwtProvider.validateToken(refreshToken)) {
        throw new RuntimeException("Invalid refresh token");
    }

    String userId = jwtProvider.getUserIdFromToken(refreshToken);

    // 저장된 refresh token과 비교
    String stored = refreshTokenService.get(userId);
    if (!stored.equals(refreshToken)) {
        throw new RuntimeException("Token mismatch");
    }

    String newAccessToken = jwtProvider.createAccessToken(userId);
    return ResponseEntity.ok(new TokenResponse(newAccessToken, refreshToken));
}

4. Refresh Token 저장 예시 (Redis 기반)

@Service
public class RefreshTokenService {

    private final RedisTemplate<String, String> redisTemplate;

    public RefreshTokenService(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void save(String userId, String token) {
        redisTemplate.opsForValue().set(userId, token, 14, TimeUnit.DAYS); // 유효 기간 설정
    }

    public String get(String userId) {
        return redisTemplate.opsForValue().get(userId);
    }
}

Q. 에시에서 RefreshToken 에서 refresh save전에 기존꺼는 안지워도 될까?

A. 일반적으로는 기존 Refresh Token을 삭제하거나 덮어써야 합니다.

왜냐면:

  • 하나의 유저당 하나의 Refresh Token만 유효하도록 관리하는 게 일반적인 보안 패턴이에요.
  • 그렇지 않으면 이전에 발급된 토큰으로도 계속 Access Token을 재발급받을 수 있어, 탈취된 토큰을 막기 어려워짐.

✅ 덮어쓰는 방식이면 OK

Redis에서 SET 명령은 기본적으로 같은 key면 덮어씁니다.

redisTemplate.opsForValue().set(userId, token, 14, TimeUnit.DAYS);

위 코드는 기존 토큰을 자동으로 덮어쓰기 때문에 별도 삭제는 필요 없습니다.


✅ 만약 여러 Refresh Token을 허용하고 싶다면?

예: 여러 디바이스 로그인 허용 시

  • userId:deviceId 형태로 Redis 키를 저장하거나
  • Refresh Token 자체를 key로 저장하고, userId를 값으로 두는 구조 등으로 토큰 여러 개를 관리할 수 있어요.

요약:

  • 유저당 1개의 토큰 정책이라면, 현재 방식처럼 set()만으로도 충분합니다.
  • 보안 강화가 목적이라면 토큰 변경 시마다 재발급 및 교체하는 방식이 맞습니다.

Q. refresh token이 만료되면 갱신은 어떻게 하나요?

A. Refresh Token이 만료된 경우에는 자동 갱신할 수 없고, 사용자는 재로그인 해야 합니다.

즉, 유저 인증을 다시 받아야 새로운 Access/Refresh Token을 발급할 수 있어요.


🔁 갱신 흐름 정리

상황 처리 방식
✅ Access Token 만료 Refresh Token으로 Access Token 재발급
❌ Refresh Token 만료 사용자가 재로그인해야 함

왜 자동 갱신이 안 될까?

Refresh Token은 이미 **“로그인 상태를 유지하는 유일한 증거”**예요.
이게 만료됐다는 건 서버 입장에선 “이 사용자가 여전히 로그인 상태라고 신뢰할 수 없다"는 뜻이죠.
→ 그래서 반드시 사용자의 **다시 인증 (ex. 아이디/비밀번호 로그인, OAuth 등)**이 필요해요.


💡 실무에서의 UX 처리

  • 클라이언트가 토큰 만료 에러 받으면:
    • Access Token 만료 → 자동 재발급 시도
    • Refresh Token 만료 → 사용자에게 “세션이 만료되었습니다. 다시 로그인 해주세요” 메시지
HTTP/1.1 401 Unauthorized
{
  "code": "REFRESH_TOKEN_EXPIRED",
  "message": "세션이 만료되었습니다. 다시 로그인 해주세요."
}

추가 팁

  • 프론트에서 accessTokenrefreshToken의 만료 시간을 관리해두면 사용자 경험 개선 가능
  • 백엔드에서 Refresh Token 만료 시 로그아웃 처리를 해두는 것도 깔끔한 방식이에요 (e.g., Redis에서 토큰 삭제)

Q. 재로그인 안하고, 로그인을 유지할 방법은 없을까요?

A. “Refresh Token까지 만료됐는데 재로그인 없이 로그인 상태 유지“를 원한다면, 일반적인 보안 정책에서는 권장되지 않지만, 일부 대안적인 방법이 있습니다. 각각의 보안 트레이드오프를 이해하고 선택하는 게 중요해요.


✅ 1. Refresh Token Rotation + Sliding Expiration (슬라이딩 만료)

만료 시간을 매번 연장하는 방식

🔄 어떻게 작동하나요?

  • 사용자가 Refresh Token으로 Access Token을 재발급할 때마다,
    새로운 Refresh Token도 함께 재발급해서 유효 기간을 갱신해줍니다.
  • 이걸 계속 반복하면, 사용자는 로그인 유지가 가능하죠.

✅ 장점

  • 사용자가 장시간 활동하면 로그인 유지 가능
  • UX가 훨씬 부드러움 (카카오/구글 등도 유사 전략 사용)

❌ 단점

  • Refresh Token 탈취 시 위험 (회전 로직을 모르면 계속 사용 가능)
  • 서버 상태 저장 필요 (Redis 등으로 마지막 토큰 상태 관리해야 함)

✅ 2. Long-lived Refresh Token

Refresh Token을 매우 길게 설정 (예: 180일)

  • 토큰 자체는 그대로지만, 서버에서 마지막 사용 시간을 기준으로 만료 처리
  • 또는 그냥 클라이언트에 오래 살아있는 refresh token을 두는 전략

✅ 장점

  • 사용자 재로그인 필요 없음

❌ 단점

  • 유출되면 오랫동안 사용 가능
  • 모바일 앱에만 제한적으로 사용 (보안 컨트롤 쉬움)

✅ 3. Silent Re-authentication (조용한 재인증)

OAuth2 기반에서는 Refresh Token이 만료되더라도,
사용자가 여전히 세션에 로그인되어 있다면,
백그라운드에서 재로그인 요청이 가능함

  • 예: Google OAuth → prompt=none 파라미터로 로그인 페이지 없이 다시 인증 가능
  • 조건: 사용자가 아직 Google에 로그인 상태여야 함

💡 현실적인 전략

사용자 환경 추천 전략
모바일 앱 긴 Refresh Token + Secure Storage + Rotation
웹 (보안 중요) Rotation + Redis 관리 + 주기적 재로그인 유도
OAuth 로그인 (Google 등) Silent Re-authentication

마무리 요약

Refresh Token 없이 로그인 유지하는 건 보안 리스크가 크지만,
“갱신 전략"이나 “OAuth 세션 기반 재인증” 등을 잘 활용하면,
UX와 보안 사이 균형을 잡을 수 있어요.