Spring Security 6 JWT 인증 구현



Spring Security 6에서 JWT 토큰 기반 인증을 구현하는 방법입니다.



의존성


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>


Security 설정 (Spring Security 6)


@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}


JWT 유틸리티


@Component
public class JwtUtil {
private final String SECRET = "your-secret-key";
private final long EXPIRATION = 86400000; // 24시간

public String generateToken(UserDetails user) {
return Jwts.builder()
.subject(user.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(Keys.hmacShaKeyFor(SECRET.getBytes()))
.compact();
}

public String extractUsername(String token) {
return Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(SECRET.getBytes()))
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}

public boolean isTokenValid(String token, UserDetails user) {
String username = extractUsername(token);
return username.equals(user.getUsername()) && !isTokenExpired(token);
}
}


JWT 필터


@Component
public class JwtAuthFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) {
String header = request.getHeader("Authorization");

if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
String username = jwtUtil.extractUsername(token);

if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails user = userDetailsService.loadUserByUsername(username);
if (jwtUtil.isTokenValid(token, user)) {
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
}
chain.doFilter(request, response);
}
}


로그인 컨트롤러


@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthenticationManager authManager;
@Autowired
private JwtUtil jwtUtil;

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
Authentication auth = authManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(), request.getPassword()));

UserDetails user = (UserDetails) auth.getPrincipal();
String token = jwtUtil.generateToken(user);

return ResponseEntity.ok(Map.of("token", token));
}
}


Refresh Token 구현


// Access Token: 15분
// Refresh Token: 7일
@PostMapping("/refresh")
public ResponseEntity<?> refresh(@RequestBody RefreshRequest request) {
String refreshToken = request.getRefreshToken();
if (jwtUtil.isRefreshTokenValid(refreshToken)) {
String username = jwtUtil.extractUsername(refreshToken);
UserDetails user = userDetailsService.loadUserByUsername(username);
String newAccessToken = jwtUtil.generateAccessToken(user);
return ResponseEntity.ok(Map.of("accessToken", newAccessToken));
}
return ResponseEntity.status(401).build();
}