Security · 2025-07-28 · 1분 읽기

JWT 인증 구현과 refresh token 전략

목차
  • 1. JWT 구조
  • 2. Access Token + Refresh Token 전략
  • 3. Spring Security 구현
  • 4. Refresh Token 재발급 API
  • 5. Refresh Token은 httpOnly 쿠키로

1. JWT 구조

Header.Payload.Signature

eyJhbGciOiJIUzI1NiJ9          // Header: 알고리즘
.eyJzdWIiOiIxMjMiLCJleHAiOjE3MDAwMDAwfQ  // Payload: claims
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c  // Signature

중요: Payload는 Base64 인코딩이지 암호화가 아니다. 민감 정보를 넣으면 안 된다.

2. Access Token + Refresh Token 전략

Access Token만 사용하면:

  • 만료 시간을 짧게 설정 → 자주 재로그인
  • 만료 시간을 길게 설정 → 탈취 시 장기간 악용

해결책: 짧은 Access Token + 긴 Refresh Token

Access Token: 15분
Refresh Token: 7일 (DB에 저장)

3. Spring Security 구현

@Component
public class JwtProvider {
    @Value("${jwt.secret}")
    private String secret;
 
    public String createAccessToken(Long userId, String role) {
        return Jwts.builder()
            .subject(userId.toString())
            .claim("role", role)
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + 15 * 60 * 1000))
            .signWith(getKey())
            .compact();
    }
 
    public String createRefreshToken(Long userId) {
        String token = Jwts.builder()
            .subject(userId.toString())
            .expiration(new Date(System.currentTimeMillis() + 7L * 24 * 60 * 60 * 1000))
            .signWith(getKey())
            .compact();
 
        refreshTokenRepository.save(new RefreshToken(userId, token));
        return token;
    }
 
    public Claims parse(String token) {
        return Jwts.parser()
            .verifyWith(getKey())
            .build()
            .parseSignedClaims(token)
            .getPayload();
    }
 
    private SecretKey getKey() {
        return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
    }
}

4. Refresh Token 재발급 API

@PostMapping("/auth/refresh")
public ResponseEntity<TokenResponse> refresh(
    @CookieValue("refreshToken") String refreshToken
) {
    Claims claims = jwtProvider.parse(refreshToken);
    Long userId = Long.parseLong(claims.getSubject());
 
    // DB에 저장된 토큰과 비교
    RefreshToken stored = refreshTokenRepository.findByUserId(userId)
        .orElseThrow(() -> new UnauthorizedException("로그인이 필요합니다."));
 
    if (!stored.getToken().equals(refreshToken)) {
        // 탈취 감지: 모든 토큰 무효화
        refreshTokenRepository.deleteByUserId(userId);
        throw new UnauthorizedException("비정상적인 접근이 감지되었습니다.");
    }
 
    String newAccessToken = jwtProvider.createAccessToken(userId, stored.getRole());
    String newRefreshToken = jwtProvider.createRefreshToken(userId);
 
    return ResponseEntity.ok()
        .header(HttpHeaders.SET_COOKIE, createRefreshTokenCookie(newRefreshToken))
        .body(new TokenResponse(newAccessToken));
}

5. Refresh Token은 httpOnly 쿠키로

private String createRefreshTokenCookie(String token) {
    return ResponseCookie.from("refreshToken", token)
        .httpOnly(true)   // JS 접근 불가
        .secure(true)     // HTTPS만
        .sameSite("Strict")
        .maxAge(Duration.ofDays(7))
        .path("/auth/refresh")
        .build()
        .toString();
}

XSS로 탈취 불가, CSRF는 SameSite로 방어.

'Security ' 카테고리의 다른 글

  • 이 카테고리에 다른 글이 없습니다.