Spring Security
- Spring 기반 애플리케이션을 위해 선언적 보안 기능을 제공하는 보안 프레임워크
- Servlet Filter 및 AOP 기반
인증 관련 Architecture 설명 (로그인 인증)
- 사용자가 아래와 같은 방식으로 로그인을 요청을 했다고 가정을하자.
1
2
3
4
{
"username": "admin",
"password": 12345
}
1. Http Request -> AbstractAuthenticationProcessingFilter
- 사용자가 인증 요청이 들어오면 AuthenticationFilter 를 들리게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
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);
//...
this.successfulAuthentication(request, response, chain, authenticationResult);
} catch (InternalAuthenticationServiceException var5) {
//...
}
}
}
public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException;
}
- 인증이 필요하기 때문에 else 문으로 들어가게 되고 attemptAuthentication 를 호출한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
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 : "admin"
username = username != null ? username.trim() : "";
String password = this.obtainPassword(request); // password : "password"
password = password != null ? password : "";
// 토큰 생성
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
}
- 일반 로그인 요청이기 때문에 attemptAuthentication 의 구현체인 UsernamePasswordAuthenticationFilter 가 처리한다.
- username , password 를 설정하고 UsernamePasswordAuthenticationToken 에 인증받기 전 토큰 생성을 요청한다.
2. UsernamePasswordAuthenticationFilter -> UsernamePasswordAuthenticationToken
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private Object credentials;
//생성자
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection) null); // 상위 클래스인 AbstractAuthenticationToken 에 authorities = null 설정 즉, 아직 인가정보가 없기 때문에 null 로 설정.
this.principal = principal; // principal : admin (아이디)
this.credentials = credentials; // credentials : 12345 (패스워드)
this.setAuthenticated(false); // 인증 처리전 이기 떄문에 false 로 설정
}
//filter 에서 호출하는 메서드
public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
return new UsernamePasswordAuthenticationToken(principal, credentials);
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);//AbstractAuthenticationToken 초기 설정
}
}
- filter 에서 요청한 메서드 unauthenticated(Object principal, Object credentials) 를 통해 생성자 호출
- 생성자를 통해 token 을 생성
1
2
3
4
5
6
7
8
9
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer { // token 은 보다시피 Authentication 구현체 이다.
private final Collection<GrantedAuthority> authorities; // size = 0 즉, 없다.
private Object details; // null
private boolean authenticated; // false;
public void setAuthenticated(boolean authenticated) {
this.authenticated = authenticated;
}
}
- 상위 클래스 AbstractAuthenticationToken 에 있는 필드를 null 또는 empty 로 초기 설정을 한다.
1
2
3
4
5
6
7
8
9
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// ...
// 토큰 생성
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
- token 을 받아온 filter 는 상위 클래스인 AbstractAuthenticationProcessingFilter 의 getAuthenticationManager 메서드를 통해 AuthenticationManager 를 받아와서 authenticate 메서드 인자로 토큰을 전달 한다.
3. UsernamePasswordAuthenticationFilter -> AuthenticationManager (ProviderManager)
1
2
3
4
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
- AuthenticationManager 는 인터페이스 이고 구현체인 ProviderManager 에서 authenticate 를 실행한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
public List<AuthenticationProvider> getProviders() {
return providers;
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//while 문으로 모든 provider 를 순회하여 처리하고 result 가 나올 때까지 반복한다.
while (var9.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider) var9.next();
if (provider.supports(toTest)) {
//...
try {
// Provider 의 authenticate 메서드 호출
result = provider.authenticate(authentication);
//...
} catch (InternalAuthenticationServiceException | AccountStatusException var14) {
//...
}
}
}
//...
if (result != null) {
//...
return result;
}
}
}
- manager 는 provider 를 순회하면서 result 를 받아오는 역할만 하고 실 인증 처리는 provider 가 한다.
- AuthenticationProvider 의 authenticate 메서드를 통해 token 을 전달한다.
4. AuthenticationManager (ProviderManager) -> AuthenticationProvider(s)
1
2
3
4
5
6
7
8
public interface AuthenticationProvider {
// 인증 전의 token 을 받아서 인증된 Authentication 객체를 반환
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
- Manager 에 의해 호출되었을때 AuthenticationProvider 의 구현체인 AbstractUserDetailsAuthenticationProvider 의 authenticate 메서드가 호출된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
});
String username = this.determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//this.retrieveUser
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); //load
} catch (UsernameNotFoundException var6) {
//...
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
//...
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
}
- AbstractUserDetailsAuthenticationProvider 에서 this.retrieveUser메서드를 자식 클래스인 DaoAuthenticationProvider 에서 해당 메서드를 처리한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
//loadUserByUsername()
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
//...
}
//...
}
}
- loadUserByUsername() 메서드를 호출하여 UserDetailsService 에 UserDetails 를 요청한다.
5. AuthenticationProvider -> UserDetailsService
1
2
3
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
- getUserDetailsService() 를 호출하여 UserDetailsService 가 호출된다.
6. UserDetailsService -> UserDetails
- UserDetailsService 를 구현체가 해당 loadUserByUsername 을 호출하여 DB에 조회하여 UserDetails 를 생성한다.
7. UserDetailsService -> AuthenticationProvider
1
2
3
4
5
6
7
8
9
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//service 로 부터 받은 UserDetails
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
//... 인증 로직
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
}
- UserDetails 를 받아 인증을 시도한 이후 인증이 성공하면 인증이 완료된 Authentication 을 반환한다.
8. AuthenticationProvider(s) -> AuthenticationManager(ProviderManager)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Authentication result = null;
//while 문으로 모든 provider 를 순회하여 처리하고 result 가 나올 때까지 반복한다.
while (var9.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider) var9.next();
// Provider 의 authenticate 메서드 호출
result = provider.authenticate(authentication);
}
//...
if (result != null) {
// 1~8 단계를 통해 받아온 Authentication
return result;
}
}
}
- 모든 provider 의 인증을 통과한 Authentication 을 반환한다.
9. AuthenticationManager -> AuthenticationFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
try {
// 1~9 단계를 통해 받아온 Authentication
Authentication authenticationResult = this.attemptAuthentication(request, response);
//최종적으로 인증을 성공한 Authentication 을 등록
this.successfulAuthentication(request, response, chain, authenticationResult);
} catch (InternalAuthenticationServiceException var5) {
//...
}
}
}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authResult);// 인증 받은 Authentication
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);// successHandler
}
}
- 인증이 완료된 Authentication 를 SecurityContextHolder 에 전달
10. AuthenticationFilter -> SecurityContextHolder
- 인증에 성공한 Authentication 을 비어있는 SecurityContext 에 저장을 하고 Context 를 SecurityContextRepository 에 저장한다.
- 이후 successHandler 에서 인증 받은 Authentication 을 이용하여 request 응답을 한다.