[Spring Security] 인증

4. 인증

4-1. 개요

인증 (Authentication)

  • 식별(Identification)에 대한 주장이 사실인지 검증하는 과정
  • 신분을 주장하는 주체가 리소스에 접근하는 주체가 본인인지 확인받기 위해 사용하는 모든 수단과 방법을 포함한다.
  • 웹 애플리케이션에서 인증은 주로 주체가 알고 있는 정보를 기반으로 수행된다.

Servlet 기반 웹 애플리케이션에서의 인증

  • Username/Password 방식
  • OpenID Connect 기반의 OAuth 2.0 로그인
  • 중앙 인증 서버를 통한 인증

Username and Password 기반 인증 방식

  • 스프링 시큐리티는 HttpServletRequest 객체로부터 username과 password를 취득하여 인증을 수행할 수 있는 내장 기능을 제공한다.
  • 개발자는 스프링 시큐리티의 인증 프로세스를 사용하여 인증을 간편하게 구현할 수 있다.
  • 세부 처리 절차
    • HTTP Basic 인증
      • 클라이언트가 서버에 요청할 때 Authorization 헤더에 username과 password를 Base64로 인코딩하여 전달하는 방식
    • Form 기반 인증
      • 로그인 폼을 사용하여 username과 password를 제출하고, 서버는 이를 검증하여 인증을 수행하는 방식입니다.
    • Digest 인증
      • 클라이언트가 서버에 요청할 때, 암호화된 방식으로 username과 password를 전달한다.
      • 보안 상의 이유로 모던 웹 애플리케이션 개발에서는 사용하지 않는 것이 권장된다.

4-2. 인증 프로세스

image

  • 인증을 수행하는 메서드 호출
    • AbstractAuthenticationProcessingFilter.java
    • doFilter에서 인증을 수행하는 메소드를 호출하게 되는데, 이 때 사용되는 클래스가 UsernamePasswordAuthenticationFilter
      void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
      // Attempt an authentication process
      Authentication authenticationResult = attemptAuthentication(request, response);
      // ...
      }
    
  • UsernamePasswordAuthenticationFilter.java
    • 아직 인증되지 않은 인증 처리 용도의 임시 토큰을 생성한다.
    • 인증 매니저에게 생성한 토큰을 전달하면서 실제 인증 처리를 요청한다.
    • username, password라는 parameter를 request에서 받아 온 후, 새로 만든 Authentication 객체에 두 개의 값을 담아 AuthenticationManager로 넘긴다.
      Authentication attemptAuthentication(HttpServletRequest request,
                         HttpServletResponse response) {
      // retrieve Username, password value from HttpServletRequest..
        
      // Create a token that is not yet authenticated
      UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
       password);    
    
      // call AuthenticationManager.authenticate(authRequest);
      return this.getAuthenticationManager().authenticate(authRequest);    
      }
    
  • AuthenticationManager
    • AuthenticationManager는 인터페이스이며, ProviderManager 구현체를 사용한다.
    • 실제 인증 처리를 담당할 Provider를 list로 가지고 있다.
    • 인증 처리 요청을 받은 인증 매니저는 각 인증 방식에 적합한 실제 인증을 수행할 Provider를 찾고, 해당 Provider에게 인증 처리 작업을 위임한다.
      Authentication authenticate(Authentication authentication) {
      for (AuthenticationProvider provider: getProviders( )) {
        ...
          Authentication result = provider.authenticate(authentication);
         // from AbstractUserDetailsAuthenticationProvider.java
        ...
      }
      }
    
  • AuthenticationProvider
    • 실제 인증 처리 작업을 담당한다.
    • retrieveUser(username, authentication) : username으로 사용자를 조회해 UserDetails 객체를 받아온다.
    • additionalAuthenticationChecks : password와 조회한 UserDetails의 password의 일치여부를 체크한다.
      Authentication authenticate(Authentication authentication) {
      ...
      UserDetails user = retrieveUser(username, authentication);
      additionalAuthenticationChecks(user, authentication);
      }
    
  • retrieveUser(username, authentication)
    • loadUserByUsername(username)을 통해 DB에서 username에 해당하는 User 정보를 조회한다.
      UserDetails retrieveUser(username, authentication) {
      ...
      UserDetails loadedUser =
        UserDetailsService.loadUserByUsername(username);
        
      return loadedUser;
      }
    
  • DaoAuthenticationProvider.java
    • additionalAuthenticationChecks(user, authentication)를 통해 DB에서 조회된 User의 비밀번호(Hashed value)가 인증 토큰에 들어있는 비밀번호와 일치하는 지 검증한다.
      • PasswordEncoder를 사용하여 이전 단계에서 반환된 UserDetails의 암호를 확인한다.
      void additionalAuthenticationChecks(UserDetails userDetails,
       UsernamePasswordAuthenticationToken authentication) {
      ...
      String presentedPassword = authentication.getCredentials().toString();
      if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
        logger.debug("Failed to authenticate since password does not match stored value");
           throw new BadCredentialsException(...);
      }
      }
    
  • BCryptPasswordEncoder.java
    • 제출된 인코딩 되지 않은 패스워드와 인코딩 된 패스워드의 일치 여부를 확인한다.
      boolean matches(CharSequence rawPassword,String encodedPassword) {
      // Verify that the raw password matches the encoded password
      return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
      }
    
  • AbstractUserDetailsAuthenticationProvider.java
    • 비밀번호 검증까지 통과했을 경우, createSuccessAuthentication 메서드를 통해 인증이 성공한 토큰을 생성하는 작업을 수행한다.
    • 매개변수로 Authentication 객체와 UserDetails 객체를 담아서 호출한다.
      Authentication authenticate(Authentication authentication) {
      ...
      UserDetails user = retrieveUser(username, authentication);
      ...
      additionalAuthenticationChecks(user, authentication);
        
      // create Authenticated Token with Authentication type
      return createSuccessAuthentication(Object principal,
                        Authentication authentication,
                        UserDetails user);
      }    
    
  • ProviderManager.java
    • 인증 처리가 된 Authentication 객체가 AuthenticationProvider의 최종 반환값이 된다. (인증 완료된 토큰)
    • eraseCredentials() : 토큰에서 비밀 정보를 지운다.
      Authentication authenticate(Authentication authentication) {
      for (AuthenticationProvider provider: getProviders( )) {
        Authentication result = provider.authenticate(authentication);
      }
      // erase Credentials info
      eraseCredentials( );
        
      return result;
      }
    
  • UsernamePasswordAuthenticationFilter.java
    • 인증 처리가 완료된 인증 토큰을 응답받아서, AbstractAuthenticationProcessingFilter에 반환한다.
      Authentication attemptAuthentication(HttpServletRequest request,
                         HttpServletResponse response) {
      ...
      UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
       password);
        
      return this.getAuthenticationManager().authenticate(authRequest);
      }
    
  • AbstractAuthenticationProcessingFilter.java
    • 인증 수행 메서드 호출
      void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
      // Attempt an authentication process
        Authentication authenticationResult =
        attemptAuthentication(request, response);
      ...
      }
    
  • successfulAuthentication() 메서드 호출
    • attemptAuthentication(..) 메서드로 부터 응답받은 Authentication 토큰을 SecurityContext에 저장한다.
      void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
      // retrieve Username, password value from HttpServletRequest..
      Authentication authenticationResult =
        attemptAuthentication(request, response);
      ...
      // Saving the authentication token to the SecurityContext
      successfulAuthentication(request, response, chain, authenticationResult);
      }
    
  • AbstractAuthenticationProcessingFilter.java
    • 인증에 성공한 토큰을 SecurityContext에 담는다.
      void successfulAuthentication(HttpServletRequest request,
                      HttpServletResponse response, 
                    FilterChain chain, Authentication authResult) {
          SecurityContext context =
        this.securityContextHolderStrategy.createEmptyContext( );
          context.setAuthentication(authResult);
          this.securityContextHolderStrategy.setContext(context);
          this.securityContextRepository.saveContext(context, request, response);
      ...
          this.successHandler.onAuthenticationSuccess(request, response, authResult);
      }
    
  • onAuthenticationSuccess(..)
    • SecurityContext에 인증 토큰이 저장되면, 다음 Security Filter 작업을 수행하기 위해 FilterChain의 doFilter 메서드를 호출한다.
      default void onAuthenticationSuccess(HttpServletRequest request,
                        HttpServletResponse response, 
                        FilterChain chain,
                        Authentication authentication)  {
      onAuthenticationSuccess(request, response, authentication);
      chain.doFilter(request, response);
      }
        
      void onAuthenticationSuccess(HttpServletRequest request,
                      HttpServletResponse response,
                      Authentication authentication);    
    

4-3. SecurityContext, SecurityContextHolder

SecurityContextHolder

  • SecurityContext를 관리하는 객체
  • 현재 실행 중인 스레드와 연동하여 인증된 사용자 정보를 저장한다.

SecurityContext

  • 인증된 사용자의 상세정보를 보관하는 객체
  • Authentication 객체로 표현되며, 사용자가 로그인한 후에 생성된다.
  • 인증 수행 전에는 비어있는 상태의 SecurityContext가 SecurityContextHolder에 저장된다.
  • 인증 완료 후에는 인증된 사용자의 Authentication 정보가 SecurityContext에 저장된다.

4-4. Authentication

Authentication 인터페이스

  • 현재 인증된 사용자의 자격 증명 정보를 저장한다.
  • 인증이 성공하면 SecurityContext에 저장된다.

구성요소

  • Principal (접근 주체)
    • 서비스나 리소스에 접근하는 사용자
    • Username/Password 기반 인증에서는 주로 UserDetails 객체가 사용된다.
  • Credentials (비밀 정보)
    • 주로 비밀번호를 나타낸다.
    • 인증이 완료된 후에는 보안상의 이유로 비밀번호 정보는 가려지거나 지워진다.
  • Authorities (권한)
    • 해당 사용자가 가진 권한 정보
    • 사용자가 시스템에서 어떤 작업을 수행할 수 있는 지를 나타낸다.
    • Collection 타입으로 여러 권한을 가질 수 있다.
    • 권한 정보는 GrantedAuthority 인터페이스로 관리된다.

GrantedAuthority

  • 사용자의 개별 권한 정보를 담고 있는 인터페이스
  • 사용자가 특정 리소스에 접근할 수 있는 지의 여부를 확인한다.

      public interface GrantedAuthority extends Serializable {  
          String getAuthority(); // 권한 이름
      }
    

4-5. AuthenticationManager

AuthenticationManager 인터페이스

  • 필터를 통해 들어오는 인증 요청을 처리하고, 인증이 성공 또는 실패했는 지를 결정한다.
  • authenticate() 메서드
    • 전달받은 Authentication 객체를 기반으로 인증을 시도한다.
    • 성공 시, GrantedAuthority를 포함한 인증된 Authentication 객체를 반환한다.
      • 반환된 Authentication 객체는 SecurityContextHolder에 저장되어, 이후 인증된 사용자 정보를 참조할 수 있다.
    • 실패 시, AuthenticationException을 던진다.

ProviderManager

  • AuthenticationManager의 구현체
  • 인증을 처리하는 여러 AuthenticationProvider를 관리한다.
  • 여러 AuthenticationProvider 인스턴스를 List 형태로 위임받아, 각각의 AuthenticationProvider가 인증 요청을 처리할 수 있도록 한다.
  • 인증과정
    • 여러 AuthenticationProvider 중 하나가 인증을 시도한다.
    • 인증 성공 시, 인증된 Authentication 객체가 반환된다.
    • 인증을 수행할 수 있는 AuthenticationProvider가 존재하지 않으면, 인증이 실패하고 ProviderNotFoundException이 발생한다.

4-6. AuthenticationProvider

AuthenticationProvider 인터페이스

  • 인증에 필요한 로직을 정의한다.
  • 인증 방식에 맞는 실제 인증 처리를 수행한다.
  • 사용자 정보를 데이터베이스에서 조회하기 위해 UserDetailsService를 호출하여 인증을 처리한다.

DaoAuthenticationProvider

  • AuthenticationProvider의 구현체 중 하나로, Username/Password 기반 인증을 처리한다.
  • UserDetailsService를 통해 데이터베이스에서 사용자 정보를 조회한 후, 해당 정보로 인증을 수행한다.

      public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
          private UserDetailsService userDetailsService;
        
          @Override  
          protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
              UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
              if (loadedUser == null) {
                  throw new InternalAuthenticationServiceException("UserDetailsService returned null");
              }
              return loadedUser;
          }
      }
    

4-7. UserDetails, UserDetailsService

UserDetails

  • 사용자 정보를 담고 있는 인터페이스

      public interface UserDetails extends Serializable {
          String getUsername();
          String getPassword();
          Collection<? extends GrantedAuthority> getAuthorities();
          boolean isAccountNonExpired();
          boolean isAccountNonLocked();
          boolean isCredentialsNonExpired();
          boolean isEnabled();
      }
    

User

  • UserDetails 인터페이스의 구현체
  • 스프링 시큐리티에서 제공하는 기본적인 사용자 객체
  • 사용자의 이름, 비밀번호, 권한 정보, 계정 만료 여부 등을 보관하며 UserDetails 인터페이스의 메서드를 구현한다.

      public class User implements UserDetails {
          private String password;
          private final String username;
          private final Set<GrantedAuthority> authorities;
          private final boolean accountNonExpired;
          private final boolean accountNonLocked;
          private final boolean credentialsNonExpired;
          private final boolean enabled;
          //...
      }
    

UserDetailsService

  • username을 기반으로 데이터베이스에서 사용자 정보를 조회하고, 이 정보를 UserDetails 객체에 담아 반환한다.

      public interface UserDetailsService {
          UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
      }
    

4-8. UserDetailsManager

UserDetailsManager 인터페이스

  • 사용자 정보를 관리하는 기능을 제공한다.
  • 사용자 정보를 생성, 수정, 삭제하고 비밀번호를 변경하는 등의 작업을 수행한다.

      public interface UserDetailsManager extends UserDetailsService {
          void createUser(UserDetails user);
          void updateUser(UserDetails user);
          void deleteUser(String username);
          void changePassword(String oldPassword, String newPassword);
          boolean userExists(String username);
      }
    

UserDetailsManager의 주요 구현체

  • InMemoryUserDetailsManager
    • 사용자 정보를 메모리 상에서 저장하고 관리한다.
    • 간단한 인증 처리를 위해 사용되며, 주로 테스트 또는 작은 애플리케이션에서 활용된다.
    • 애플리케이션이 종료되면 데이터가 사라진다.
  • JdbcUserDetailsManager
    • 데이터베이스에 저장된 사용자 정보를 기반으로 사용자 관리 작업을 수행한다.
    • 실제 DB와 연동하여 사용자 정보를 저장하고 관리하며, 비즈니스 애플리케이션에 적합하다.
    • 데이터를 영구적으로 저장하며, SQL 기반의 사용자 관리가 가능하다.

Spring Security 인증 실습

  • HelloController 생성
    • RestController
    • 인증이 되어야 접근할 수 있는 하나의 리소스
      @RestController
      public class HelloController {
        
          @GetMapping("/hello")
          public String hello() {
              return "Hello";
          }
      }
    


  • localhost:8080/hello 요청 시 login 창이 뜬다
    • hello에 대한 요청이 날아감 → 로그인이 안됨

    image

    • location이 login 페이지로 설정되어 있다. 인증이 안되있으면 로그인 페이지로 이동한다.

    image

    • 로그인에 성공하면 localhost:8080/hello로 POST 요청이 날아간다.
    • 쿠키에 JSession이 생성된다.
      • JSESSIONID=06F4B7E5FDC90F28D73EA975B1F84B67;

    image




우리 FISA 수업을 참고하였습니다.

© 2021. All rights reserved.

yaejinkong의 블로그