오늘 포스팅은 Spring Security 아키텍쳐에 이어서 Spring Security 인증 과정에 대하여 포스팅 하려고 한다.
form 로그인 방식에서의 인증 과정을 다루었다.
참고: https://docs.spring.io/spring-security/reference/servlet/authentication/index.html
form 형식으로 로그인 request가 들어오면(POST, ”/login”)
UsernamePasswordAuthenticationfilter
로 진입한다. 이 필터는 username(로그인 할 때 id)과 password를 통해 UsernamePasswordAuthenticationToken을 생성한다.
1): Spring Security는 디폴트 값으로 “username”, “password”를 키값으로 사용한다. 그래서 form형식에서 username으로 키를 설정해야 한다.
2): 기본 URL 값으로 /login
, 메서드는 POST
요청으로 지정되어 있다. HttpSecurity의 loginProcessingUrl() 메서드로 URL 설정이 가능하다.
3): 인증을 시도하는 메서드이다. username과 password를 사용해서 UsernamePasswordAuthenticationToken
을 생성한다. 그리고 이것을 AuthenticationManager에 전달 하여 인증 처리를 위임한다.
4): authenticate() 메서드는 Authentication
타입을 파라미터로 받고 Authentication
을 리턴한다. 즉, UsernamePasswordAuthenticationToken
은 Authentication 타입이라는 것을 알 수 있다.
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
//1)
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
//2)
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login","POST");
...
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
//3)
@Override
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());
}
String username = obtainUsername(request);
...
String password = obtainPassword(request);
...
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
...
//4)
return this.getAuthenticationManager().authenticate(authRequest);
}
...
}
UsernamePasswordAuthenticationFilter의 부모 클래스이다.
1): 인증에 성공했을 경우의 메서드를 구현하고 있다. UsernamePasswordAuthenticationToken을
SecurityContext에
저장하고 HttpSession
에 SecurityContext를 저장한다.
2): 인증에 실패할 경우의 메서드를 구현하고 있다. SeucurityContext를 초기화하고, AuthenticationFailureHandler
를 호출한다.
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
Authentication authenticationResult = attemptAuthentication(request, response);
...
//생략
}
...
//1)
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
//생략
}
//2)
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
//생략
}
...
...
}
SecurityContext
는 SecurityContextHolder에 포함되어 있으며, Authentication을 가지고 있다.SecurityContextHolder
는Spring Security가 인증된 사용자의 정보를 저장하는 곳이다. Spring Security는 어떻게 SecurityContextHolder가 채워지는 지는 신경쓰지 않고, 만약 SecurityContextHolder안에 값이 존재한다면 현재 인증된 유저로 사용할 수 있다. 기본적으로 SecurityContextHolder는 유저의 정보(details)를 저장하기 위해 ThreadLocal을 사용한다. 그래서 정보를 담고있는 SecurityContext는 thread-safe하다고 볼 수 있다.//코드가 많이 생략되어 있음
public class SecurityContextHolder {
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
...
if (!StringUtils.hasText(strategyName)) {
// Set default
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
////////////////////////////////////////// 스레드 로컬 /////////////
strategy = new ThreadLocalSecurityContextHolderStrategy();
return;
}
...
}
...
//생략
}
Authentication 인터페이스는 2가지 주요 용도로 사용된다.
Authentication
타입의 UsernamePasswordAuthenticationToken을 넘겨주어 인증을 처리하게 한다.Authentication은
principal
: username/password과 관련된 인증에서 주로 UserDetails 인스턴스.credentials
: 주로 password이고, 보안을 위해서 인증이 완료된 후에 지워짐.authorities
: user에게 부여된 권한.을 가지고 있다.
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
유저 로그인 -> UsernamePasswordAuthenticationFilter진입
-> UsernamePasswordAuthenticationToken 생성(Authentication 타입)
-> AuthenticationManager에게 인증되지 않은 Authentication 전달
-> AuthenticationManager가 ProviderManager에게 인증 위임
-> ProviderManager가 적절한 AuthenticationProvider를 찾아서 Authentication을 전달
-> DaoAuthenticationProvider(form 로그인 시)가 UserDetails 조회 후 인증이 완료되면 UserDetails의 principal을
Authentication에 추가해 인증된 Authentication을 만듦
-> AuthenticationManger가 UsernamePasswordAuthenticationFilter에 Authentication을 전달
-> SecurityContextHolder에 Authentication 저장.
-> 여러 과정을 거쳐 AuthenticationSuccessHandler 호출