세션 방식에서의 최초 인증(로그인) 과정
UsernamePasswordAuthenticationFilter
의 내부를 살펴보기 전에 먼저, 세션 방식에서의 인증 과정을 살펴보자.
- 먼저 클라이언트의 로그인 요청이 들어오면
UsernamePasswordAuthenticationFilter
필터(위 이미지의AuthenticationFilter
)에서 요청을 가로챈다.***(1) AuthenticationFilter
에서는 클라이언트 요청에 담긴 인증 정보를 가지고UsernamePasswordAuthenticationToken
객체를 생성한다.(2)- 그렇게 생성한
UsernamePasswordAuthenticationToken
을AuthenticationManager
에게 넘겨주면서 인증 과정을AuthenticationManager
에게 일임한다.(3~9) - 만약 인증이 성공되면, 해당 세션 정보를
SecurityContextHolder
에 저장한다. 이때 세션 정보는SecurityContext
객체 안에 담아서 저장한다.(10)
스프링시큐리티에서 로그인을 처리하는 로직은 위와 같다.
그렇다면 이는 어떻게 구현되어있을까? 하나씩 살펴보자.
01. UsernamePasswordAuthenticationFilter
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login", "POST");
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
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.trim() : "";
String password = this.obtainPassword(request);
password = password != null ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return this.usernameParameter;
}
public final String getPasswordParameter() {
return this.passwordParameter;
}
}
UsernamePasswordAuthenticationFilter
에서 가장 주의깊게 볼 부분은 attemptAuthentication()
메소드이다.
UsernamePasswordAuthenticationFilter
이 상속하고 있는 AbstractAuthenticationProcessingFilter
를 보면 알겠지만, 해당 필터를 통해 인증을 진행할 경우 attemptAuthentication()
메소드가 실행된다.
해당 메소드의 동작 과정은 다음과 같다.
- 요청이 들어온 메소드가
POST
메소드인지 검증 진행, 만약 다른 메소드이면 예외 발생 - 요청이
POST
로 들어 왔을 경우 아래 과정과 같이 인증을 진행- 먼저 인증을 진행하기 위해
UsernamePasswordAuthenticationToken
객체 생성- 인증을 담당하는
AuthenticationManager
는UsernamePasswordAuthenticationToken
형태로 인증을 진행하기 때문에 우선적으로 해당 객체를 만들어야한다. - 이때 만들어진 인증 객체(
UsernamePasswordAuthenticationToken
)는 인증 정보(username, password 등)를 담고 있어야 한다.
- 인증을 담당하는
- 이후
getAuthenticationManager()
메소드를 통해AuthenticationManager
를 얻은 후, 해당 객체에게 인증 과정을 일임한다.
- 먼저 인증을 진행하기 위해
이처럼 UsernamePasswordAuthenticationFilter
는 AuthenticationManager
를 사용하여 유저 인증을 진행함을 확인할 수 있다.
이때 AuthenticationManager
의 인증 과정까지 다루기에는 내용이 너무 많기때문에 해당 포스팅에서는 생략한다.
위에서 attemptAuthentication()
메소드를 통해 인증을 진행하는 과정을 살펴보았다.
그럼 인증이 진행된 후에는 어떻게 처리를 해줄까?
해당 과정은 UsernamePasswordAuthenticationFilter
가 상속하고 있는 AbstractAuthenticationProcessingFilter
의 내부를 보면 알 수 있다.
02. AbstractAuthenticationProcessingFilter
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
protected ApplicationEventPublisher eventPublisher;
protected AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private AuthenticationManager authenticationManager;
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private RememberMeServices rememberMeServices = new NullRememberMeServices();
private RequestMatcher requiresAuthenticationRequestMatcher;
private boolean continueChainBeforeSuccessfulAuthentication = false;
private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
private boolean allowSessionCreation = true;
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();
protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
this.setFilterProcessesUrl(defaultFilterProcessesUrl);
}
protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
Assert.notNull(requiresAuthenticationRequestMatcher, "requiresAuthenticationRequestMatcher cannot be null");
this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
}
protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
this.setFilterProcessesUrl(defaultFilterProcessesUrl);
this.setAuthenticationManager(authenticationManager);
}
protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher, AuthenticationManager authenticationManager) {
this.setRequiresAuthenticationRequestMatcher(requiresAuthenticationRequestMatcher);
this.setAuthenticationManager(authenticationManager);
}
public void afterPropertiesSet() {
Assert.notNull(this.authenticationManager, "authenticationManager must be specified");
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
this.doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
try {
Authentication authenticationResult = this.attemptAuthentication(request, response);
if (authenticationResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
this.successfulAuthentication(request, response, chain, authenticationResult);
} catch (InternalAuthenticationServiceException var5) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var5);
this.unsuccessfulAuthentication(request, response, var5);
} catch (AuthenticationException var6) {
this.unsuccessfulAuthentication(request, response, var6);
}
}
}
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
if (this.requiresAuthenticationRequestMatcher.matches(request)) {
return true;
} else {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));
}
return false;
}
}
public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException;
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authResult);
this.securityContextHolderStrategy.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
this.securityContextHolderStrategy.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.logger.trace("Cleared SecurityContextHolder");
this.logger.trace("Handling authentication failure");
this.rememberMeServices.loginFail(request, response);
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
protected AuthenticationManager getAuthenticationManager() {
return this.authenticationManager;
}
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
public void setFilterProcessesUrl(String filterProcessesUrl) {
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(filterProcessesUrl));
}
public final void setRequiresAuthenticationRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.requiresAuthenticationRequestMatcher = requestMatcher;
}
public RememberMeServices getRememberMeServices() {
return this.rememberMeServices;
}
public void setRememberMeServices(RememberMeServices rememberMeServices) {
Assert.notNull(rememberMeServices, "rememberMeServices cannot be null");
this.rememberMeServices = rememberMeServices;
}
public void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) {
this.continueChainBeforeSuccessfulAuthentication = continueChainBeforeSuccessfulAuthentication;
}
public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void setAuthenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
this.authenticationDetailsSource = authenticationDetailsSource;
}
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
protected boolean getAllowSessionCreation() {
return this.allowSessionCreation;
}
public void setAllowSessionCreation(boolean allowSessionCreation) {
this.allowSessionCreation = allowSessionCreation;
}
public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionStrategy) {
this.sessionStrategy = sessionStrategy;
}
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) {
Assert.notNull(successHandler, "successHandler cannot be null");
this.successHandler = successHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) {
Assert.notNull(failureHandler, "failureHandler cannot be null");
this.failureHandler = failureHandler;
}
public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
this.securityContextRepository = securityContextRepository;
}
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
protected AuthenticationSuccessHandler getSuccessHandler() {
return this.successHandler;
}
protected AuthenticationFailureHandler getFailureHandler() {
return this.failureHandler;
}
}
코드가 길다... 핵심 내용만 살펴보자
doFilter() 메소드
doFilter()
메소드는 각 요청이 필터를 거칠때 실행되는 메소드이다.
즉 Authentication
필터가 요청을 가로체면 ***가장 먼저 doFilter()
메소드가 실행된다.doFilter()
가 동작하는 과정은 아래와 같다.
- 먼저
requiresAuthentication()
메소드를 실행하여, 들어온 요청이 인증을 필요로 하는지 검사한다.- 만약 인증이 필요 없는 요청이라면, 다음 필터로 요청을 넘긴다.
attemptAuthentication()
메소드를 실행하여 인증을 시도한다.- 여기서
attemptAuthentication()
메소드는 위의UsernamePasswordAuthenticationFilter
에서 오버라이딩하여 재정의한다. - 만약 인증 결과값인 'authenticationResult`가 비어있으면 메소드를 중지한다.
- 여기서
- 인증이 성공했으면,
sessionStrategy.onAuthentication()
메소드를 호출하여 세션 관련 설정을 해준다.- 이때 설정은
sessionStrategy
에 의해 수행된다.sessionStrategy
는SessionAuthenticationStrategy
를 구현하는 구현체이다.- 기본적으로 제공되는
SessionAuthenticationStrategy
의 구현체로는ChangeSessionIdAuthenticationStrategy
,ConcurrentSessionControlAuthenticationStrategy
,RegisterSessionAuthenticationStrategy
등이 있다. - 이때
sessionStrategy
의 디폴트 값으로NullAuthenticatedSessionStrategy
이 설정되어 있기 때문에, 해당 로직에서는 어떠한 동작도 하지 않는다.
- 이때 설정은
continueChainBeforeSuccessfulAuthentication
값을 참조하여, 이후 동작을 계속할지 또는 다음 필터로 넘길지를 결정한다.- 기본값으로 false이 들어있기 때문에 이후 동작을 계속한다.
successfulAuthentication()
메소드를 실행한다.- 해당 메소드는 인증이 성공적으로 이루어지면 실행되는 메소드다.
- 만약 위의 과정중에서 에러가 발행하면
unsuccessfulAuthentication()
메소드를 실행한다.- 해당 메소드는 인증이 실패하면 실행되는 메소드다.
attemptAuthentication()
attemptAuthentication()
은 추상메소드이다.
AbstractAuthenticationProcessingFilter
를 구현하는 클래스에서 정의해 주어야 한다.
successfulAuthentication()
successfulAuthentication()
메소드는 인증이 성공적으로 이루어진 후 실행되는 메소드이다.
- 먼저 이후 인가 작업에 사용될
SecutiryContext
를 생성 후,securityContextRepository.save()
를 통해SecurityContextHolder
에 해당 세션 정보를 저장한다. - 이후
rememberMeServices.loginSuccess()
메소드를 호출하여RememberMe
객체를 활성화한다.- 이때
RememberMe
객체는 사용자의 로그인을 유지하는 기능을 담당한다.
- 이때
unsuccessfulAuthentication()
메소드 인증이 실패하면 실행되는 메소드이다.
'Back End > Spring && Spring Boot' 카테고리의 다른 글
[Spring boot / JWT] Spring security를 사용해서 Refresh Token 구현하기 (0) | 2024.07.13 |
---|---|
[Spring Security / JWT] 스프링 부트에서 JWT 구현하기 with Spring Security (0) | 2024.07.10 |
[Spring boot / h2] Embedded VS In-Memory VS Server (0) | 2024.06.26 |
[Spring boot / JPA] 스프링 부트 데이터베이스 초기화 (0) | 2024.06.26 |
JWT 개념과 구조, 작동 방식 (0) | 2024.06.22 |