JWT(Json Web Token)은 RESTful API에서 세션 없이 인증과 인가를 처리할 수 있는 토큰 기반의 인증 방식입니다.
Spring Boot에서 JWT 기반 인증/인가를 구현하는 흐름을 알려드리겠습니다.
JWT란 무엇인가?
JWT는 Header, Payload, Signature 3가지로 구성된 JSON 기반 토큰입니다.
일반적으로 인증이 완료된 사용자의 정보를 Payload에 담아 클라이언트에 전달하며, 이 토큰을 재사용해 API 요청 시 인증 정보를 대신합니다.
HEADER: { "alg": "HS256", "typ": "JWT" }
PAYLOAD: { "sub": "userId", "role": "ROLE_USER", "exp": 1700000000 }
SIGNATURE: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
JWT는 자체적으로 정보를 포함하므로 DB 세션 조회 없이 Stateless 인증이 가능합니다.
Spring Boot에서 JWT 인증 흐름
- 사용자가 로그인 요청 (ID/PW)
- 서버에서 인증 성공 시 JWT 발급
- 클라이언트는 이후 요청 시
Authorization: Bearer {token}헤더에 포함 - 서버는 JWT를 검증 후 사용자 정보 추출
- 인가(권한 체크) 처리 후 요청 수행
이 과정은 Spring Security 필터 체인에서 대부분 처리됩니다.
JWT 의존성 추가 (Gradle)
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
jjwt는 JWT 생성을 위한 가장 널리 사용되는 라이브러리입니다.
JWT 생성/검증 유틸 클래스
@Component
public class JwtUtil {
private final String SECRET_KEY = "your-secret-key";
public String generateToken(String username, List<String> roles) {
Claims claims = Jwts.claims().setSubject(username);
claims.put("roles", roles);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600_000)) // 1시간
.signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()), SignatureAlgorithm.HS256)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(SECRET_KEY.getBytes())
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException e) {
return false;
}
}
public String getUsernameFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(SECRET_KEY.getBytes())
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
JWT 인증 필터 구현
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = resolveToken(request);
if (token != null && jwtUtil.validateToken(token)) {
String username = jwtUtil.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
Security 설정에 필터 적용
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtFilter;
public SecurityConfig(JwtAuthenticationFilter jwtFilter) {
this.jwtFilter = jwtFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
로그인 API 구현
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authManager;
private final JwtUtil jwtUtil;
public AuthController(AuthenticationManager authManager, JwtUtil jwtUtil) {
this.authManager = authManager;
this.jwtUtil = jwtUtil;
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
Authentication auth = authManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
List<String> roles = auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList();
String token = jwtUtil.generateToken(request.getUsername(), roles);
return ResponseEntity.ok(Map.of("token", token));
}
}
실무 팁
- 토큰 만료 시간은 너무 길게 설정하지 마세요 (보통 15분 ~ 1시간)
- Refresh Token을 도입하면 자동 로그인 유지가 가능합니다
- 쿠키 대신
Authorization: Bearer {token}방식 권장 - JWT는 위조 위험이 있으므로 SECRET_KEY는 노출되면 안 됩니다
JWT는 REST API에서 세션 없이 인증을 처리할 수 있는 강력한 도구입니다.
Spring Boot에서 JWT 인증을 구현하면, 마이크로서비스 구조나 프론트-백엔드 분리 아키텍처에서 유연하게 인증 로직을 처리할 수 있습니다.
단, 토큰 저장 위치, 만료 정책, 리프레시 토큰 처리 등은 보안상 충분한 고민이 필요합니다.
'개발 > JAVA' 카테고리의 다른 글
| [JAVA] Spring에서 세션 관리와 Redis 세션 클러스터링 (0) | 2025.10.27 |
|---|---|
| [JAVA] Spring Boot에서 OAuth2 소셜 로그인 구현하기 (kakao) (0) | 2025.10.26 |
| [JAVA] Spring Security 기본 개념 이해하기 (0) | 2025.10.24 |
| [JAVA] Spring Boot에서 이메일 발송 기능 구현하기 (JavaMailSender) (0) | 2025.10.23 |
| [JAVA] Spring Boot로 파일 업로드 및 다운로드 API 구현하기 (0) | 2025.10.22 |
