[Spring Security 6.x] 스프링 시큐리티 동작 원리

dh·2024년 8월 19일
0

인증(Authentication)

목록 보기
3/5
post-thumbnail

Spring Security

Spring Security는 Spring 기반 애플리케이션의 인증, 인가 및 보안을 담당하는 스프링 하위 프레임워크입니다.
spring security를 도입한 이유는 spring 기반의 애플리케이션에서 보안에 필요한 기능들을 제공하기 때문에 보안 관련 로직을 작성하지 않아도 되어, 개발의 작업 효율을 높일 수 있습니다.

시큐리티 동작 원리

DelegatingFilterProxy


스프링은 서블릿 컨테이너(톰캣)의 라이프사이클과 스프링컨테이너 사이를 연결하는 DelegatingFilterProxy라는 Filter를 제공한다.
서블릿 컨테이너는 Filter instance를 등록할 수 있지만 Filter에서 spring에 정의된 Beans를 인식할 수 없습니다.
DelegatingFilterProxy를 통해 모든 일을 Filter를 구현한 Spring Bean에 위임합니다.
DelegatingFilterProxy는 스프링 컨테이너로 부터 Bean Filter0를 찾고, Bean Filter0를 불러 작업을 위임합니다.

FilterChainProxy

FilterChainProxy는 Spring Security에서 제공하는 특별한 Filter로, SecurityFilterChain을 통해 많은 Filter들로 위임할 수 있습니다. FilterChainProxy는 Bean으로 DelegatingFilterProxy 안에 감싸지게 됩니다.

SecurityFilterChain

SecurityFilterChain은 FilterChainProxy에서 현재 요청에 대해 호출할 Spring Security Filter 인스턴스를 결정하는 데 사용됩니다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
public SecurityFilterChain filterChain1(HttpSecurity http) throws Exception {

    http
            .securityMatchers((auth) -> auth.requestMatchers("/user"));

    http
            .authorizeHttpRequests((auth) -> auth
                    .requestMatchers("/user").permitAll());

    return http.build();
}

@Bean
public SecurityFilterChain filterChain2(HttpSecurity http) throws Exception {

    http
            .securityMatchers((auth) -> auth.requestMatchers("/admin"));

    http
            .authorizeHttpRequests((auth) -> auth
                    .requestMatchers("/admin").authenticated());

    return http.build();
}
}

securityMatchers로 경로 설정을 하지 않으면, /admin 요청에 대해 filter1이 먼저 등록되어 filterChain1으로 요청이 가고 filterChain1에는 /admin에 대한 설정이 없기 때문에 거부가 발생됩니다.

Security Filters

필터들은 다양한 다른 목적(인증, 인가 등)으로 사용됩니다.
필터들은 적절한 타임에 호출되도록 보장하기위해 특정한 순서로 실행됩니다.
ex) 인증 필터는 인가필터가 수행되기 전에 호출되야함.

  • DisableEncodeUrlFilter

    	- URL로 간주되지 않는 부분을 포함하지 않도록 설정
  • WebAsyncManagerIntegrationFilter

    	- 비동기로 처리되는 작업에 대해 알맞은 시큐리티 컨텍스트(세션)을 적용
  • SecurityContextHolderFilter

    	- 접근한 유저에 대해 시큐리티 컨텍스트 관리
  • HeaderWriterFilter

    	- 보안을 위한 응답 헤더 추가 (X-Frame-Options, X-XSS-Protection and X-Content-Type-Options)
  • CorsFilter

    	- CORS 설정 필터
  • CsrfFilter

    	- CSRF 방어 필터
  • LogoutFilter

    	- 로그아웃 요청 처리 시작점 GET : “/logout”
  • UsernamePasswordAuthenticationFilter

    	- username/password 기반 로그인 처리 시작점 POST : “/login”
  • DefaultLoginPageGeneratingFilter

    	- 기본 로그인 페이지 생성 GET : “/login”
  • DefaultLogoutPageGeneratingFilter

    	- 기본 로그아웃 페이지 생성 GET : “/logout”
  • BasicAuthenticationFilter

    	- http basic 기반 로그인 처리 시작점
  • RequestCacheAwareFilter

    	- 이전 요청 정보가 존재하면 처리 후 현재 요청 판단
  • SecurityContextHolderAwareRequestFilter

    	- ServletRequest에 서블릿 API 보안을 구현
  • AnonymousAuthenticationFilter

    	- 최초 접속으로 인증 정보가 없고, 인증을 하지 않았을 경우 세션에 익명 사용자 설정
  • ExceptionTranslationFilter

    	- 인증 및 접근 예외에 대한 처리
  • AuthorizationFilter

    	- 경로 및 권한별 인가 (구. filterSecurityIntercepter)

정리

was단에서 요청을 DelegatingFilterProxy가 가로채고, FilterChainProxy가 받아서 자신이 갖고 있는 SecurityFilterChain에게 전달.

SecurityFilterChain은 내부에 n개의 필터이 있고, 각각의 필터들은 로그인, 로그아웃, 인가, csrf등 특정한 시큐리티 작업을 수행.

https://www.youtube.com/watch?v=ovXZrpZ2X7U&list=PLJkjrxxiBSFCFM0pjDwm6F98veieD0MER
https://docs.spring.io/spring-security/reference/servlet/architecture.html

UsernamePasswordAuthenticationFilter


Form Login기반 인증을 처리하는 필터(세션 로그인 방식 사용한다)
1. 사용자가 username, password 제출
2. 인증된 사용자 정보가 담긴 인증 객체인 Authentication의 한 종류인 UsernamePasswordAuthenticationToken 생성
3. 이 Token을 AuthenticationManager에 넘겨 인증 시도
4. 인증 실패 시 SecurityContextHolder를 비움
5. 인증 성공 시 SecurityContextHolder에 Authentication저장
6. 인증 처리 완료

SecurityContextHolder

  • SecurityContext는 인증이 완료된 사용자의 상세 정보(Authentication)를 저장합니다.
  • SecurityContext는 SecurityContextHolder 로 접근할 수 있습니다.
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
Authentication authentication = 
				new UsernamePasswordAuthenticationToken(principal, credentials, authorities);

// SecurityContext 에 인증 객체 Authentication 를 저장합니다.
securityContext.setAuthentication(authentication); 

SecurityContextHolder.setContext(securityContext);

Authentication

  • 현재 인증된 사용자를 나타내며 SecurityContext에서 가져올 수 있습니다.
  • Principal : 사용자를 식별하니다.
    username/password 방식으로 인증할 때 일반적으로 UserDetails인스턴스 입니다.
  • Credentials : 주로 비밀번호로, 대부분 사용자 인증에 사용 후 비웁니다.
  • authorities : 사용자에게 부여한 권한을 GrantedAuthority로 추상화하여 사용합니다.

로그인 처리 과정

UserDetailsService

  • UserDetailsService는 username/password 인증방식을 사용할 때 사용자를 DB에서 조회하고 검증한 후 user객체를 UserDetails로 반환합니다. Custom하여 Bean으로 등록 후 사용 가능합니다.
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    public UserDetailsServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));

        return new UserDetailsImpl(user);
    }
}

UserDetails

  • 검증된 UserDetails는 UsernamePasswordAuthenticationToken 타입의 Authentication를 만들 때 사용되며 해당 인증객체는 SecurityContextHolder에 세팅됩니다. Custom하여 사용가능합니다.
  • UserDetailsService와 UserDetails를 직접 구현해서 사용하게 되면 Security의 default 로그인 기능을 사용하지 않겠다는 설정이 되어 Security의 password를 더 이상 제공하지 않습니다.
public class UserDetailsImpl implements UserDetails {

    private final User user;

    public UserDetailsImpl(User user) {
        this.user = user;
    }

    public User getUser() {
        return user;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        UserRoleEnum role = user.getRole();
        String authority = role.getAuthority();

        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(simpleGrantedAuthority);

        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

0개의 댓글