이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
Authentication은 왜 인증 요청과 인증 결과라는 두 역할을 모두 가지는가?AuthenticationManager,ProviderManager,AuthenticationProvider는 어떻게 책임을 나누는가?- 인증 성공 후
SecurityContextHolder에는 무엇이 저장되고 언제 주의해야 하는가?
개요
Spring Security 인증 구조의 핵심은 Controller 직접 판단에서 벗어나는 것이다. Controller가 username/password를 직접 비교하고 세션을 직접 세팅하는 방식은 보안 책임이 웹 계층에 흩어진다. Security는 자격 증명을 Authentication으로 표현하고, 검증을 AuthenticationManager와 AuthenticationProvider에게 맡긴다.
기존 문서의 최종 목표 문장도 이 흐름이었다. Authentication -> AuthenticationManager -> AuthenticationProvider -> SecurityContext -> AuthorizationFilter로 책임이 이어진다.
Authentication
Authentication은 두 가지 역할을 한다. 인증 전에는 사용자가 제출한 자격 증명을 담은 요청 객체다. 예를 들어 username/password 로그인에서는 UsernamePasswordAuthenticationToken.unauthenticated(username, password) 같은 객체가 인증 요청이 된다.
인증 후에는 현재 사용자 표현이다. principal, credentials, authorities, authenticated 상태를 가진다. 성공 후 ProviderManager는 기본적으로 민감한 credentials를 제거하려고 하므로, 인증 성공 객체를 로그나 세션에 남길 때도 credentials 보관 여부를 조심해야 한다.
Authentication request = UsernamePasswordAuthenticationToken.unauthenticated(
loginRequest.username(),
loginRequest.password());
Authentication result = authenticationManager.authenticate(request);이 코드는 Controller가 비밀번호를 직접 검증하지 않고 인증 요청을 Security 구조로 넘기는 모습을 보여 준다.
Manager와 Provider
AuthenticationManager는 인증 API다. 가장 흔한 구현은 ProviderManager다. ProviderManager는 여러 AuthenticationProvider를 순서대로 보며, 현재 Authentication 타입을 처리할 수 있는 Provider에게 검증을 맡긴다.
예를 들어 username/password 인증은 DaoAuthenticationProvider가 UserDetailsService와 PasswordEncoder를 이용해 처리할 수 있다. JWT 인증에서는 JwtAuthenticationProvider가 JwtDecoder로 토큰을 검증하고 authority를 변환한다.
이 구조 덕분에 앱은 username/password, JWT, OAuth2, pre-authentication 같은 여러 인증 방식을 하나의 AuthenticationManager 모델로 다룰 수 있다.
SecurityContextHolder
인증이 성공하면 결과 Authentication은 SecurityContext에 담긴다. SecurityContextHolder는 현재 요청에서 이 context를 꺼내 볼 수 있는 저장소다. 기본 전략은 ThreadLocal이므로 같은 요청 스레드 안에서 현재 사용자 정보를 참조할 수 있다.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();Spring MVC Controller에서는 @AuthenticationPrincipal을 사용해 더 깔끔하게 현재 principal을 받을 수 있다. 다만 비동기 처리나 새 스레드에서는 SecurityContext가 자동 전파되지 않을 수 있으므로 별도 전략이 필요하다. SecurityContext를 직접 세팅해야 하는 커스텀 인증 흐름에서는 기존 context에 바로 authentication을 꽂기보다 새 SecurityContext를 만들어 설정하는 편이 race condition을 피하기 좋다.
또한 SecurityContextHolderFilter 기반 흐름에서는 나중 요청에서도 context가 복원되기를 원한다면 SecurityContextRepository에 저장하는 책임을 확인해야 한다. 단순히 현재 ThreadLocal에 넣는 것과 다음 요청까지 유지하는 것은 다른 문제다.
실전 팁
- Controller는 인증 요청을 만들고 결과를 응답으로 연결하되, 검증 자체는 AuthenticationManager에 맡긴다.
- 인증 전 token과 인증 후 token을 구분한다.
- PasswordEncoder 없이 비밀번호를 직접 비교하지 않는다.
- 비동기 작업에서 현재 사용자 정보가 필요하면 SecurityContext 전파 방식을 명확히 한다.
- 커스텀 인증 성공 후 다음 요청에서도 인증 상태가 필요하다면 SecurityContext 저장 정책을 확인한다.
- 커스텀 AuthenticationProvider는 처리 가능한 Authentication 타입을 분명히 제한한다.
위험 신호!
- Controller가 DB에서 password hash를 꺼내 직접 비교한다.
- 인증 성공 후 credentials가 로그에 남는다.
- SecurityContextHolder를 전역 사용자 저장소처럼 장기 보관한다.
- ThreadLocal SecurityContext에만 인증을 넣고 세션이나 repository 저장이 필요한 흐름을 빠뜨린다.
- 비동기 작업에서 현재 사용자 정보가 없는데 우연히 ThreadLocal이 유지되길 기대한다.
- 여러 Provider가 같은 Authentication 타입을 애매하게 처리한다.
확인 질문
확인 질문
Authentication의 두 역할은 무엇인가?
- 인증 전 자격 증명 요청 객체와 인증 후 현재 사용자 정보 표현이다.
ProviderManager는 인증을 어떻게 수행하는가?
- 여러 AuthenticationProvider 중 처리 가능한 Provider에게 검증을 위임한다.
- SecurityContextHolder의 기본 저장 전략에서 주의할 점은 무엇인가?
- 요청 스레드 중심 ThreadLocal이므로 비동기나 새 스레드에서는 context 전파를 별도로 고려해야 한다.