본문 바로가기

Language/Spring Security

인증 - Authentication

시큐리티 인증 / 인가 흐름도

  • Servlet Filter
    • DelegationFilterProxy
      • 최초 요청을 에서 받아 스프링 컨테이너에 있는 FilterChainProxy에게 요청 전달
    • FilterChainProxy
      • 넘겨받은 요청을 여러 필터에 전달
      • 각 필터는 해당 요청을 처리하고 AuthenticationFilter로 연결
  • Authentication
    • AuthenticationFilter
      • 인증 처리 수행
      • 인증 성공 시 Authentication 객체를 생성하여 AuthenticationManager에게 전달
    • AurthenticationManager
      • AuthenticationProvider에게 인증 위임
      • AuthenticationProvider는 사용자 인증 정보를 확인
        • UserDetailsService를 통해 사용자 정보 조회
        • UserDetailsService는 최종적으로 UserDetails 객체 반환
    • AuthenticationProvider
      • PasswordEncoder를 사용하여 비밀번호가 일치하는 지 확인
      • 비밀번호 일치 시 Authentication 객체 생성
        → 인증 객체 ⇒ 유저 객체와 권한 정보를 저장
      • AuthenticationManager에게 Authentication 객체 반환
    • SecurityContextHolder
      • 최종적으로 AuthenticationFilter는 SecurityContextHolder에 Authentication 객체를 저장

Authentication

  • 특정 자원에 접근하려는 사람의 신원을 확인하는 방법을 의미
    → 너는 누구냐?
  • 사용자 인증의 일반적인 방법은 사용자 이름과 비밀번호를 입력하게 하는 것으로서 인증이 수행되면
    신원을 알고(인증) 권한 부여(인가) 가능
  • 사용자의 인증 정보를 저장하는 토큰 개념의 객체로 활용되며 인증 이후 SecurityContext 에 저장되어 전역적으로 참조 가능

구조

  • getPrincipal() : 인증 주체를 의미하며 인증 요청의 경우 사용자 이름을, 인증 후 에는 UserDetails 타입의 객체가 될 수 있음
  • getCredentials() : 인증 주체가 올바른 것을 증명하는 자격 증명으로서 대개 비밀번호를 의미
  • getAuthorities() : 인증 주체(principal)에게 부여된 권한을 나타냄
  • getDetails() : 인증 요청에 대한 추가적인 세부 사항을 저장한다. IP 주소, 인증서 일련 번호 등
  • isAuthenticated() : 인증 상태 반환
  • setAuthenticated(boolean) : 인증 상태를 설정

인증 절차 흐름

  1. 클라이언트가 id,pw를 입력하여 로그인 시도
  2. AuthenticationFilter가 사용자가 입력한 정보로 Authentication 객체 생성하여 AuthenticationManager에게 전달
  3. 인증 처리 성공 시 Authentication 객체를 다시 생성하여 아래의 정보를 저장하여 반환
    • principal ⇒ 시스템에서 가지고 온 사용자 정보가 저장(유저 정보)
    • credentials ⇒ 비밀번호는 노출되면 안되므로 null 상태
    • authorities ⇒ 사용자에게 부여된 권한(GrantedAuthority 타입의 컬렉션 제공)
  4. AuthenticationFilter는 최종 인증 결과를 받아 SecurityContextHolder의 SecurityContext에 저장

코드 흐름 정리

사용자 username, passowrd 입력

  • UsernamePasswordAuthenticationFilter의 attemptAuthentication() 메서드 실행
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 = username != null ? username.trim() : "";
        String password = this.obtainPassword(request);
        password = password != null ? password : "";
        UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
        this.setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

  • 사용자가 입력한 username, password를 이용하여 UsernamePasswordAuthenticationToekn 객체 생성
public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
    return new UsernamePasswordAuthenticationToken(principal, credentials);
}

  • UsernamePasswordAuthenticationToken 생성자
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
    super((Collection)null); // 권한 X
    this.principal = principal; // username
    this.credentials = credentials; // password
    this.setAuthenticated(false); // 인증 false
}

 


  • this(UsernamePasswordAuthenticationFilter).getAuthenticationManager().authenticate(authRequest)
    → AuthenticationManager의 authenticate() 메서드
    → ProviderManager의 authenticate() 메서드
    → ProviderManager ⇒ AuthenticationManager 인터페이스의 구현체
    • provider.authenticate() 메서드에게 위임
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		...
    while(var9.hasNext()) {
        AuthenticationProvider provider = (AuthenticationProvider)var9.next();
        ...
            try {
                result = provider.authenticate(authentication);
								...

  • DaoAuthenticationProvider.retrieveUser() 메서드
    • AbstractUserDetailsAuthenticationProvider.authenticate() 메서드에서 이동
    • this.getUserDetailsService().loadUserByUsername(username)의 UserDetailsService는 빈으로 선언해둔 것
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
  this.prepareTimingAttackProtection();

  try {
      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) {
      this.mitigateAgainstTimingAttack(authentication);
      throw var4;
  } catch (InternalAuthenticationServiceException var5) {
      throw var5;
  } catch (Exception var6) {
      throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
  }
}

  • SecurityConfig에서 선언한 Bean
@Bean
public UserDetailsService userDetailsService(){
    UserDetails user = User.withUsername("user").password("{noop}1111").roles("USER").build();
    return  new InMemoryUserDetailsManager(user);
}

 

  • 여기까지가 username을 가지고 사용자 정보를 가지고옴 ⇒ 이제 password와 검증

  • DaoAuthenticationProvider의 additionalAuthenticationChecks() 메서드
    • 비밀번호가 일치한 지 확인
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    if (authentication.getCredentials() == null) {
        ...
    } else {
        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"));
        }
    }
}

 


  • AbstractUserDetailsAuthenticationProvider의 createSuccessAuthentication() 메서드
    • 유저 정보, 비밀번호, 권한 정보를 담아 생성한 인증 객체 반환
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
    UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
    result.setDetails(authentication.getDetails());
    this.logger.debug("Authenticated user");
    return result;
}

 


  • 인증 성공한 인증 객체를 반환 받아 AbstractAuthenticationProcessingFilter의 successfulAuthentication() 메서드에서 SecurityContext에 저장
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
    SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
    context.setAuthentication(authResult);
    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);
}