💡 오늘 학습 키워드
- Spring Security에서 흔히 발생하는 문제 & 해결 사례
🎯 학습 내용 정리
Spring Security를 처음 적용하면 기능 구현보다 오류를 해결하는 데 더 많은 시간을 보내는 경우가 많다.
JWT를 정상적으로 발급했는데도 401 Unauthorized가 반환되거나, 인증이 완료되었는데 403 Forbidden이 발생하기도 한다. 또한 SecurityContextHolder에서 Authentication이 null로 조회되거나 hasRole()이 정상적으로 동작하지 않는 문제도 자주 발생한다.
이러한 문제들은 대부분 Spring Security의 동작 원리를 충분히 이해하지 못한 상태에서 발생한다. 따라서 단순히 오류를 해결하는 것보다 왜 이런 현상이 발생하는지를 이해하는 것이 중요하다.
문제 1. JWT가 있는데도 401 Unauthorized가 발생
가장 많이 발생하는 문제이다.
클라이언트는 JWT를 전송했지만 서버는 인증되지 않은 사용자로 판단한다.
대표적인 원인은 다음과 같다.
- Authorization Header 누락
- Bearer Prefix 누락
- JWT 만료
- JWT 서명(Signature) 오류
- JwtAuthenticationFilter가 등록되지 않음
예를 들어 Header가 다음과 같다면
Authorization: eyJhbGc...
JWT Filter는 토큰을 찾지 못한다.
정상적인 형식은 다음과 같다.
Authorization: Bearer eyJhbGc...
해결 방법
JWT Filter에서는 반드시 Header를 확인해야 한다.
String bearerToken =
request.getHeader("Authorization");
if (bearerToken == null ||
!bearerToken.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
또한 JWT 검증이 성공한 경우에만 Authentication을 생성해야 한다.
if (jwtProvider.validateToken(token)) {
Authentication authentication =
jwtProvider.getAuthentication(token);
SecurityContextHolder.getContext()
.setAuthentication(authentication);
}
문제 2. 인증은 되었는데 403 Forbidden이 발생
JWT는 정상적으로 검증되었지만 권한이 부족한 경우이다.
예를 들어
@PreAuthorize("hasRole('ADMIN')")
현재 사용자가
ROLE_USER
라면 403이 발생한다.
해결 방법
현재 Authentication이 가지고 있는 권한을 확인한다.
Authentication authentication =
SecurityContextHolder.getContext()
.getAuthentication();
authentication.getAuthorities()
.forEach(System.out::println);
실제로 저장된 권한과 Security 설정이 일치하는지 확인하는 것이 중요하다.
문제 3. hasRole()이 동작하지 않음
다음과 같은 코드가 있다고 가정한다.
@PreAuthorize("hasRole('ADMIN')")
그런데 Authentication에는 다음 권한이 저장되어 있다.
ADMIN
Spring Security는 내부적으로
ROLE_ADMIN
을 찾는다.
따라서 항상 권한이 없다고 판단한다.
해결 방법
방법은 두 가지이다.
방법 1
권한을 ROLE_PREFIX와 함께 저장한다.
new SimpleGrantedAuthority(
"ROLE_ADMIN"
);
방법 2
ROLE Prefix를 사용하지 않는다면
@PreAuthorize(
"hasAuthority('ADMIN')"
)
을 사용하는 것이 좋다.
문제 4. Authentication이 null
다음 코드를 실행했는데
Authentication authentication =
SecurityContextHolder
.getContext()
.getAuthentication();
null이 반환되는 경우가 있다.
대표적인 원인은 다음과 같다.
- JwtAuthenticationFilter가 실행되지 않음
- Authentication을 저장하지 않음
- permitAll() 요청
- Filter 등록 순서 오류
해결 방법
Authentication을 생성한 뒤 반드시 저장해야 한다.
Authentication authentication =
jwtProvider.getAuthentication(token);
SecurityContextHolder
.getContext()
.setAuthentication(authentication);
문제 5. Controller에서 로그인한 사용자를 가져오지 못함
다음과 같은 코드가 자주 보인다.
@GetMapping("/me")
public UserResponse me(
@RequestHeader("Authorization")
String token
) {
Long userId =
jwtProvider.getUserId(token);
...
}
Controller에서 JWT를 직접 파싱하는 방식은 권장되지 않는다.
해결 방법
Authentication 또는 @AuthenticationPrincipal을 사용한다.
@GetMapping("/me")
public UserResponse me(
@AuthenticationPrincipal
CustomUserDetails user
) {
return userService.findById(
user.getUserId()
);
}
문제 6. JwtAuthenticationFilter가 실행되지 않음
SecurityConfig에 Filter를 등록하지 않으면 JWT 인증은 수행되지 않는다.
잘못된 예시이다.
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.build();
}
해결 방법
Filter를 등록한다.
http.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
);
문제 7. permitAll()인데도 JwtAuthenticationFilter가 실행
많은 개발자가 오해하는 부분이다.
다음과 같은 설정이 있다고 가정한다.
.requestMatchers("/login")
.permitAll()
JWT Filter는 여전히 실행된다.
permitAll은 인가를 허용하는 설정이지 Filter 실행을 생략하는 설정이 아니다.
해결 방법
JWT 검증이 필요 없는 URL은 Filter 내부에서 제외한다.
@Override
protected boolean shouldNotFilter(
HttpServletRequest request
) {
return request.getServletPath()
.startsWith("/auth");
}
문제 8. SecurityContext가 다음 요청에도 유지된다고 생각함
Authentication은 요청마다 새로 생성된다.
요청이 끝나면 SecurityContext는 제거된다.
SecurityContextHolder.clearContext();
다음 요청에서는 JWT를 다시 검증해야 한다.
문제 9. Refresh Token으로 API 인증을 수행
Refresh Token은 Access Token을 재발급하기 위한 토큰이다.
API 인증에는 사용해서는 안 된다.
올바른 구조는 다음과 같다.
Access Token
↓
API 요청
----------------
Refresh Token
↓
재발급 API
문제 10. 401과 403을 구분하지 않음
많은 프로젝트가 다음과 같이 처리한다.
모든 인증 오류
↓
403
하지만 의미는 다르다.
| 상태 코드 | 의미 |
| 401 | 인증 실패 |
| 403 | 권한 부족 |
이를 명확히 구분해야 한다.
Spring Security 문제를 해결할 때는 다음 순서대로 확인하면 대부분의 원인을 빠르게 찾을 수 있다.
다음과 같은 디버깅 체크리스트를 작성해보자.
- Authorization Header가 정상적으로 전달되는가?
- Bearer Prefix가 포함되어 있는가?
- JWT가 만료되지 않았는가?
- JwtAuthenticationFilter가 실행되는가?
- SecurityContextHolder.setAuthentication()이 호출되는가?
- Authentication에 권한이 정상적으로 저장되는가?
- hasRole()과 hasAuthority()를 올바르게 사용했는가?
- Filter 등록 순서가 올바른가?
- AuthenticationEntryPoint와 AccessDeniedHandler가 정상 등록되어 있는가?
- permitAll()과 shouldNotFilter()의 역할을 혼동하지 않았는가?
Spring Security에서 발생하는 대부분의 문제는 인증과 인가의 흐름을 이해하면 원인을 빠르게 파악할 수 있다. 401 Unauthorized는 인증 정보가 없거나 유효하지 않은 경우 발생하며, 403 Forbidden은 인증은 완료되었지만 필요한 권한이 없는 경우 발생한다. 또한 hasRole()과 hasAuthority()의 차이, ROLE_ Prefix 처리 방식, SecurityContextHolder의 생명주기, JwtAuthenticationFilter의 등록 위치와 실행 순서를 이해하는 것은 안정적인 인증 시스템을 구현하는 데 필수적이다.
문제가 발생했을 때는 단순히 설정을 수정하기보다 요청이 Security Filter Chain을 통과하면서 Authentication이 언제 생성되고, 어디에 저장되며, 어떤 시점에 권한 검사가 수행되는지를 추적하는 것이 가장 효과적인 디버깅 방법이다.
📚 한줄 정리
Spring Security 문제의 대부분은 Authentication이 생성되고 SecurityContextHolder에 저장되어 권한 검사를 거쳐 Controller까지 전달되는 흐름을 이해하면 원인을 빠르게 찾고 해결할 수 있다.
'Bootcamp > Implementation' 카테고리의 다른 글
| Spring Security 전체 과정 흐름 추적 (0) | 2026.07.16 |
|---|---|
| Refresh Token (0) | 2026.07.15 |
| CORS와 CSRF (0) | 2026.07.14 |
| AuthenticationEntryPoint와 AccessDeniedHandler 예외 처리 (0) | 2026.07.13 |
| AuthenticationManager와 AuthenticationProvider (0) | 2026.07.10 |