Spring Security 7.0 + jjwt 0.12.6 JWT ์ธ์ฆ Spring Boot 4.0 · Spring Security 7.0 ํ๊ฒฝ์์ jjwt 0.12.x๋ก JWT ์ธ์ฆ์ ์ฒ์๋ถํฐ ๊ตฌํํ๋ ์ ์ฒด ๊ณผ์ ์ ๊ธฐ๋กํ๋ค.
๋ค์ด๊ฐ๋ฉฐ
Spring Boot 4.0.3 ๊ธฐ๋ฐ ํ๋ก์ ํธ๋ฅผ ์ธํ
ํ๋ฉด์ JWT ์ธ์ฆ ๊ตฌ์กฐ๋ฅผ ์ฒ์๋ถํฐ ์ก์๋ค.
์์ ์ ์ฐ๋ jjwt 0.11.x ์ฝ๋๋ฅผ ๊ทธ๋๋ก ๊ฐ์ ธ์๋ค๊ฐ ์ปดํ์ผ ์๋ฌ๋ฅผ ๊ฝค ๋ง๋ฌ๋๋ฐ, 0.12.x์์ API๊ฐ ์๋นํ ์ ๋ฆฌ๋๊ธฐ ๋๋ฌธ์ด๋ค.
Spring Security 7.0๋ WebSecurityConfigurerAdapter๊ฐ ์์ ํ ์ ๊ฑฐ๋๊ณ , Jackson 3.0 ์ ํ์ ๋ฐ๋ฅธ ๋ณํ๋ ์์ด์ ๊ตฌ๋ฒ์ ์์ ๋ฅผ ๊ทธ๋๋ก ์ฐ๋ฉด ์ ๋๋ ๋ถ๋ถ์ด ๋ง์๋ค.
์ด ๊ธ์ JWT ์ธ์ฆ ์ ์ฒด ํ๋ฆ์ ๊ตฌํํ ๊ณผ์ ์ ๊ธฐ๋กํ๋ค.
JWT ๊ตฌ์กฐ ๋ณต์ต
JWT(JSON Web Token)๋ RFC 7519๋ก ์ ์๋ ํ ํฐ ํ์์ด๋ค. Header.Payload.Signature ์ธ ํํธ๋ฅผ ์ (.)์ผ๋ก ์ด์ด ๋ถ์ธ Base64URL ์ธ์ฝ๋ฉ ๋ฌธ์์ด๋ก ๊ตฌ์ฑ๋๋ค.
eyJhbGciOiJIUzI1NiJ9
.eyJzdWIiOiJ1c2VyQGV4YW1wbGUuY29tIiwicm9sZSI6IlVTRVIiLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMH0
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
| ํํธ | ๋ด์ฉ |
| Header | ์๋ช ์๊ณ ๋ฆฌ์ฆ (alg: HS256), ํ ํฐ ํ์ (typ: JWT) |
| Payload | ํด๋ ์(Claims) — sub, iat, exp ๋ฑ ํ์ค ํด๋ ์ + ์ปค์คํ
ํด๋ ์ |
| Signature | HMAC-SHA256(base64url(header) + "." + base64url(payload), secretKey) |
Payload๋ Base64URL๋ก ์ธ์ฝ๋ฉ๋ ๊ฒ์ด์ง ์ํธํ๋ ๊ฒ ์๋๋ค.
๋๊ตฌ๋ ๋์ฝ๋ฉํด์ ๋ด์ฉ์ ๋ณผ ์ ์์ผ๋ฏ๋ก, ๋น๋ฐ๋ฒํธ๋ ๋ฏผ๊ฐํ ๊ฐ์ธ์ ๋ณด๋ ์ ๋ ๋ฃ์ผ๋ฉด ์ ๋๋ค.
Signature๊ฐ ํ๋ ์ญํ ์ "์ด ํ ํฐ์ด ์ฐ๋ฆฌ ์๋ฒ์์ ๋ฐ๊ธ๋๊ณ ๋ณ์กฐ๋์ง ์์๋ค"๋ ๊ฒ์ ๋ณด์ฅํ๋ ๊ฒ์ด๋ค.
Access Token๊ณผ Refresh Token ์ ๋ต
Access Token๋ง ๋จ๋ ์ผ๋ก ์ฐ๋ฉด ๋ง๋ฃ ์๊ฐ์ ๊ธธ๊ฒ ์ก์์๋ก ํ์ทจ ์ํ์ด ์ปค์ง๊ณ , ์งง๊ฒ ์ก์ผ๋ฉด ์ฌ์ฉ์๊ฐ ์์ฃผ ์ฌ๋ก๊ทธ์ธํด์ผ ํ๋ UX ๋ฌธ์ ๊ฐ ์๊ธด๋ค. ๊ทธ๋์ ์ผ๋ฐ์ ์ผ๋ก ๋ ์ข ๋ฅ์ ํ ํฐ์ ํจ๊ป ์ด์ฉํ๋ค.
| ๊ตฌ๋ถ | Access Token | Refresh Token |
| ์ฉ๋ | API ์์ฒญ ์ธ์ฆ | Access Token ์ฌ๋ฐ๊ธ |
| ๋ง๋ฃ ์๊ฐ | ์งง๊ฒ (1์๊ฐ ๋ด์ธ) | ๊ธธ๊ฒ (7์ผ ~ 30์ผ) |
| ์ ์ฅ ์์น | ๋ฉ๋ชจ๋ฆฌ ๋๋ ํด๋ผ์ด์ธํธ | DB ๋๋ Redis |
| ํ์ทจ ์ ๋์ | ๋ง๋ฃ๊น์ง ๋๊ธฐ | ์๋ฒ์์ ์ฆ์ ํ๊ธฐ ๊ฐ๋ฅ |
Refresh Token์ ์๋ฒ DB์ ์ ์ฅํด๋๊ณ , ์ฌ๋ฐ๊ธ ์์ฒญ์ด ๋ค์ด์ค๋ฉด DB์ ์ ์ฅ๋ ๊ฐ๊ณผ ๋น๊ต ๊ฒ์ฆํ๋ค.
์ฌ๊ธฐ์ Refresh Token Rotation ์ ๋ต์ ๋ํ๋ฉด ๋ณด์์ด ๊ฐํ๋๋ค.
์ฌ๋ฐ๊ธํ ๋๋ง๋ค ๊ธฐ์กด Refresh Token์ ํ๊ธฐํ๊ณ ์ ํ ํฐ์ ๋ฐ๊ธํ๋ ๋ฐฉ์์ธ๋ฐ,
ํ์ทจ๋ Refresh Token์ด ์ฌ์ฉ๋๋ ์๊ฐ ์ ์ ์ฌ์ฉ์์ ํ ํฐ๋ ๋ฌดํจํ๋์ด ์ด์ ์งํ๋ฅผ ๊ฐ์งํ ์ ์๋ค.
์์กด์ฑ ์ถ๊ฐ
jjwt๋ 0.12.x๋ถํฐ API / ๊ตฌํ์ฒด / JSON ํ์๋ฅผ ๋ถ๋ฆฌํด์ ์ ์ธํด์ผ ํ๋ค. jjwt-impl์ ๋ฐ๋์ runtimeOnly๋ก ์ ์ธํด์ผ ํ๋๋ฐ, ๋ด๋ถ ๊ตฌํ์ฒด์ ์ปดํ์ผ ํ์ ์์กด์ฑ์ด ์๊ธฐ๋ฉด ๋ฒ์ ์
๊ทธ๋ ์ด๋ ์ ํธํ์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
// build.gradle.kts
dependencies {
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
}
Spring Boot 4.0์ Jackson 3.0์ ๊ธฐ๋ณธ์ผ๋ก ์ฌ์ฉํ์ง๋ง, jjwt-jackson์ Jackson 2 ๊ธฐ๋ฐ์ด๋ค.
์ด ํ๋ก์ ํธ์์๋ jjwt-jackson์ ๊ทธ๋๋ก ์ฌ์ฉํ๊ณ Jackson 2 ํธํ ์์กด์ฑ์ ์ ์งํ๋ค.
Jackson 3๋ง ์ฐ๊ณ ์ถ๋ค๋ฉด jjwt-gson์ผ๋ก ๋์ฒดํ๊ฑฐ๋ ์ปค์คํ
JSON ํ์๋ฅผ ๋ฑ๋กํ๋ฉด ๋๋ค.
JwtProvider ๊ตฌํ
jjwt 0.12.x์์ ๋ฌ๋ผ์ง ํต์ฌ API๋ฅผ ์ ๋ฆฌํ๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.
| 0.11.x ๋ฒ์ | 0.12.x ๋ฒ์ |
Keys.secretKeyFor(SignatureAlgorithm.HS256) |
Jwts.SIG.HS256.key().build() ๋๋ Keys.hmacShaKeyFor(bytes) |
.signWith(key, SignatureAlgorithm.HS256) |
.signWith(key) (์๊ณ ๋ฆฌ์ฆ ์๋ ์ถ๋ก ) |
.parseClaimsJws(token) |
.parseSignedClaims(token) |
.setSubject(), .setExpiration() |
.subject(), .expiration() |
// src/main/java/com/example/security/jwt/JwtProvider.java
package com.example.security.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Component
public class JwtProvider {
private final SecretKey secretKey;
private final long accessTokenExpiry;
private final long refreshTokenExpiry;
public JwtProvider(
@Value("${jwt.secret}") String secret,
@Value("${jwt.access-token-expiry:3600000}") long accessTokenExpiry,
@Value("${jwt.refresh-token-expiry:604800000}") long refreshTokenExpiry
) {
// HS256 ๊ธฐ์ค ์ต์ 256๋นํธ(32๋ฐ์ดํธ) ์ด์์ ํค๊ฐ ํ์ํ๋ค
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
this.accessTokenExpiry = accessTokenExpiry;
this.refreshTokenExpiry = refreshTokenExpiry;
}
public String generateAccessToken(String email, String role) {
return Jwts.builder()
.subject(email)
.claim("role", role)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + accessTokenExpiry))
.signWith(secretKey) // 0.12.x: ์๊ณ ๋ฆฌ์ฆ ์๋ ์ถ๋ก , ๋ณ๋ ๋ช
์ ๋ถํ์
.compact();
}
public String generateRefreshToken(String email) {
return Jwts.builder()
.subject(email)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + refreshTokenExpiry))
.signWith(secretKey)
.compact();
}
public Claims parseToken(String token) {
// 0.12.x: parseClaimsJws() ๋์ parseSignedClaims() ์ฌ์ฉ
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
}
public String extractEmail(String token) {
return parseToken(token).getSubject();
}
public boolean isTokenValid(String token) {
try {
parseToken(token);
return true;
} catch (ExpiredJwtException e) {
// ๋ง๋ฃ๋ ํ ํฐ — ์ฌ๋ฐ๊ธ ํ๋ฆ์ผ๋ก ์ ๋
return false;
} catch (JwtException | IllegalArgumentException e) {
// ์๋ช
๋ถ์ผ์น, ํ์ ์ค๋ฅ ๋ฑ
return false;
}
}
}
jwt.secret์ application.yml์์ ํ๊ฒฝ ๋ณ์๋ก ์ฃผ์
๋ฐ๋๋ค.
์ด์ ํ๊ฒฝ์์๋ ์ ๋ ์์ค ์ฝ๋์ ํ๋์ฝ๋ฉํ์ง ์๋๋ค.
# application.yml
jwt:
secret: ${JWT_SECRET} # ์ต์ 32๋ฐ์ดํธ ์ด์์ ๋๋ค ๋ฌธ์์ด
access-token-expiry: 3600000 # 1์๊ฐ (ms)
refresh-token-expiry: 604800000 # 7์ผ (ms)
JwtAuthenticationFilter ๊ตฌํ
Spring Security ํํฐ ์ฒด์ธ์ JWT ๊ฒ์ฆ ๋ก์ง์ ๋ผ์ ๋ฃ๋๋ค. OncePerRequestFilter๋ฅผ ์์ํ๋ฉด ๊ฐ์ ์์ฒญ์์ ํํฐ๊ฐ ์ค๋ณต ์คํ๋๋ ๊ฒ์ ๋ง์์ค๋ค.
Authorization ํค๋์์ Bearer ์ ๋์ฌ๋ฅผ ์ ๊ฑฐํ๊ณ ํ ํฐ์ ์ถ์ถํ ๋ค, ์ ํจํ๋ฉด SecurityContextHolder์ ์ธ์ฆ ์ ๋ณด๋ฅผ ์ค์ ํ๋ค.
// src/main/java/com/example/security/jwt/JwtAuthenticationFilter.java
package com.example.security.jwt;
import com.example.domain.user.UserRepository;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;
private final UserRepository userRepository;
public JwtAuthenticationFilter(JwtProvider jwtProvider, UserRepository userRepository) {
this.jwtProvider = jwtProvider;
this.userRepository = userRepository;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String token = extractToken(request);
if (StringUtils.hasText(token) && jwtProvider.isTokenValid(token)) {
String email = jwtProvider.extractEmail(token);
userRepository.findByEmail(email).ifPresent(user -> {
var authToken = new UsernamePasswordAuthenticationToken(
user, null, user.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
});
}
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
SecurityConfig ์ค์
Spring Security 7.0์์๋ WebSecurityConfigurerAdapter๊ฐ ์์ ํ ์ ๊ฑฐ๋๋ค. SecurityFilterChain ๋น์ ์ง์ ๋ฑ๋กํ๋ ๋ฐฉ์๋ง ์ง์ํ๋ฉฐ, JWT ๊ธฐ๋ฐ Stateless ์ธ์ฆ์ด๋ฏ๋ก ์ธ์
์์ฑ ์ ์ฑ
์ STATELESS๋ก ์ค์ ํ๊ณ CSRF๋ ๋นํ์ฑํํ๋ค.
// src/main/java/com/example/security/SecurityConfig.java
package com.example.security;
import com.example.security.jwt.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final UserDetailsService userDetailsService;
public SecurityConfig(
JwtAuthenticationFilter jwtAuthenticationFilter,
UserDetailsService userDetailsService
) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
this.userDetailsService = userDetailsService;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.authenticationProvider(authenticationProvider())
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
)
.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
var provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config
) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
์ธ์ฆ API ๊ตฌํ
๋ก๊ทธ์ธ / ์ฌ๋ฐ๊ธ / ๋ก๊ทธ์์
DTO๋ Java 16+์ Record๋ก ๊ฐ๊ฒฐํ๊ฒ ์ ์ํ๋ค.
public record LoginRequest(String email, String password) {}
public record RefreshRequest(String refreshToken) {}
public record TokenResponse(String accessToken, String refreshToken) {}
// src/main/java/com/example/api/auth/AuthController.java
package com.example.api.auth;
import com.example.security.jwt.JwtProvider;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtProvider jwtProvider;
private final RefreshTokenService refreshTokenService;
public AuthController(
AuthenticationManager authenticationManager,
JwtProvider jwtProvider,
RefreshTokenService refreshTokenService
) {
this.authenticationManager = authenticationManager;
this.jwtProvider = jwtProvider;
this.refreshTokenService = refreshTokenService;
}
@PostMapping("/login")
public ResponseEntity<TokenResponse> login(@RequestBody LoginRequest request) {
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.email(), request.password()
)
);
var user = (com.example.domain.user.User) auth.getPrincipal();
String accessToken = jwtProvider.generateAccessToken(user.getEmail(), user.getRole().name());
String refreshToken = jwtProvider.generateRefreshToken(user.getEmail());
refreshTokenService.save(user.getEmail(), refreshToken, user.getRole().name());
return ResponseEntity.ok(new TokenResponse(accessToken, refreshToken));
}
@PostMapping("/refresh")
public ResponseEntity<TokenResponse> refresh(@RequestBody RefreshRequest request) {
String oldRefreshToken = request.refreshToken();
if (!jwtProvider.isTokenValid(oldRefreshToken)) {
return ResponseEntity.status(401).build();
}
String email = jwtProvider.extractEmail(oldRefreshToken);
// DB์ ์ ์ฅ๋ ํ ํฐ๊ณผ ๋น๊ต — ๋ถ์ผ์น ์ ์์ธ ๋ฐ์
refreshTokenService.validate(email, oldRefreshToken);
// Rotation: ๊ธฐ์กด ํ ํฐ ํ๊ธฐ ํ ์ ํ ํฐ ๋ฐ๊ธ
String newRefreshToken = jwtProvider.generateRefreshToken(email);
String role = refreshTokenService.getRole(email);
refreshTokenService.rotate(email, newRefreshToken);
String newAccessToken = jwtProvider.generateAccessToken(email, role);
return ResponseEntity.ok(new TokenResponse(newAccessToken, newRefreshToken));
}
@PostMapping("/logout")
public ResponseEntity<Void> logout(@RequestBody RefreshRequest request) {
String email = jwtProvider.extractEmail(request.refreshToken());
refreshTokenService.delete(email);
return ResponseEntity.noContent().build();
}
}
RefreshTokenService
// src/main/java/com/example/api/auth/RefreshTokenService.java
package com.example.api.auth;
import com.example.domain.token.RefreshToken;
import com.example.domain.token.RefreshTokenRepository;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class RefreshTokenService {
private final RefreshTokenRepository repository;
public RefreshTokenService(RefreshTokenRepository repository) {
this.repository = repository;
}
@Transactional
public void save(String email, String token, String role) {
repository.findById(email)
.ifPresentOrElse(
rt -> rt.rotate(token),
() -> repository.save(new RefreshToken(email, token, role))
);
}
public void validate(String email, String token) {
RefreshToken stored = repository.findById(email)
.orElseThrow(() -> new InvalidTokenException("Refresh token not found"));
if (!stored.getToken().equals(token)) {
// ํ ํฐ ๋ถ์ผ์น — ํ์ทจ ๊ฐ๋ฅ์ฑ, ์ ์ฅ๋ ํ ํฐ ์ ์ฒด ํ๊ธฐ
repository.deleteById(email);
throw new InvalidTokenException("Refresh token mismatch. Possible token theft.");
}
}
@Transactional
public void rotate(String email, String newToken) {
repository.findById(email).ifPresent(rt -> rt.rotate(newToken));
}
public String getRole(String email) {
return repository.findById(email)
.map(RefreshToken::getRole)
.orElseThrow(() -> new InvalidTokenException("Refresh token not found"));
}
@Transactional
public void delete(String email) {
repository.deleteById(email);
}
}
์ ์ฒด ์ธ์ฆ ํ๋ฆ
๊ตฌํํ JWT ์ธ์ฆ ํ๋ฆ์ ์ํ์ค๋ก ์ ๋ฆฌํ๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.
ํด๋ผ์ด์ธํธ ์๋ฒ
โ โ
โโโ POST /api/auth/login โโโโโโโโโโ>โ
โ { email, password } โ
โ โโโ AuthenticationManager.authenticate()
โ โโโ BCrypt ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ
โ โโโ Access Token ์์ฑ (1h)
โ โโโ Refresh Token ์์ฑ (7d) → DB ์ ์ฅ
โ<โโ { accessToken, refreshToken } โโโ
โ โ
โโโ GET /api/resource โโโโโโโโโโโโโ>โ
โ Authorization: Bearer {AT} โ
โ โโโ JwtAuthenticationFilter
โ โโโ ์๋ช
๊ฒ์ฆ + ๋ง๋ฃ ํ์ธ
โ โโโ SecurityContextHolder ์ค์
โ<โโ 200 OK โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ (Access Token ๋ง๋ฃ ํ) โ
โโโ POST /api/auth/refresh โโโโโโโโโ>โ
โ { refreshToken } โ
โ โโโ DB ์ ์ฅ ํ ํฐ๊ณผ ๋น๊ต ๊ฒ์ฆ
โ โโโ ๊ธฐ์กด RT ํ๊ธฐ + ์ RT ๋ฐ๊ธ (Rotation)
โ โโโ ์ Access Token ๋ฐ๊ธ
โ<โโ { newAccessToken, newRefreshToken } โ
โ โ
โโโ POST /api/auth/logout โโโโโโโโโโ>โ
โ { refreshToken } โ
โ โโโ DB์์ RT ์ญ์
โ<โโ 204 No Content โโโโโโโโโโโโโโโ โ
๋ง์น๋ฉฐ
jjwt 0.12.x๋ก ๋์ด์ค๋ฉด์ API๊ฐ ๊ฝค ์ ๋ฆฌ๋๋ค.
๊ตฌ๋ฒ์ ์์ ์ฝ๋์์ ์์ฃผ ๋ณด์ด๋ parseClaimsJws(), setSubject(), Keys.secretKeyFor(SignatureAlgorithm.HS256) ๊ฐ์ ๋ฉ์๋๋ค์ด deprecated๋๊ฑฐ๋ ์ ๊ฑฐ๋์ผ๋, ์์ ์ฝ๋๋ฅผ ๊ทธ๋๋ก ๊ฐ์ ธ๋ค ์ฐ๋ฉด ์ปดํ์ผ ์๋ฌ๊ฐ ๋๋ค.
Refresh Token Rotation์ ๊ตฌํ ๋น์ฉ ๋๋น ๋ณด์ ํจ๊ณผ๊ฐ ํฌ๋ค.
ํ์ทจ๋ ํ ํฐ์ด ์ฌ์ฉ๋๋ ์๊ฐ ์ ์ ์ฌ์ฉ์์ ํ ํฐ๋ ๋ฌดํจํ๋๊ธฐ ๋๋ฌธ์, ์ด์ ์งํ๋ฅผ ๋น ๋ฅด๊ฒ ๊ฐ์งํ๊ณ ๋์ํ ์ ์๋ค.
์ฐธ๊ณ ์ถ์ฒ
Spring Boot 4.0 Release Notes — https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Release-Notes
Spring Security 7.0 Migration Guide — https://docs.spring.io/spring-security/reference/migration/index.html
RFC 7519: JSON Web Token (JWT) — https://datatracker.ietf.org/doc/html/rfc7519
jjwt GitHub Repository — https://github.com/jwtk/jjwt
'๐ Tech Stack > Backend' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| ์ฟ ํค vs ์ธ์ vs ํ ํฐ โ ์น ์ธ์ฆ ๋ฐฉ์ ๋น๊ต (0) | 2026.06.09 |
|---|---|
| JPA N+1 ๋ฌธ์ ์์ ์ ๋ณต โ ์ฌํ๋ถํฐ Fetch Join, Batch Size๊น์ง (0) | 2026.06.03 |
| RestClient๋ก ์ธ๋ถ API ํธ์ถํ๊ธฐ (0) | 2026.05.27 |