앞서 스프링 시큐리티의 구조를 살펴보았다. 스프링 시큐리티에서 인증이 어떻게 처리되는지 살펴보고자 한다.
스프링 시큐리티 인증에서 사용되는 핵심적인 요소들이다.
- SecurityContextHolder - 인증된 사람에 대한 정보를 저장하는 곳
- SecurityContext - 인증된 사용자를 포함한 SecurityContextHolder가진 객체이다
- GrantedAuthority - 권한 (예: 역할, 범위 등)
- AuthenticationManager - Spring Security 필터가 인증을 수행하는 방법을 정의하는 API
- ProviderManager - 가장 일반적인 AuthenticationManager
- AuthenticationProvider - 특정 유형의 인증을 수행하는데 사용되는 ProviderManager
SecurityContextHolder는 아래와 같은 구조로 되어있다.

인증된 사용자의 정보를 securityContext가 가지고 있다고 보면된다.
- principal : 사용자를 식별하는 정보로 이름/비밀번호 등이 저장되어있다
- credentials : 비밀번호인 경우가 많으며, 사용자가 인증된 후에는 지워진다.
- authorities : 역할 또는 범위가 저장되어 있다.
스프링 시큐리티가 인증하는 과정은 아래와 같다

1) 사용자가 로그인을 했을때, AbstractAuthenticationProcessingFilter는 Authentication 객체를 생성하는데 이 Authentication는 해당 필터의 구현클래스에 따라 다르다. 예를들어 위 필터의 구현클래스 중 하나인 usernamePasswordAuthenticationFilter는 사용자가 제출한 id와 비밀번호를 가진 UsernamePasswordAuthenticationToken을 생성한다.
2) 그리고 인증을 위해 AuthenticationManager로 전달된다. AuthenticationManager도 인터페이스이므로 여러가지로 구현할 수 있는데 가장 일반적으로는 ProviderManager 클래스를 사용한다.
3) 인증에 실패하게 되면 SecurityContextHolder는 지워지고, RememberMeServices.loginFail 이 호출된다. 하지만 Remember me가 구성되지 않았다면 작동하지 않는다.
AuthenticationFailureHandler 호출된다. 이를 통해 실패시 처리를 할 수 있다.
4) 인증이 성공하게 되면 SessionAuthenticationStrategy에서 새로운 로그인에 대한 알림을 받는다. Authentication 객체는 SecurityContextHolder에 저장된다.
AuthenticationSuccessHandler 호출되고 이를 통해 성공시 처리를 할 수 있다.
사용자를 인증하는 방식 중 가장 일반적인 방법으로 사용자의 이름(ID)와 비밀번호를 검증하는 것이다. 검증하는 방식은 1) form 방식 2) basic 방식 3) digest 방식이 있는데 3번은 시큐리티 문서를 보니 최신 애플리케이션에서는 권장하지 않는 방식이라고 한다.
나는 가장 흔하게 쓰이는 form방식에 대해서 알아보려고 한다.

저번 글에서 로그인한 사용자만 허용하는 url을 설정하는 코드가 있다. 이러한 url로 접근하게 되면 사용자 권한이 없는 리소스에 요청을 한 것으로 1) AccessDeniedException을 호출할 것이다.
사용자가 인증되지 않았으므로 2) ExceptionTranslationFilter에서 로그인 페이지로 리다이렉션을 보낸다.
그러면 3) 브라우저는 리다이렉션된 로그인 페이지를 요청하고, 로그인 페이지를 렌더링하게 된다.
사용자가 로그인을 요청하면 UsernamePasswordAuthenticationFilter에서 UsernamePasswordAuthenticationToken 을 생성하게 되는데 HttpServletRequest 인스턴스에서 username과 password를 추출한다. 다음으로 AuthenticationManager에 넘기게 되는데 이는 사용자 정보가 저장되는 방식에 따라 달라진다. 위에서 말한것처럼 ProviderManager가 가장 일반적으로 사용된다.
이러한 form 로그인 방식을 설정하는 것은 아래와 같다.
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
// ...
}
코드를 보면 loginPage를 통해 직접 내가 설정한 login url을 설정해줄 수 도 있다.이렇게 설정하면 위의 과정대로 처리가 되는 것이다.
로그인 요청 시 UsernamePasswordAuthenticationToken 을 생성하는 로직을 볼 수 있다. 사용자가 제출한 username과 password를 추출하여 UsernamePasswordAuthenticationToken 객체에 넣어주고, this.getAuthenticationManager().authenticate(authRequest); 를 통해 Authentication 객체를 리턴하고 있다. 위에 말했듯이 AuthenticationManager로 위임하여 인증 과정을 거치는데, 이때 어떤 구현체를 사용하느냐에 따라 인증하는 로직이 다르다.
보통 security config에서 커스텀하여 설정을 바꿀 수도 있지만 formlogin을 사용했을 경우에 별다른 설정이 없다면 security에서 기본적으로 사용하는 provider 구현체를 사용한다.
provider 클래스에서 어떻게 인증 과정을 거치는지는 다음 시간에 정리하곘다 ^^
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
username = username != null ? username : "";
username = username.trim();
String password = this.obtainPassword(request);
password = password != null ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
[출처]