Home Security
Post
Cancel

Security

Spring Security

  • Spring 기반 애플리케이션을 위해 선언적 보안 기능을 제공하는 보안 프레임워크
  • Servlet Filter 및 AOP 기반

인증 관련 Architecture 설명 (로그인 인증)

  • 사용자가 아래와 같은 방식으로 로그인을 요청을 했다고 가정을하자.
1
2
3
4
  {
  "username": "admin",
  "password": 12345
}

1. Http Request -> AbstractAuthenticationProcessingFilter

  1. 사용자가 인증 요청이 들어오면 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;
}
  1. 인증이 필요하기 때문에 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);
    }
  }
}
  1. 일반 로그인 요청이기 때문에 attemptAuthentication 의 구현체인 UsernamePasswordAuthenticationFilter 가 처리한다.
  2. 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 초기 설정
  }
}
  1. filter 에서 요청한 메서드 unauthenticated(Object principal, Object credentials) 를 통해 생성자 호출
  2. 생성자를 통해 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;
  }
}
  1. 상위 클래스 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);
  }
}
  1. token 을 받아온 filter 는 상위 클래스인 AbstractAuthenticationProcessingFilter 의 getAuthenticationManager 메서드를 통해 AuthenticationManager 를 받아와서 authenticate 메서드 인자로 토큰을 전달 한다.

    3. UsernamePasswordAuthenticationFilter -> AuthenticationManager (ProviderManager)

1
2
3
4
public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

  1. 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;
    }
  }
}
  1. manager 는 provider 를 순회하면서 result 를 받아오는 역할만 하고 실 인증 처리는 provider 가 한다.
  2. 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);

}
  1. 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);
    }
}
  1. 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) {
            //...
        }
        //...
    }
}
  1. 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

  1. 인증에 성공한 Authentication 을 비어있는 SecurityContext 에 저장을 하고 Context 를 SecurityContextRepository 에 저장한다.
  2. 이후 successHandler 에서 인증 받은 Authentication 을 이용하여 request 응답을 한다.

추가로 공부해야할 부분

  • SecurityContextHolder 종류
    • MODE_GLOBAL, MODE_INHERITABLETHREADLOCAL 등의 다른 방식도 지원

This post is licensed under CC BY 4.0 by the author.