Spring Boot

Spring Boot에서 Refresh Token을 사용해 JWT 재발급하기

Pro.Dev 2025. 1. 8. 15:00
반응형

Spring Boot에서 Refresh Token을 사용해 JWT 재발급하기

JWT(JSON Web Token)를 사용하는 애플리케이션에서는 액세스 토큰(Access Token)의 만료를 관리하기 위해 리프레시 토큰(Refresh Token)을 사용하는 방법이 일반적입니다. 리프레시 토큰을 통해 사용자는 재인증 없이 새로운 액세스 토큰을 발급받을 수 있습니다. 이 글에서는 Spring Boot에서 Refresh Token을 활용한 JWT 재발급 방법을 단계별로 설명합니다.


1. Refresh Token의 필요성

Access Token의 한계:

  • 액세스 토큰은 보안성을 높이기 위해 짧은 유효 기간을 설정합니다.
  • 토큰이 만료되면 사용자는 다시 로그인을 해야 합니다.

Refresh Token의 역할:

  • 긴 유효 기간을 가지며, 액세스 토큰이 만료되었을 때 새로운 액세스 토큰을 발급받는 데 사용됩니다.
  • 사용자가 로그아웃하거나 리프레시 토큰이 만료되지 않는 한 재로그인 없이 액세스 토큰을 갱신할 수 있습니다.

2. 프로젝트 설정

  1. 의존성 추가

pom.xml 또는 build.gradle에 JWT 관련 의존성을 추가합니다.

Maven:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

3. JwtUtil 클래스 확장

JWT 생성 및 검증 기능을 제공하는 JwtUtil 클래스에 Refresh Token 관련 기능을 추가합니다.

JwtUtil.java:

import io.jsonwebtoken.*;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.function.Function;

@Component
public class JwtUtil {

    private final String SECRET_KEY = "mysecretkey";
    private final long ACCESS_TOKEN_EXPIRATION = 1000 * 60 * 15; // 15분
    private final long REFRESH_TOKEN_EXPIRATION = 1000 * 60 * 60 * 24 * 7; // 7일

    public String generateAccessToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION))
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

    public String generateRefreshToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRATION))
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
    }

    public Boolean isTokenExpired(String token) {
        return extractClaim(token, Claims::getExpiration).before(new Date());
    }

    public Boolean validateToken(String token, String username) {
        return extractUsername(token).equals(username) && !isTokenExpired(token);
    }
}

4. Refresh Token 저장 및 관리

리프레시 토큰은 보안 강화를 위해 서버에 저장하는 것이 좋습니다. 데이터베이스나 캐시를 이용해 관리할 수 있습니다.

예제 엔티티:

import jakarta.persistence.*;

@Entity
public class RefreshToken {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String token;

    @Column(nullable = false)
    private String username;

    @Column(nullable = false)
    private Date expiryDate;

    // Getter와 Setter
}

리포지토리:

import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
    Optional<RefreshToken> findByToken(String token);
    void deleteByUsername(String username);
}

5. Refresh Token을 사용한 JWT 재발급 컨트롤러

AuthController.java:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Date;
import java.util.Optional;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private RefreshTokenRepository refreshTokenRepository;

    @PostMapping("/refresh")
    public ResponseEntity<?> refreshAccessToken(@RequestBody TokenRequest tokenRequest) {
        Optional<RefreshToken> optionalRefreshToken = refreshTokenRepository.findByToken(tokenRequest.getRefreshToken());

        if (optionalRefreshToken.isEmpty() || optionalRefreshToken.get().getExpiryDate().before(new Date())) {
            return ResponseEntity.status(403).body("Invalid or expired refresh token");
        }

        String username = jwtUtil.extractUsername(tokenRequest.getRefreshToken());
        String newAccessToken = jwtUtil.generateAccessToken(username);

        return ResponseEntity.ok(new TokenResponse(newAccessToken));
    }
}

class TokenRequest {
    private String refreshToken;
    // Getter와 Setter
}

class TokenResponse {
    private String accessToken;

    public TokenResponse(String accessToken) {
        this.accessToken = accessToken;
    }

    // Getter
}

6. 테스트 및 실행

  1. 로그인 후 토큰 발급:

    • 액세스 토큰과 리프레시 토큰을 반환합니다.
  2. 액세스 토큰 만료 후 리프레시 요청:

    • POST /api/auth/refresh
    • Body:
      {
          "refreshToken": "<Refresh Token>"
      }
    • 응답:
      {
          "accessToken": "<New Access Token>"
      }

반응형