
사용자가 로그인 인증을 요청했을 때 spring security에서 해당 요청을 어떻게 처리하는지 도식화한 이미지이다. 일반적으로 ID/Password(spring security에서는 username/password)를 이용해서 로그인하기 때문에 위의 이미지도 ID/Password를 이용한 로그인 인증을 처리하는 상황이다.
1. 가장 먼저 사용자가 로그인 폼(form)을 통해 spring security가 적용된 애플리케이션으로 Id(username)와 password를 포함한 request를 전송한다. 사용자의 요청이 [spring security filter] 영역으로 들어오면 가장 먼저 UsernamePasswordAuthenticationFilter가 요청을 전달받는다.
(Filter가 요청을 가로챈다.)
2. request를 전달받은 UsernamePasswordAuthenticationFilter는 id(username)와 password를 이용해 UsernamePasswordAuthenticationToken을 생성한다.
UsernamePasswordAuthenticationToken는 Authentication 인터페이스를 구현한 클래스이고, 여기서의 Authentication은 아직 인증되지 않은 Authentication이다.
3. UsernamePasswordAuthenticationFilter는 아직 인증되지 않은 Authentication을 AuthenticationManager로 전달한다.
AuthenticationManager는 인증 처리를 총괄하는 매니저 역할을 맡은 인터페이스이고, 이를 구현한 클래스가 ProviderManager 클래스이다.
➡️ 인증 작업을 실질적으로 총괄하는 역할은 ProviderManager
➡️ 인증 작업을 직접 하는 것이 아니라 다른 클래스에게 위임 > AuthenticationProvider
4. AuthenticationProvider는 ProviderManger로부터 Authentication을 전달받는다.
(UsernamePasswordAuthenticationFilter--Token-->AuthenticationManger--Token-->AuthenticationProvider)
AuthenticationProvider는 username으로 DB에서 데이터를 조회하고 비밀번호 일치 여부를 검사하는 방식으로 동작한다.
5. AuthenticationProvider는 UserDetailsService를 이용해 UserDetails를 조회한다.
(UserDetailsService로 조회할 아이디를 전달한다.)
UserDetails는 DB 등의 저장소에 저장된 사용자의 username과 password(Credential), 사용자 권한 정보를 포함하고 있는 컴포넌트이다.
6. UserDetailsService가 DB등의 저장소에서 사용자의 credential을 포함한 사용자 정보를 조회한다.
(UserDetails를 제공해주는 컴포넌트가 UserDetailsService)
7. 조회한 결과를 반환하기 위해 UserDetails를 생성한다.
8. 생성된 UserDetails를 다시 AuthenticationProvider에게 전달한다.
9. AuthenticationProvider는 전달 받은 UserDetails에 포함된 password를 PasswordEncoder를 통해 암호화하고, Authentication에 포함된 비밀번호(입력받은 비밀번호)와 일치하는지 확인한 뒤, 일치한다면 UserDetails를 이용해 인증된 Authentication을 생성하고, 일치하지 않는다면 BadCredentialException을 throw하고 인증 처리를 중단한다.
10. 인증된 Authentication을 ProviderManager에게 전달한다.
[2번 과정]에서의 Authentication은 인증에 필요한 사용자의 로그인 정보를 담고 있고, 여기서의 Authentication은 인증에 성공한 사용자 정보(Principal, Credential, GrantedAuthorities)를 가지고 있다.
11. 인증된 Authentication을 UsernamePasswordAuthenticationFilter에게 전달한다.
12. UsernamePasswordAuthenticationFilter는 SecurityContextHolder를 통해 인증된 Authentication을 SecurityContext에 저장한다.
인증 과정을 어느 정도 이해했으면, 다음은 코드! 코드를 보자.
1️⃣ 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");
...
public UsernamePasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
@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);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
...
}
어김없이 찾아오는 편두통..
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter
1. UsernamePasswordAuthenticationFilter는 AbstractAuthenticationProcessingFilter를 상속한다.
Filter역할을 하려면 doFilter() 메소드가 구현되어 있어야 하는데 여기서는 찾아볼 수 없어서 엥??할 수 있지만 상위 클래스에 구현되어 있어서 문제없다. ==> 사용자의 로그인 request를 doFilter() 메소드가 구현되어 있는 AbstractAuthenticationProcessingFilter가 가장 먼저 전달받는다.
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
2. 클라이언트의 로그인 폼을 통해 전송되는 request 파라미터의 디폴트값은 username과 password이다.
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login","POST");
3. AntPathRequestMatcher는 클라이언트의 URL에 매치되는 matcher이다.
클라이언트의 URL이 [/login]이고 HTTP method가 POST일 경우 매칭될 것이라 예상할 수 있다..
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
4. DEFAULT_ANT_PATH_REQUEST_MATCHER는 AbstractAuthenticationProcessingFilter 클래스에 전달되어 filter가 구체적인 작업을 수행할지 다른 filter를 호출할지 결정할 때 사용된다.
@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);
...
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
...
return this.getAuthenticationManager().authenticate(authRequest);
}
5. username과 password를 이용해 인증을 하는 메소드가 있다.
- attemptAuthentication() 메소드는 상위 클래스인 AbstractAuthenticationProcessingFilter 클래스의 doFilter() 메소드에서 호출된다(Filter에서 어떤 처리의 시작점은 doFilter() 메소드이다).
- HTTP method가 POST가 아니면 AthenticationServiceException을 throw한다.
- 클라이언트가 전달한 username과 password를 통해 UsernamePasswordAuthenticationToken을 생성한다.
(도식화된 인증 흐름을 볼 때 등장한 인증된 Authentication과는 상관없음. 현재 코드에서 만들어지는 토큰은 인증을 하기 위해 필요한 인증 토큰일 뿐!)
- AuthenticationManager를 get해서 authenticate()의 파라미터로 authRequest를 넘겨줌으로써 인증 처리를 위임했다.
2️⃣ AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter 클래스는 HTTP 기반의 인증 요청을 처리하지만, 실질적인 인증 시도는 하위 클래스에게 맡기고, 인증 성공 시 인증된 사용자의 정보를 SecurityContext에 저장하는 역할을 한다.


if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
...
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;
}
1.
- AbstractAuthenticationProcessingFilter 클래스가 인증 처리를 할지 다음 filter를 호출할지 결정한다.
- requiresAuthentication() 메소드는 하위 클래스에서 전달받은 requiresAuthenticationRequestMatcher 객체를 통해서 들어오는 요청이 인증 처리를 해야 하는지 판단한다.
(1️⃣- 3 에서 등장한 AntPathRequestMatcher("/login", "POST")의 파라미터인 URL과 HTTP method가 매칭 조건이다.)
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);
}
2. 하위 클래스(UsernamePasswordAuthenticationFilter)에 인증을 시도해 줄 것을 요청한다(attemptAuthentication()).
3. 인증에 성공하면 successfulAuthentication() 메소드를 호출한다.
exception이 throw될 경우(실패할 경우) unsuccessfulAuthentication() 메소드를 호출한다.

successfulAuthentication() 메소드가 호출되면 SecurityContextHolder를 통해 사용자의 인증정보를 SecurityContext에 저장한 뒤, SecurityContext를 HttpSession에 저장한다.
3️⃣ UsernamePasswordAuthenticationToken
- Spring Security에서 username/password로 인증하기 위해 필요한 토큰이다.
- 인증 성공 후, 인증에 성공한 사용자의 인증 정보를 UsernamePasswordAuthenticationToken에 포함해서 Authentication 객체 형태로 SecurityContext에 저장한다.
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
...
private final Object principal;
private Object credentials;
...
public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
return new UsernamePasswordAuthenticationToken(principal, credentials);
}
public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
}
...
}
1. principal: username 등의 신원
credentials: password
2. unauthenticated() 메소드
인증에 필요한 용도에 맞게 인증되지 않은 UsernamePasswordAuthenticationToken 객체를 생성
3. authenticated() 메소드
인증에 성공한, 이후 SecurityContext에 저장 될 UsernamePasswordAuthenticationToken 객체를 생성
4️⃣ Authentication
Spring Security에서 인증 자체를 표현하는 인터페이스이다.
(UsernamePasswordAuthenticationToken이 AbstractAuthenticationToken 추상 클래스를 상속받고, AbstractAuthenticationToken은 Authentication 인터페이스를 일부 구현했다.)
인증 과정에서 생성되는 토큰과 인증 성공 후 생성되는 토큰 모두 UsernamePasswordAuthenticationToken과 같은 Authentication의 하위 클래스인 형태로 생성된다. 하지만 토큰을 받거나 SecurityContext에 저장되는 경우에는 Authentication 형태로 리턴받거나 저장된다.
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Authorities: AuthenticationProvider에 의해 부여된 사용자 접근 권한 목록.
Credentials: 사용자 인증에 필요한 password. 인증이 이루어지고 난 직후에 ProviderManager가 해당 Credentials를 삭제.
Principal: 사용자를 식별하는 고유정보. username, userDetails 등
5️⃣ AuthenticationManager
인증 처리를 총괄하는 매니저 역할의 인터페이스이다.
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
인증을 위한 실질적인 관리는 AuthenticationManager를 구현하는 클래스를 통해 이루어진다.
6️⃣ ProviderManager
AuthenticationProvider를 관리하고 AuthenticationProvider에게 인증 처리를 위임한다.
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
checkState();
}
...
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
...
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
...
}
...
}
public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent)
1. ProviderManager가 Bean등록 될 때 List<AuthenticationProvider>를 DI 받는다.
for (AuthenticationProvider provider : getProviders())
2. 의존성 주입을 받은 List에서 for문으로 적절한 AuthenticationProvider를 찾는다.
try {
result = provider.authenticate(authentication); // (3)
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
3. AuthenticationProvider를 찾으면 해당 provider에게 인증 처리를 위임한다.
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials(); // (4)
}
...
4. 인증이 정상적으로 되었다면 사용된 Credentials를 제거한다.
7️⃣ AuthenticationProvider
AuthenticationManager로부터 인증 처리를 위임 받아 실질적인 인증 수행을 담당하는 컴포넌트이다.
현재 보고 있는 과정은 username/password 기반의 인증 처리이고, DaoAuthenticationProvider가 담당하고 있다.
DaoAuthenticationProvider는 UserDetailsService로부터 전달받은 Userdetails를 이용해 인증을 수행한다.
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
...
private PasswordEncoder passwordEncoder;
...
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
...
}
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider
1. AbstractUserDetailsAuthenticationProvider는 AuthenticationProvider 인터페이스의 구현 클래스이고, DaoAuthenticationProvider는 구현 클래스를 상속한 확장 클래스이다.
➡️ 실질적인 인증 처리는 AbstractUserDetailsAuthenticationProvider의 authenticate() 메소드부터 시작한다.
protected final UserDetails retrieveUser(..) throws AuthenticationException{
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
...
}
2.
- retrieveUser() 메소드는 UserDetailsService로부터 UserDetails를 조회하는 역할을 맡고 있다.
- UserDetails는 사용자를 인증하거나 인증된 Authentication 객체를 생성할 때 사용된다.
- loadUserByUsername() 메소드를 통해 UserDetails를 조회한다.
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
...
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
...
}
3. additionalAuthentionChecks() 메소드에서 PasswordEncoder로 사용자의 패스워드를 검증한다.
🌟AbstractUserDetailsAuthenticationProvider와 DaoAuthenticationProvider의 코드 실행 흐름
1. AbstractUserDetailsAuthenticationProvider의 authenticate() 호출
2. DaoAuthenticationProvider의 retrieveUser() 호출
3. DaoAuthenticationProvider의 additionalAuthenticationChecks() 호출
4. DaoAuthenticationProvider의 createSuccessAuthentication() 호출
5. AbstractUserDetailsAuthenticationProvider의 createSuccessAuthentication() 호출
6. 인증된 Authentication을 ProviderManger로 리턴
8️⃣ UserDetails
DB 등의 저장소에 저장된 사용자의 username과 사용자의 자격을 증명하는 credential(password), 사용자의 권한 정보를 포함하는 컴포넌트이다. AuthenticationProvider가 자격 증명을 수행할 때 사용된다.
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities(); // 권한 정보
String getPassword(); // 패스워드
String getUsername(); // Username
boolean isAccountNonExpired(); // 계정 만료 여부
boolean isAccountNonLocked(); // 계정 lock 여부
boolean isCredentialsNonExpired(); // password 만료 여부
boolean isEnabled(); // 계정 활성화 여부
}
9️⃣ UserDetailsService
- UserDetails를 load하는 인터페이스이다.
- 하나의 메소드만 정의되어 있다.
- 사용자의 정보를 어디에서 로드하는지는 애플리케이션에서 사용자 정보를 어디에 보관하고 있느냐에 따라 달라진다.
➡️사용자의 정보가 메모리나 DB 등 어디에서 load되든간에 UserDetails로 리턴해주기만 하면 된다.
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
🔟SecurityContext / SecurityContextHolder
- SecurityContext: 인증된 Authentication 객체를 저장하는 컴포넌트
- SecurityContextHolder: SecurityContext를 관리하는 역할
(Spring Security에서는 SecurityContextHolder에 의해 SecurityContext에 값이 저장되어 있다면 인증된 사용자로 판단)
- SecurityContextHolder에 SecurityContext가 포함되어 있기 때문에, SecurityContextHolder를 통해 인증된 Authentication을 SecurityContext에 설정할 수 있고, SecurityContextHolder를 통해 인증된 Authentication 객체에 접근 가능하다.
'🌿With Spring > Spring Security' 카테고리의 다른 글
Servlet Filter와 Filter Chain (0) | 2022.11.28 |
---|---|
OAuth2 알아보기 (0) | 2022.11.25 |
JWT 알아보기 (0) | 2022.11.23 |
Spring Security의 인가 처리 과정 (0) | 2022.11.22 |
Spring Security의 요청 처리 과정 (0) | 2022.11.21 |

사용자가 로그인 인증을 요청했을 때 spring security에서 해당 요청을 어떻게 처리하는지 도식화한 이미지이다. 일반적으로 ID/Password(spring security에서는 username/password)를 이용해서 로그인하기 때문에 위의 이미지도 ID/Password를 이용한 로그인 인증을 처리하는 상황이다.
1. 가장 먼저 사용자가 로그인 폼(form)을 통해 spring security가 적용된 애플리케이션으로 Id(username)와 password를 포함한 request를 전송한다. 사용자의 요청이 [spring security filter] 영역으로 들어오면 가장 먼저 UsernamePasswordAuthenticationFilter가 요청을 전달받는다.
(Filter가 요청을 가로챈다.)
2. request를 전달받은 UsernamePasswordAuthenticationFilter는 id(username)와 password를 이용해 UsernamePasswordAuthenticationToken을 생성한다.
UsernamePasswordAuthenticationToken는 Authentication 인터페이스를 구현한 클래스이고, 여기서의 Authentication은 아직 인증되지 않은 Authentication이다.
3. UsernamePasswordAuthenticationFilter는 아직 인증되지 않은 Authentication을 AuthenticationManager로 전달한다.
AuthenticationManager는 인증 처리를 총괄하는 매니저 역할을 맡은 인터페이스이고, 이를 구현한 클래스가 ProviderManager 클래스이다.
➡️ 인증 작업을 실질적으로 총괄하는 역할은 ProviderManager
➡️ 인증 작업을 직접 하는 것이 아니라 다른 클래스에게 위임 > AuthenticationProvider
4. AuthenticationProvider는 ProviderManger로부터 Authentication을 전달받는다.
(UsernamePasswordAuthenticationFilter--Token-->AuthenticationManger--Token-->AuthenticationProvider)
AuthenticationProvider는 username으로 DB에서 데이터를 조회하고 비밀번호 일치 여부를 검사하는 방식으로 동작한다.
5. AuthenticationProvider는 UserDetailsService를 이용해 UserDetails를 조회한다.
(UserDetailsService로 조회할 아이디를 전달한다.)
UserDetails는 DB 등의 저장소에 저장된 사용자의 username과 password(Credential), 사용자 권한 정보를 포함하고 있는 컴포넌트이다.
6. UserDetailsService가 DB등의 저장소에서 사용자의 credential을 포함한 사용자 정보를 조회한다.
(UserDetails를 제공해주는 컴포넌트가 UserDetailsService)
7. 조회한 결과를 반환하기 위해 UserDetails를 생성한다.
8. 생성된 UserDetails를 다시 AuthenticationProvider에게 전달한다.
9. AuthenticationProvider는 전달 받은 UserDetails에 포함된 password를 PasswordEncoder를 통해 암호화하고, Authentication에 포함된 비밀번호(입력받은 비밀번호)와 일치하는지 확인한 뒤, 일치한다면 UserDetails를 이용해 인증된 Authentication을 생성하고, 일치하지 않는다면 BadCredentialException을 throw하고 인증 처리를 중단한다.
10. 인증된 Authentication을 ProviderManager에게 전달한다.
[2번 과정]에서의 Authentication은 인증에 필요한 사용자의 로그인 정보를 담고 있고, 여기서의 Authentication은 인증에 성공한 사용자 정보(Principal, Credential, GrantedAuthorities)를 가지고 있다.
11. 인증된 Authentication을 UsernamePasswordAuthenticationFilter에게 전달한다.
12. UsernamePasswordAuthenticationFilter는 SecurityContextHolder를 통해 인증된 Authentication을 SecurityContext에 저장한다.
인증 과정을 어느 정도 이해했으면, 다음은 코드! 코드를 보자.
1️⃣ 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");
...
public UsernamePasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
@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);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
...
}
어김없이 찾아오는 편두통..
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter
1. UsernamePasswordAuthenticationFilter는 AbstractAuthenticationProcessingFilter를 상속한다.
Filter역할을 하려면 doFilter() 메소드가 구현되어 있어야 하는데 여기서는 찾아볼 수 없어서 엥??할 수 있지만 상위 클래스에 구현되어 있어서 문제없다. ==> 사용자의 로그인 request를 doFilter() 메소드가 구현되어 있는 AbstractAuthenticationProcessingFilter가 가장 먼저 전달받는다.
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
2. 클라이언트의 로그인 폼을 통해 전송되는 request 파라미터의 디폴트값은 username과 password이다.
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login","POST");
3. AntPathRequestMatcher는 클라이언트의 URL에 매치되는 matcher이다.
클라이언트의 URL이 [/login]이고 HTTP method가 POST일 경우 매칭될 것이라 예상할 수 있다..
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
4. DEFAULT_ANT_PATH_REQUEST_MATCHER는 AbstractAuthenticationProcessingFilter 클래스에 전달되어 filter가 구체적인 작업을 수행할지 다른 filter를 호출할지 결정할 때 사용된다.
@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);
...
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
...
return this.getAuthenticationManager().authenticate(authRequest);
}
5. username과 password를 이용해 인증을 하는 메소드가 있다.
- attemptAuthentication() 메소드는 상위 클래스인 AbstractAuthenticationProcessingFilter 클래스의 doFilter() 메소드에서 호출된다(Filter에서 어떤 처리의 시작점은 doFilter() 메소드이다).
- HTTP method가 POST가 아니면 AthenticationServiceException을 throw한다.
- 클라이언트가 전달한 username과 password를 통해 UsernamePasswordAuthenticationToken을 생성한다.
(도식화된 인증 흐름을 볼 때 등장한 인증된 Authentication과는 상관없음. 현재 코드에서 만들어지는 토큰은 인증을 하기 위해 필요한 인증 토큰일 뿐!)
- AuthenticationManager를 get해서 authenticate()의 파라미터로 authRequest를 넘겨줌으로써 인증 처리를 위임했다.
2️⃣ AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter 클래스는 HTTP 기반의 인증 요청을 처리하지만, 실질적인 인증 시도는 하위 클래스에게 맡기고, 인증 성공 시 인증된 사용자의 정보를 SecurityContext에 저장하는 역할을 한다.


if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
...
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;
}
1.
- AbstractAuthenticationProcessingFilter 클래스가 인증 처리를 할지 다음 filter를 호출할지 결정한다.
- requiresAuthentication() 메소드는 하위 클래스에서 전달받은 requiresAuthenticationRequestMatcher 객체를 통해서 들어오는 요청이 인증 처리를 해야 하는지 판단한다.
(1️⃣- 3 에서 등장한 AntPathRequestMatcher("/login", "POST")의 파라미터인 URL과 HTTP method가 매칭 조건이다.)
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);
}
2. 하위 클래스(UsernamePasswordAuthenticationFilter)에 인증을 시도해 줄 것을 요청한다(attemptAuthentication()).
3. 인증에 성공하면 successfulAuthentication() 메소드를 호출한다.
exception이 throw될 경우(실패할 경우) unsuccessfulAuthentication() 메소드를 호출한다.

successfulAuthentication() 메소드가 호출되면 SecurityContextHolder를 통해 사용자의 인증정보를 SecurityContext에 저장한 뒤, SecurityContext를 HttpSession에 저장한다.
3️⃣ UsernamePasswordAuthenticationToken
- Spring Security에서 username/password로 인증하기 위해 필요한 토큰이다.
- 인증 성공 후, 인증에 성공한 사용자의 인증 정보를 UsernamePasswordAuthenticationToken에 포함해서 Authentication 객체 형태로 SecurityContext에 저장한다.
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
...
private final Object principal;
private Object credentials;
...
public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
return new UsernamePasswordAuthenticationToken(principal, credentials);
}
public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
}
...
}
1. principal: username 등의 신원
credentials: password
2. unauthenticated() 메소드
인증에 필요한 용도에 맞게 인증되지 않은 UsernamePasswordAuthenticationToken 객체를 생성
3. authenticated() 메소드
인증에 성공한, 이후 SecurityContext에 저장 될 UsernamePasswordAuthenticationToken 객체를 생성
4️⃣ Authentication
Spring Security에서 인증 자체를 표현하는 인터페이스이다.
(UsernamePasswordAuthenticationToken이 AbstractAuthenticationToken 추상 클래스를 상속받고, AbstractAuthenticationToken은 Authentication 인터페이스를 일부 구현했다.)
인증 과정에서 생성되는 토큰과 인증 성공 후 생성되는 토큰 모두 UsernamePasswordAuthenticationToken과 같은 Authentication의 하위 클래스인 형태로 생성된다. 하지만 토큰을 받거나 SecurityContext에 저장되는 경우에는 Authentication 형태로 리턴받거나 저장된다.
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Authorities: AuthenticationProvider에 의해 부여된 사용자 접근 권한 목록.
Credentials: 사용자 인증에 필요한 password. 인증이 이루어지고 난 직후에 ProviderManager가 해당 Credentials를 삭제.
Principal: 사용자를 식별하는 고유정보. username, userDetails 등
5️⃣ AuthenticationManager
인증 처리를 총괄하는 매니저 역할의 인터페이스이다.
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
인증을 위한 실질적인 관리는 AuthenticationManager를 구현하는 클래스를 통해 이루어진다.
6️⃣ ProviderManager
AuthenticationProvider를 관리하고 AuthenticationProvider에게 인증 처리를 위임한다.
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
checkState();
}
...
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
...
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
...
}
...
}
public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent)
1. ProviderManager가 Bean등록 될 때 List<AuthenticationProvider>를 DI 받는다.
for (AuthenticationProvider provider : getProviders())
2. 의존성 주입을 받은 List에서 for문으로 적절한 AuthenticationProvider를 찾는다.
try {
result = provider.authenticate(authentication); // (3)
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
3. AuthenticationProvider를 찾으면 해당 provider에게 인증 처리를 위임한다.
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials(); // (4)
}
...
4. 인증이 정상적으로 되었다면 사용된 Credentials를 제거한다.
7️⃣ AuthenticationProvider
AuthenticationManager로부터 인증 처리를 위임 받아 실질적인 인증 수행을 담당하는 컴포넌트이다.
현재 보고 있는 과정은 username/password 기반의 인증 처리이고, DaoAuthenticationProvider가 담당하고 있다.
DaoAuthenticationProvider는 UserDetailsService로부터 전달받은 Userdetails를 이용해 인증을 수행한다.
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
...
private PasswordEncoder passwordEncoder;
...
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
...
}
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider
1. AbstractUserDetailsAuthenticationProvider는 AuthenticationProvider 인터페이스의 구현 클래스이고, DaoAuthenticationProvider는 구현 클래스를 상속한 확장 클래스이다.
➡️ 실질적인 인증 처리는 AbstractUserDetailsAuthenticationProvider의 authenticate() 메소드부터 시작한다.
protected final UserDetails retrieveUser(..) throws AuthenticationException{
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
...
}
2.
- retrieveUser() 메소드는 UserDetailsService로부터 UserDetails를 조회하는 역할을 맡고 있다.
- UserDetails는 사용자를 인증하거나 인증된 Authentication 객체를 생성할 때 사용된다.
- loadUserByUsername() 메소드를 통해 UserDetails를 조회한다.
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
...
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
...
}
3. additionalAuthentionChecks() 메소드에서 PasswordEncoder로 사용자의 패스워드를 검증한다.
🌟AbstractUserDetailsAuthenticationProvider와 DaoAuthenticationProvider의 코드 실행 흐름
1. AbstractUserDetailsAuthenticationProvider의 authenticate() 호출
2. DaoAuthenticationProvider의 retrieveUser() 호출
3. DaoAuthenticationProvider의 additionalAuthenticationChecks() 호출
4. DaoAuthenticationProvider의 createSuccessAuthentication() 호출
5. AbstractUserDetailsAuthenticationProvider의 createSuccessAuthentication() 호출
6. 인증된 Authentication을 ProviderManger로 리턴
8️⃣ UserDetails
DB 등의 저장소에 저장된 사용자의 username과 사용자의 자격을 증명하는 credential(password), 사용자의 권한 정보를 포함하는 컴포넌트이다. AuthenticationProvider가 자격 증명을 수행할 때 사용된다.
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities(); // 권한 정보
String getPassword(); // 패스워드
String getUsername(); // Username
boolean isAccountNonExpired(); // 계정 만료 여부
boolean isAccountNonLocked(); // 계정 lock 여부
boolean isCredentialsNonExpired(); // password 만료 여부
boolean isEnabled(); // 계정 활성화 여부
}
9️⃣ UserDetailsService
- UserDetails를 load하는 인터페이스이다.
- 하나의 메소드만 정의되어 있다.
- 사용자의 정보를 어디에서 로드하는지는 애플리케이션에서 사용자 정보를 어디에 보관하고 있느냐에 따라 달라진다.
➡️사용자의 정보가 메모리나 DB 등 어디에서 load되든간에 UserDetails로 리턴해주기만 하면 된다.
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
🔟SecurityContext / SecurityContextHolder
- SecurityContext: 인증된 Authentication 객체를 저장하는 컴포넌트
- SecurityContextHolder: SecurityContext를 관리하는 역할
(Spring Security에서는 SecurityContextHolder에 의해 SecurityContext에 값이 저장되어 있다면 인증된 사용자로 판단)
- SecurityContextHolder에 SecurityContext가 포함되어 있기 때문에, SecurityContextHolder를 통해 인증된 Authentication을 SecurityContext에 설정할 수 있고, SecurityContextHolder를 통해 인증된 Authentication 객체에 접근 가능하다.
'🌿With Spring > Spring Security' 카테고리의 다른 글
Servlet Filter와 Filter Chain (0) | 2022.11.28 |
---|---|
OAuth2 알아보기 (0) | 2022.11.25 |
JWT 알아보기 (0) | 2022.11.23 |
Spring Security의 인가 처리 과정 (0) | 2022.11.22 |
Spring Security의 요청 처리 과정 (0) | 2022.11.21 |