Form Login 설정
스프링 시큐리티는 세션 기반 인증을 기본으로 제공한다.
1. 인증이 안되면 로그인 페이지로 리다이렉트
2. 회원이 로그인을 하게 되면 인증 결과를 담은 Authentication 객체 생성 및 시큐리티 컨텍스트에 저장, 해당 시큐티리 컨텍스트를 세션에 저장
3. 세션에 저장된 인증 토큰으로 접근 및 인증 유지
`configure()` 메서드에서 Form Login 인증을 설정해보자.
@Configuration
@EnableWebSecurity
class SecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.anyRequest().authenticated()
http
// 폼 로그인 사용
.formLogin()
// 사용자 정의 로그인 페이지
.loginPage("/loginPage")
// 로그인 성공 후 이동 페이지 (핸들러 지정 없을 경우)
.defaultSuccessUrl("/")
// 로그인 실패 후 이동 페이지 (핸들러 지정 없을 경우)
.failureUrl("/loginPage")
// 아이디 파라미터명 설정(폼으로부터 넘어오는 파라미터 이름, input tag의 name 등)
.usernameParameter("userId")
// 패스워드 파라미터명 설정
.passwordParameter("passwd")
// 로그인 Form Action Url (폼 클릭 시 어느 Url로 요청을 보낼 지)
.loginProcessingUrl("/login_proc")
// 로그인 성공 후 실행되는 핸들러
.successHandler { request, response, authentication ->
println("Authentication: ${authentication.name}")
response.sendRedirect("/")
}
// 로그인 실패 후 실행되는 핸들러
.failureHandler { request, response, exception ->
println("Exception: ${exception.message}")
response.sendRedirect("/login")
}
// 해당 요청은 인증 없이도 이용 가능하도록 모두 혀용
.permitAll()
}
}
각 코드에 대한 설명은 주석에 잘 남겨놓았다.
로그인 성공/실패 후 처리되는 핸들러는 각각 `AuthenticationSuccessHandler`, `AuthenticationFailureHandler` 클래스를 매개변수 로 받고 있는데, 이 두 클래스는 모두 함수형 인터페이스이기 때문에 람다식으로 구현하여 넣어주었다.
`permitAll()`은 현재 위에 `authorizeRequests().anyRequest().authenticated()` 를 통해 모든 요청에 대해 인증된 사용자만 접근하도록 설정해 놓았는데, 이는 위에서 지정한 사용자 지정 로그인 페이지 /loginPage 역시 인증된 사용자만 접근이 되기 때문에, 이를 방지하기 위해 모든 요청에 대해 허용시켜 놓은 것이다.
Form Login 인증 필터 - UsernamePasswordAuthenticationFilter
위에서 Form Login 인증을 설정해 놓았는데, 이러한 설정에 따른 인증을 담당하는 곳은 `UsernamePasswordAuthenticationFilter`이다.
Form Login 인증이 수행되는 과정은 아래와 같다.
1. UsernamePasswordAuthenticationFilter가 AntPathRequestMatcher를 통해 요청 정보가 매칭되는지 확인
- 요청 정보가 매칭되지 않았다면 FilterChain의 doFilter를 통해 다음 필터 호출
2. 요청 정보 매칭 시 Username + Password 구성의 Authentication 객체 생성
3. AuthenticationManager를 통해 Authentication 객체 인증
- AuthenticationManager는 AuthenticationProvider에게 위임하여 (여러 구현체가 있음) 인증
- 인증 실패 시 AuthenticationException 발생, UsernamePasswordAuthenticationFilter가 해당 예외를 핸들링 하여 인증 실패 케이스 제어
4. AuthenticationProvider로 부터 인증에 성공했다면, User + Authorities 구성의 Authentication 객체 생성
5. Security Context에 저장 (전역적으로 인증 객체를 참조할 수 있도록 한 객체, 추후 설명)
6. SuccessHandler 호출
`UsernamePasswordAuthenticationFilter`는 `AbstractAuthenticationProcessingFilter`를 상속받아 구현된 클래스이다.
한번 `AbstractAuthenticationProcessingFilter`의 `doFilter()` 메서드를 살펴보자.
package org.springframework.security.web.authentication;
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);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
...
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
if (this.requiresAuthenticationRequestMatcher.matches(request)) {
return true;
}
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;
...
}
`requiresAuthentication()`을 통해 인증이 필요한지 확인한 후, `attemptAuthentication()`을 통해 인증을 시도한다.
`attemptAuthentication()`은 추상 메서드로 선언되어 있기 때문에 이는 `UsernamePasswordAuthenticationFilter`에 구현하여야 한다.
package org.springframework.security.web.authentication;
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
...
@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);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
...
}
구현된 `attemptAuthentication()`이다. 마지막에는 `AuthenticationManager`를 얻어서 인증을 수행하고 있다. (`AbstractAuthenticationProcessingFilter`의 멤버로 선언되어 있다)
`AuthenticationManager`는 인터페이스인데, 이의 구현체가 여러가지 있고, 구현에 역시 인증 방법이 달라지게 되는 것이다.
그 이후 과정은 `successfulAuthentication()`, `unsuccessfulAuthentication()` 메서드를 통해 인증의 결과를 처리하게 되는데, 성공한 경우에는 `SecurityContextHolder`에 인증 객체를 저장하게 된다.
`successfulAuthentication()`, `unsuccessfulAuthentication()`의 자세한 구현을 알고 싶다면 직접 코드를 살펴보기를 권장한다.