Spring Security의 구조
- 사용자가 로그인 정보와 함께 인증을 요청한다.
- AuthenticationFilter가 요청을 가로 챈 뒤, UsernamePasswordAuthenticationTocken의 인증용 객체를 생성한다.
- AuthenticationManger의 구현체인 ProviderManager에게 생성한 인증 토큰을 전달한다.
- AuthenticationManager는 등록된 AuthenticationProvider를 조회하여 인증을 시도한다.
- 실제 DB에서 사용자 인증정보를 가져오는 UserDeatailService에 사용자 정보를 전달한다.
- 넘겨받은 사용자 정보를 토대로 DB에서 찾은 사용자 정보인 UserDatails 객체를 만든다.
- AuthenticationProvider는 UserDetails를 넘겨받고 사용자 정보를 비교한다.
- 인증이 완료되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환한다.
- 다시 최초의 AuthenticationFilter에 Authentication 객체가 반한된다.(인증완료)
- Authentication 객체를 SecurityContext에 저장한다.
최종적으로 SecurityContextHolder는 세션 영역에 있는 SecurityContext에 Authentication 객체를 저장한다. 사용자 정보를 저장한다는 것은 Spring Security가 전통적인 세션-쿠키 기반의 인증 방식을 사용한다는 것을 의미한다.
이모든 과정을 거치면 Dispacher Servlet으로 요청을 넘긴다.
여기서 주의깊게 봐야할 부분은 UserDetailsService와 UserDetails이다. 실질적인 인증과정은 사용자가 입력한 데이터와 UserDetailsService의 loadUserByUsername()메소드가 반환하는 UserDetails 객체를 비교함으로써 동작한다. 따라서 UserDetailsService와 UserDetails 구현을 어떻게 하느냐에 따라 인증의 세부과정이 달라진다.
Spring Security의 주요모듈
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter는 일반적으로 로그인 폼에서 제출되는 Username과 Password를 통한 인증을 처리하는 Filter이다. UsernamePasswordAuthenticationFilter는 클라이언트로부터 전달받은 Username과 Password를 Spring Security가 인증 프로세스에서 이용할 수 있도록 UsernamePasswordAuthenticationToken을 생성한다.
// AbstractAuthenticationProcessingFilter -> doFilter() 메서드를 포함함
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";
//DEFAULT_ANT_PATH_REQUEST_MATCHER는 filter가 구체적인 작업을 수행할지 특별한 작업없이 다른 filter를 호출하지 결정하는데 사용
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login","POST");
...
...
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
// AbstractAuthenticationProcessingFilter에 해당 파라미터 전달
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
// 인증 시도
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
// Post가 아니면 익셉션 발생
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 생성, 인증을 하기 위해 필요한 인증 토큰
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
...
return this.getAuthenticationManager().authenticate(authRequest); // AuthenticationManager에게 인증 처리 위임
}
...
...
}
AbstractAuthenticationProcessingFilter
UsernamePasswordAuthenticationFilter가 상속하는 클래스이다. Spring Security에서 제공하는 Filter중 하나.
AbstractAuthenticationProcessingFilter는 인증 요청을 처리하지만, 실질적인 인증 시도는 하위클래스에 맡기고, 인증에 성공하면 인증된 사용자의 정보를 SecurityContext에 저장하는 역할을 한다.
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
...
...
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
// doFilter 메서드를 총해 Filter라는 것을 알 수 있다.
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 인증 처리를 해야 하는지, 다음 Filter를 호출하지 여부를 결정
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
//UsernamePasswordAuthenticationFilter에 인증을 시도해줄 것을 요청
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;
}
...
...
// SecurityContextHolder를 통해 사용자의 인증 정보를 SecurityContext에 저장한 뒤, SecurityContext를 HttpSession에 저장
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.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);
}
// SeucurityContext를 초기화하고, AuthenticationFailureHandler를 호출
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.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);
}
...
...
}
Authentication
인증 자체를 표현하며, 현재 접근하는 주체의 정보와 권한을 담는 인터페이스. Authentication 객체는 SecurityContext에 저장되며, SecurityContextHolder를 통해 SecurityContext에 접근하고, SecurityContext를 통해 Authentication에 접근할 수 있다.
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials(); // AuthenticationProvider에 부여되는 사용자의 접근권한 목록
// 일반적으로 GrantedAuthority 인터페이스 구현 클래스는 SimpleGrantedAuthority
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
@GetMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) throws Exception {
// Authentication 객체에 접근
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
new SecurityContextLogoutHandler().logout(request, response, auth);
}
return "redirect:/";
}
UsernamePasswordAuthenticationToken
Authentication의 구현체인 AbstractAuthenticationToken의 하위 클래스이다. User의 ID가 Principal 역할을 하고, Password가 Credential의 역할을 한다. 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);
}
// 인증에 성공한 이후 SecurityContext에 저장될 객체를 생성
public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
}
...
...
}
AuthenticationManager
인증에 대한 부분은 AuthenticationManager를 통해서 처리하게 되는데, 실질적으로는 AuthenticationManger에 등록된 AuthenticationProvider에 의해 처리된다. 인증에 성공하면 객체를 생성하여 SecurityContext에 저장한다.
public interface AuthenticationManager {
// 인증을 위한 Filter는 AuthenticationManager를 통해 느슨한 결합을 유지
// 인증을 위한 실질적인 관리는 AuthenticationManager를 구현한 구현 클래스를 통해 이루어짐
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
AuthenticationProvider
실제 인증에 대한 부분을 처리하는데, 인증 전의 Authentication 객체를 받아서 인증이 완료된 객체를 반환하는 역할을 한다. Username/Password 기반의 인증 처리는 DaoAuthenticationProvider가 담당하고 있으며, DaoAuthenticationProvider는 UserDetailsService로부터 전달받은 UserDetails를 이용해 인증을 처리한다.
public class HelloUserAuthenticationProvider implements AuthenticationProvider {
private final HelloUserDetailsServiceV3 userDetailsService;
private final PasswordEncoder passwordEncoder;
public HelloUserAuthenticationProvider(HelloUserDetailsServiceV3 userDetailsService,
PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
@Override // 인증 처리 로직 작성
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken authToken = (UsernamePasswordAuthenticationToken) authentication;
String username = authToken.getName();
Optional.ofNullable(username).orElseThrow(() -> new UsernameNotFoundException("Invalid User name or User Password"));
try {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String password = userDetails.getPassword();
verifyCredentials(authToken.getCredentials(), password);
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
return UsernamePasswordAuthenticationToken.authenticated(username, password, authorities);
} catch (Exception ex) {
throw new UsernameNotFoundException(ex.getMessage());
}
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
private void verifyCredentials(Object credentials, String password) {
if (!passwordEncoder.matches((String)credentials, password)) {
throw new BadCredentialsException("Invalid User name or User Password");
}
}
}
ProviderManager
AuthenticationManager를 구현한 ProviderManager는 AuthenticationProvider를 구성하는 목록을 갖는다. ProviderManager는 이름에서 유추할 수 있듯이 AuthenticationProvider를 관리하고, AuthenticationProvider에게 인증 처리를 위임하는 역할을 한다.
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
...
// List<AuthenticationProvider> 객체를 DI 받는다.
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();
//DI 받는 리스트를 이용해 적절한 AuthenticationProvider를 찾는다.
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 {
// 적절한 AuthenticationProvider를 찾았다면 인증 처리를 위임한다.
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)) {
인증이 정산적으로 처리되면, 인증에 사용된 Credentials를 제거한다.
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
...
...
}
...
...
}
UserDetailsService
UserDetails 객체를 반환하는 하나의 메소드만을 가지고 있다. 일반적으로 이를 구현한 클래스에 UserRepository를 주입받아 DB와 연결하여 처리한다.
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetails
인증에 성공하여 생성된 UserDetails 객체는 Authentication 객체를 구현한 UsernamePasswordAuthenticationToken을 생성하기 위해 사용된다. UserDetails를 구현하여 처리한다.
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities(); // 권한 정보
String getPassword(); // 패스워드
String getUsername(); // Username
boolean isAccountNonExpired(); // 계정 만료 여부
boolean isAccountNonLocked(); // 사용자 계정의 lock여부
boolean isCredentialsNonExpired(); // Credentials(Password)의 만료여부
boolean isEnabled(); // 사용자의 활성화 여부
}
SecurityContextHolder
보안 주체의 세부 정보를 포함하여 응용 프로그램의 현재 보안 컨텍스트에 대한 세부정보가 저장된다. SecurityContext를 관리하는 역할을 한다.SecurityContextHolder에 의해 SecurityContext에 값이 채워져 있다면 인증된 사용자로 간주한다.
public class SecurityContextHolder {
...
...
// 전략을 의미. SecurityContextHolder의 기본 전략은 ThreadLocalSecurityContextHolderStrategy이다.
// 이 전략은 현재 실행 스레드에 SecurityContext를 연결하기위해 ThreadLocal을 사용한다는 전략이다.
private static SecurityContextHolderStrategy strategy;
...
...
// SecurityContext를 얻는 메서드
public static SecurityContext getContext() {
return strategy.getContext();
}
...
...
// SecurityContext를 연결한다, 인증된 Authentication을 포함한
// SecurityContext를 현재 실행 스레드에 연결하는 데 사용된다.
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
...
...
}
SecurityContext
Authentication을 보관하는 역할을 하며, SecurityContext를 통해 Authentication을 저장하거나 꺼내올 수 있다.
GrantedAuthortiy
현재 사용자가 가지고 있는 권한을 의미한다. ROLE_ADMIN, ROLE_USER와 같이 ROLE_* 형태로 사용된다. GrantedAuthortiy객체는 UserDeatailsService에 의해 불러올 수 있고, 특정 자원에 대한 권한이 있는지를 검사하여 접근 허용 여부를 결정한다.
Reference
https://dev-coco.tistory.com/174
코드스테이츠 학습 자료
'Spring > 개념' 카테고리의 다른 글
[Spring] 스프링 PSA (0) | 2023.05.11 |
---|---|
[Spring] 스프링 AOP (0) | 2023.05.11 |
[Spring Security] 스프링 시큐리티와 FilterChain (1) | 2023.05.11 |
[Spring] Spring 기초 및 모듈 구성, Sprig VS SpringBoot (0) | 2023.05.02 |
[Spring] Filter, Interceptor, AOP의 차이점에 대해 (0) | 2023.04.10 |