스프링 시큐리티 - 구조와 흐름

choko's avatar
Jun 29, 2024
스프링 시큐리티 - 구조와 흐름
 
 
 

스프링 시큐리티

  • Spring 기반 애플리케이션의 보안을 담당하는 프레임워크
    • 인증과 인가를 담당
    • 웹 클라이언트의 요청은 필터 → 인터셉터 → 디스패처 서블릿 → 컨트롤러 순으로 들어오는데, 스프링 시큐리티는 서블릿 필터 앞쪽 끝단에서 동작한다.
    • 다양한 보안 관련 요구사항을 각각의 필터 형태로 애플리케이션에 삽입하고, 사용자의 요청을 가로채 필요한 보안적 역할을 수행하는 형식으로 동작한다.
notion image
  1. 사용자의 요청이 스프링 시큐리티 인증필터인 AuthenticationFilter에 가로채진다.
  1. UsernamePasswordAuthenticationToken : 추후 인증이 끝나고 SecurityContextHolder에 등록될 Authentication 객체
  1. AuthenticationFilter는 내부적으로 AuthenticationManager를 호출하여 사용자의 요청을 검증한다.
  1. AuthenticationManager는 다수의 AuthenticationProvider를 주입받아 해당 인증의 작업을 수행한다.
  1. 다양한 AuthenticationProvider 실행 , ex) UserDetailsService : 회원을 스프링 시큐리티가 사용할 수 있는 형태로 가져올 수 있음
  1. ex) UserDetails : UserDetailsService에 사용할 유저 정보를 UserDetails 인터페이스에 저장.
7, 8 ex) UserDetails를 AuthenticationProvider에 반환, AuthenticationManager에 주입
9, 10 인증에 성공하면, 해당 정보를 SecurityContextHolder 내부에 저장한다. 실패할 경우 Exception
 
 
 

회원 관리 인터페이스

UserDetails

  • 스프링 시큐리티가 사용 가능한 회원의 형태를 기술한 인터페이스
public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
  • 해당 메서드들을 오버라이딩하여 사용한다.
  • GrantedAuthority
    • 특정 회원에게 인가할 권한 목록을 담는다. 다음과 같이 GrantedAuthority 인터페이스를 구현한다.
    • public class CustomGrantedAuthority implements GrantedAuthority { private final String authority; public CustomGrantedAuthority(String authority) { this.authority = authority; } @Override public String getAuthority() { return authority; } }
       

UserDetailsService

  • AuthenticationProvider가 회원 정보를 인증할 수 있도록 시스템상으로 회원 정보를 불러온다.
  • 하나의(loadUserByUsername) 메서드를 갖는 인터페이스
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
  • loadUserByUsername
    • username을 받아 일치하는 회원이 있으면 해당 유저의 UserDetails를 반환
    • 일치하는 회원이 없으면 UsernameNotFoundException을 AuthenticationProvider에게 전달
 

UserDetailsManager

  • loadUserByUsername밖에 없는 UserDetailsService에 부족한 역할을 보안하기 위한 인터페이스
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); }
 
 
 
 

Application Context에 userDetailsService() 구현체 등록

@Configuration public class SecurityConfiguration { @Bean public UserDetailsServiceImpl userDetailsService() { CustomUser tom = new CustomUser("tom","tom","1234","SUPER"); CustomUser richard = new CustomUser("richard","richard","1234","NORMAL"); return new UserDetailsServiceImpl(List.of(tom, richard)); } @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } }
  • @Configuration 어노테이션으로 Application Context에 해당 클래스가 설정 파일임을 알려준다.
    • → 스프링 컨테이너가 생성될 때 해당 클래스의 @Bean들이 컨테이너 내부에 등록된다.
  • CustomUser은 UserDetails의 구현체이다.
  • 스프링 시큐리티는 회원 비밀번호 데이터를 암호화하여 저장하기 때문에, PasswordEncoder를 지정해 주어야 한다.
    • PasswordEncoder는 단방향 암호화로 복호화가 불가능하다.
 
 
 
 

Authentication

  • 인증된 회원의 정보를 저장하는 인터페이스, Principal, Credential, Authority에 정보가 저장되어 있다.
    • Principal : 정보의 주체로, 시스템에 접근하고 인증 허가된 회원/시스템 등을 일컫는다.
    • Credential: 주체가 올바르다는 것을 증명할 수 있는 자격증명
    • Authority: 주체에게 부여된 권한
public interface Authentication extends Principal, Serializable { Collection<? extends GrantedAuthority> getAuthorities(); Object getCredentials(); Object getDetails(); Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
 
 

AuthenticationProvider

  • 회원 정보와 사용자의 요청을 비교해 사용자의 요청을 검증한다.
  • AuthenticationManager는 다수의 AuthenticationProvider을 가질 수 있다.
public interface AuthenticationProvider { Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class<?> authentication); }
  • authenticat 메서드
    • 회원의 정보와 사용자의 요청을 비교해 각각의 정보가 일치하는지 검증
    • 일치 시 Authentication 객체에 회원 정보를 담아 return, 일치하지 않으면 AuthenticationException
    • authenticate 메서드 내부에는 @Bean으로 등록된 UserDetailsService, PasswordEncoder가 사용됨
  • supports 메서드
    • 사용자의 요청이 자신이 수행하는 인증 검증과 일치하는지 조회
 
 
 

SecurityContext

  • SecurityContext로 Authentication을 setter/getter 할 수 있다.
public interface SecurityContext extends Serializable { Authentication getAuthentication(); void setAuthentication(Authentication authentication); }
  • SecurityContext는 데이터를 담는 일종의 상자로, AuthenticationProvider와 AuthenticaionManager에 의해 인증된 회원 정보를 저장하는 보관소이다.
  • SecurityContextHloder
    • SecurityContext를 담는 또 하나의 상자이다.
    • Authentication → SecurityContext → SecurityContextHolder를 담는다.
    • SecurityContextHloder 내부 다음과 같은 메서드들로 SecurityContext에 접근하거나 제어할 수 있다.
    • public static void clearContext() { strategy.clearContext(); } public static SecurityContext getContext() { return strategy.getContext(); } public static int getInitializeCount() { return initializeCount; } private static void initialize() { ... } public static void setContext(SecurityContext context) { strategy.setContext(context); }
 
 

회원 로그인 인증 전체 프로세스 - 구현

  1. User와 User에 담을 Authority의 Entity를 생성한다.
  1. UserReposiotry, AuthorityRepository를 생성한다.(JpaRepository 상속)
  1. UserDetailsService를 상속받은 서비스를 생성한다.
    1. public class JpaUserDetailsService implements UserDetailsService { private final UserRepository userRepository; @Override public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findUserByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("조회 실패")); return new CustomUserDetails(user); } }
      • loadUserByUsername: UserDetails의 구현체인 CustomUserDetails를 반환
  1. AuthenticationProvider를 생성한다.
      • AuthenticationProvider는 UserDetailsService로 조회된 UserDetails 구현체를 이용해 실제 인증 작업을 수행한다.
      public class CustomAuthenticationProvider implements AuthenticationProvider { private final JpaUserDetailsService jpaUserDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getName(); String password = authentication.getCredentials().toString(); CustomUserDetails customUserDetails = jpaUserDetailsService.loadUserByUsername(username); return new UsernamePasswordAuthenticationToken(username, password, userDetails.getAuthorities()); } @Override public boolean supports(Class<?> authentication) { return UsernamePasswordAuthenticationToken.class .isAssignableFrom(authentication); } }
       
       
       

      회원 로그인 인증 전체 프로세스 - 내부 프로세스

    1. 사용자가 filterChain에 .formLogin 에 정의된 로그인 페이지에 접근
    2. 사용자는 아이디/패스워드를 입력하여 로그인 정보를 서버에 제출
    3. .addFilterBefore({jwt 필터}, UsernamePasswordAuthenticationFilter.class) 사용 시 custom jwt 필터로 jwt 검증과정 진행, jwt 만료기한 check, refresh 등
    4. UsernamePasswordAuthenticationFilter : 사용자의 로그인 인증 정보를 가로채서 AuthenticationManager 에게 인증을 요청한다.
    5. AuthenticationManagerAuthenticationProvider 에게 사용자 인증을 위임한다.
    6. AuthenticationProvider 는 실제 제공된 아이디/비밀번호를 기반으로 실제 인증을 수행한다. 데이터베이스의 회원 정보와 비교하는 과정이 진행된다.
    7. 인증에 성공하면, AuthenticationProviderUsernamePasswordAuthenticationTokenAuthentication를 생성하여 AuthenticationManager로 반환한다.
    8. Authentication 가 성공적으로 반환되면, AuthenticationSuccessHandler 가 호출되어 로그인 성공 로직, 성공 페이지로 리다이렉트 등이 일어난다.
    9. Authentication 가 성공적으로 반환되지 않으면, AuthenticationFailureHandler 가 호출되어 로그인 실패 페이지로 리다이렉션 or 실패 메세지를 표시한다.
    10. 로그인 성공 시, 사용자의 인증 정보는 SecurityContextHolder 에 저장되어 현제 세션동안 유지된다.
    11. 사용자 인증이 완료되면, 권한 기반의 접근 제어가 적용되어 사용자가 특정 리소스에 접근할 수 있는지 여부를 결정한다.
    12.  
 

Filter

  • 서블릿 필터
    • 클라이언트 요청이 스프링 컨테이너로 들어가기 전 단계에서 동작하는 객체 혹은 계층
    • 일반적으로 인증, 인가와 같은 작업 수행
    • javax.servlet의 filter 인터페이스를 구현함으로써 사용 가능
    • filter 인터페이스는 다음 세 메서드가 존재한다.
      • init : 필터 객체가 초기화 될 때 실행
      • doFilter : 해당 필터가 스프링 컨테이너에 의해 호출될 때 실행
      • destroy : 필터 객체가 종료될 때 실행
      •  
  • Filter Chain
    • 각각의 필터들은 필터 체인 내부의 순서에 따라 차곡차곡 보관되고, 클라이언트의 요청이 있을 때마다 필터 체인에 의해 순서대로 호출됨.
    • @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { chain.doFilter(request, response); // 호출된 필터는 반드시 필터 체인을 호출해 다음 필터를 실행해야 합니다. }
    • doFilter 메서드는 3번째 매개변수로 필터 체인을 호출하여 반드시 다음 필터를 실행시켜야 한다.
 
 

AuthenticationEntryPoint

notion image
  • AuthenticationEntryPoint 를 통해 필터에서 예외 처리를 할 수 있다.
  • commence를 오버라이딩 한다.
public class CustomJwtEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { } } // SecurityConfig .. private final CustomJwtEntryPoint customJwtEntryPoint; http.addFilterBefore(new JwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class). exceptionHandling(exceptionConfig -> exceptionConfig.authenticationEntryPoint(customJwtEntryPoint))
 
 

authorizeHttpRequests().requestMatchers()

  • authorizeHttpRequests()
    • HTTP 요청에 대한 인가 설정을 구성하는데 사용
    • 인가 규칙 정의, 경로별로 다른 권한 설정 가능
  • requestMatchers()
    • 특정한 HTTP 요청 메서드를 적용할 수 있게 해줌.
Share article

Tom의 TIL 정리방