Contents
see ListJWT는 발급보다 검증 경로가 더 중요합니다
Spring Boot에서 모바일 앱이나 SPA를 위한 로그인 API를 만들 때 JWT를 선택하는 경우가 많습니다. 서버가 로그인 상태를 별도 세션에 저장하지 않아 확장이 쉽다는 장점이 있지만, 토큰을 잘못 검증하면 인증이 사실상 무력화됩니다. 특히 서명 검증만 하고 만료 시간, 발급자, 권한 값을 느슨하게 다루거나 예외 처리를 통합하지 않으면 운영 중 원인을 찾기 어려운 401·403 오류가 반복됩니다.
실전 구성의 목표는 단순합니다. 로그인 성공 시 짧은 수명의 access token을 발급하고, 요청마다 Spring Security 필터에서 토큰을 검증한 뒤, 검증된 사용자만 SecurityContext에 넣습니다. 인증 실패와 권한 부족은 서로 다른 응답으로 분리하며, 비밀키와 만료 시간은 코드가 아닌 환경 설정에서 관리합니다.
권한 판단의 기준을 먼저 정합니다
토큰에는 사용자 식별자와 최소한의 권한 정보만 넣는 편이 안전합니다. 이메일, 전화번호, 내부 운영 메모처럼 바뀌거나 민감한 데이터는 넣지 않습니다. 권한은 ROLE_USER, ROLE_ADMIN처럼 서버 정책에 맞춘 값으로 제한하고, 클라이언트가 보낸 역할 값을 그대로 신뢰하지 않습니다. 로그인 시 데이터베이스의 사용자 상태와 비밀번호 해시를 확인한 뒤에만 토큰을 발급해야 합니다.
- access token은 짧게 설정하고, 유출을 전제로 만료 시간을 운영합니다.
- refresh token이 필요하면 별도 저장소와 폐기 정책을 두고 access token과 용도를 분리합니다.
- 비밀키는 Git 저장소, 로그, 오류 응답에 포함하지 않습니다.
- 비활성·잠금 계정은 토큰 발급 전 차단하고 기존 토큰의 무효화 정책도 정합니다.
SecurityFilterChain에서 무상태 인증을 명시합니다
REST API만 제공하는 서비스라면 세션을 만들지 않는 정책을 명확히 지정합니다. CSRF는 브라우저 쿠키 기반 세션 인증을 전제로 하는 방어이므로, Authorization 헤더의 Bearer 토큰만 받는 API에서는 사용 방식에 맞게 검토합니다. 다만 관리자 화면처럼 쿠키 인증을 함께 쓴다면 같은 설정을 그대로 적용하면 안 됩니다. 공개 URL은 최소 범위로 열고 나머지는 인증을 요구하는 기본값을 유지하는 것이 좋습니다.
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/login", "/actuator/health").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}필터 위치도 중요합니다. JWT 필터는 아이디·비밀번호 로그인 처리를 담당하는 UsernamePasswordAuthenticationFilter보다 먼저 실행되어야 이후 인가 규칙이 인증 정보를 사용할 수 있습니다. 공개 경로에서도 필터가 실행될 수 있으므로 토큰이 없으면 조용히 다음 필터로 넘기고, 토큰이 있는데 유효하지 않은 경우에는 명확히 인증 오류로 종료하는 정책을 정해야 합니다.
필터에서는 파싱, 검증, 컨텍스트 설정을 분리합니다
Authorization 헤더는 반드시 Bearer 접두어를 검사합니다. 토큰 문자열을 얻은 뒤 서명, 만료 시각, 발급자, 대상 서비스 값을 확인하고 예외를 세분화합니다. 검증 결과의 subject는 내부 사용자 ID처럼 변경 가능성이 낮은 값으로 두는 편이 좋습니다. 토큰의 권한 정보만으로 중요한 권한을 즉시 허용하기보다, 권한 변경이 잦은 서비스에서는 사용자 상태를 조회하거나 토큰 수명을 더 짧게 유지하는 방식을 선택합니다.
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null || !header.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
try {
String token = header.substring(7);
JwtClaims claims = jwtService.validate(token);
var authorities = claims.roles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList();
var authentication = new UsernamePasswordAuthenticationToken(
claims.userId(), null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
} catch (ExpiredJwtException e) {
SecurityContextHolder.clearContext();
writeUnauthorized(response, "TOKEN_EXPIRED");
} catch (JwtException e) {
SecurityContextHolder.clearContext();
writeUnauthorized(response, "TOKEN_INVALID");
}예제의 jwtService.validate는 단순 디코딩 함수가 아니라 검증 함수여야 합니다. Base64 디코딩만으로 payload를 읽는 것은 인증이 아닙니다. 알고리즘을 서버에서 고정하고, 허용하지 않은 알고리즘이나 서명 없는 토큰은 거부해야 합니다. 토큰 원문을 로그에 남기지 말고 사용자 ID, 오류 코드, 요청 경로, 추적 ID만 남겨 사고 조사에 필요한 정보를 확보합니다.
401과 403을 일관되게 운영합니다
토큰이 없거나 만료·위조된 경우는 401 Unauthorized, 로그인은 되었지만 관리자 권한처럼 필요한 권한이 없는 경우는 403 Forbidden으로 구분합니다. 프런트엔드는 오류 메시지 문장에 의존하지 말고 TOKEN_EXPIRED 같은 안정적인 오류 코드를 기준으로 재로그인 또는 토큰 갱신을 처리해야 합니다. 로그인 실패 시에도 아이디 존재 여부를 알려 주지 않는 공통 메시지를 사용하면 계정 열거 위험을 줄일 수 있습니다.
배포 전 점검 목록
- 토큰 검증에서 서명, 만료 시간, 발급자와 대상 값을 모두 확인했는지 점검합니다.
- 시크릿과 refresh token, Authorization 헤더가 로그와 모니터링 도구에 남지 않는지 확인합니다.
- 401과 403 응답 형식 및 오류 코드가 API 전체에서 동일한지 테스트합니다.
- 권한 변경, 계정 잠금, 로그아웃 이후 기존 토큰을 어떻게 처리할지 정책과 테스트를 준비합니다.
- 공개 API 경로가 필요한 범위만 허용되어 있는지 코드 리뷰에서 다시 확인합니다.
JWT 인증은 토큰을 만든 뒤 끝나는 기능이 아닙니다. 검증 규칙, 예외 응답, 키 관리, 권한 변경 정책을 함께 운영해야 예측 가능한 API 보안 경계가 만들어집니다.
spring
| No | 작성일 | Title |
|---|---|---|
| 2131 | 2026. 02. 11. | Testcontainers로 Spring 통합 테스트 자동화 |
| 2130 | 2026. 02. 11. | Spring Batch 5.x 대용량 데이터 처리 패턴 |
| 2129 | 2026. 02. 11. | Spring Modulith: 모듈러 모놀리스 아키텍처 |
| 2128 | 2026. 02. 11. | Spring Data JPA 쿼리 최적화 고급 기법 |
| 2127 | 2026. 02. 11. | GraalVM Native Image와 Spring Boot AOT 컴파일 |
| 2126 | 2026. 02. 11. | Spring Cloud Gateway로 API Gateway 구축하기 |
| 2125 | 2026. 02. 11. | Spring Security 6.x OAuth2/OIDC 실전 구현 |
| 2124 | 2026. 02. 11. | Virtual Thread와 Spring WebFlux 성능 비교 |
| 2123 | 2026. 02. 11. | Spring AI: 스프링에서 LLM 통합하기 |
| 2122 | 2026. 02. 11. | Spring Boot 3.4 신기능과 마이그레이션 가이드 |