프로젝트 정리/인턴

Spring Security (스프링 시큐리티 내부 구조 총정리 -1)

Ark B 2024. 10. 18. 17:28

사용자의 요청 -> 컨트롤러 (시큐리티 의존성 없는 경우) 과정

1. 사용자의 요청 (Client Request)

  • 사용자(클라이언트)는 HTTP 요청(GET, POST, PUT, DELETE 등)을 서버로 보낸다. 이 요청은 브라우저, 모바일 앱, Postman 등 다양한 클라이언트에서 생성될 수 있다.

2. DispatcherServlet (Front Controller) 수신

  • Spring MVC 애플리케이션의 진입점은 DispatcherServlet이다. Spring에서는 모든 요청이 DispatcherServlet으로 전달되고, DispatcherServlet이 해당 요청을 적절한 곳으로 분배한다.
  • DispatcherServlet이란?
    • 더보기
      Dispatcher Servlet은 HTTP 프로토콜로 들어오는 모든 요청을 가장 먼저 받아 적합한 컨트롤러에 보내주는 Front Controller(주로 서블릿 컨테이너의 제일 앞단에서 서버로 들어오는 클라이언트의 모든 요청을 받아서 처리해주는 컨트롤러)이다.
      즉, Dispatcher Servlet은 Front Controller로서 클라이언트로부터 어떠한 요청이 오게 되면 모든 요청을 자신이 먼저 받게 되고, 이러한 요청들을 세부 컨트롤러에게 전달하는 역할이다.
    • Dispactcher Servlet 동작 방식
      1. 클라이언트 HTTP 요청을 받는다.
      2. 사용자의 요청을 Handler Mapping으로 위임하여 들어오는 요청에 해당하는 URL에 매핑된 컨트롤러를 탐색한다.
      3. 2번에서 반환된 값이 있는 경우 해당 컨트롤러를 호출할 수 있는 HandlerAdapter에 요청을 전달한다.
      4. HandlerAdapter에서 해당 컨트롤러를 호출한다.
      5. 컨트롤러에서 처리된 결과를 Model에 설정하여 뷰의 이름을 반환한다.
      6. Dispatcher Servlet은 5번에서 전달도니 View를 View Resolver에 전달하고 해당하는 View 개체를 찾아 반환
      7. 6번에서 반환된 View에 model의 값을 전달하고 화면 표시를 요청한다. 만약 전달된 Model의 값이 null이면 페이지를 그대로 사용하고 Model의 데이터가 존재하는 경우 렌더링 한다.
      8. 응답받은 View의 결과를 클라이언트에게 응답을 한다.
    •  

요청 처리 단계에서 DispatcherServlet의 위치

1. 클라이언트의 요청 : 클라이언트가 HTTP 요청을 보낸다

2. WAS(예 : Tomcat)의 필터 통과: 클라이언트의 요청은 WAS(Web Application Server)의 필터를 먼저 통과한다.
필터는 주로 인증/인가, 로깅 , CORS 처리, 공통 요청 처리 등을 수행한다. Spring Security의 필터가 있다면, 사용자의 JWT 토큰 검증 등이 여기서 이루어지게 된다.
3. 

 

https://www.youtube.com/watch?v=OmP-GNRLA0Y

 

클라이언트의 요청은 서버 컴퓨터의 WAS(톰캣)의 필터들을 통과한 뒤 스프링 컨테이너의 컨트롤러에 도달한다.

 

스프링 시큐리티는 WAS 필터단에서 요청을 가로챈 후 시큐리티의 역할을 수행한다. 

 

엄밀한 정의

- WAS의 필터에 하나의 필터를 만들어서 넣고 해당 필터에서 요청을 가로챔.

- 해당 요청은 스프링 컨테이너 내부에 구현되어 있는 스프링 시큐리티 감시 로직을 거침

(톰캣에서 처리하는 것이 아니다!)

- 시큐리티 로직을 마친 후 다시 WAS의 다음 필터로 복귀

출처: https://www.youtube.com/watch?v=OmP-GNRLA0Y

실제 공식 홈페이지 이미지를 보면 DelagatingFilterProxy에서 가로 챈 다음 SecurityFilterChain에서 검증로직을 거치는 것이다. 

DelegatingFilterProxy의 역할?

출처:https://logical-code.tistory.com/194

ServletFilter는 Servlet 스펙이기 때문에 스프링에서 정의한 빈들을 주입받아 사용할 수 없다. 

DelegatingFilterProxy 

FilterChainProxy라는 빈에다가 요청을 던져주는 역할, 매개체 역할을 한다.

 

filterChainproxy의 

getFilters(HttpServletRequest request)에 중단점 걸고 디버그 모드로 실행한다음 api 하나 날려보면 현재 걸려있는 필터및 목록 볼 수 있음 . 

나는 이러한 모습이다.

SecurityFilterChain 등록

 

SecurityFilterChain 여러 개 등록할 수 있음.

 

예를 들어 하나는 로그인 담당, 하나는 로그아웃 담당으로 등록하게 만들 수 있다.

 

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
    public SecurityFilterChain filterChain1(HttpSecurity http) throws Exception{
    
    	http
        		.authorizeHttpRequests((auth) -> auth
                		.requestMatchers("/user").permitAll());
        	
    	return http.build();
        
    }
    
    @Bean
    public SecurityFilterChain filterChain2(HttpSecurity http) throws Exception{
    
    	http
        		.authorizeHttpRequests((auth) -> auth
                		.requestMatchers("/admin").permitAll());
    
    	return http.build();
        
    }
    
}

 

멀티 SecurityFilterChain 중 하나 선택

 

FilterChainProxy는 N개의 SecurityFilterChain 중 하나를 선택해서 요청을 전달한다. 선택 기준은 아래와 같다.

 

- 기준

  1. 등록 인덱스 순

  2. 필터 체인에 대한 RequestMatcher 값이 일치하는지 확인 (인가 설정이 아님, 아래에서 설명)

만약 1번이 /admin고 2번이 /user라 가정한다면 /user로 요청보냈을 때 1번 security 필터에 requestmatching 되지 않기 때문에 2번 필터로 넘어간다.

 

문제상황

: 멀티 SecurityFilterChain을 설정시 N개의 SecurityFilterChain이 모두 "/**" 경로에서 매핑된다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
    public SecurityFilterChain filterChain1(HttpSecurity http) throws Exception{
    
    	http
        		.authorizeHttpRequests((auth) -> auth
                		.requestMatchers("/user").permitAll());
        	
    	return http.build();
        
    }
    
    @Bean
    public SecurityFilterChain filterChain2(HttpSecurity http) throws Exception{
    
    	http
        		.authorizeHttpRequests((auth) -> auth
                		.requestMatchers("/admin").permitAll());
    
    	return http.build();
        
    }
    
}

예시상황

2개의 커스텀 SecurityFilterChain을 등록하고 "/user", "/admin" 경로에 대해서 permitAll() 설정을 진행한다. (해당하는 컨트롤러는 만들어 둠)

 

이후 "/admin" 경로로 요청을 보내면 별다른 인증 과정 없이 "/admin" 컨트롤러의 데이터를 획득할 수 있다고 생각하지만 시큐리티의 매핑 자체가 없어서 잘못된 응답이 발생한다.(인가작업이 아닌 매핑 작업에 대한 설정 추가해야함)

 

이유는 filterChain1이 filterChain2 보다 먼저 등록되어 있고 SecurityFilterChain에 대한 경로 설정을 추가적으로 하지 않았기 때문에 "/**" 경로에 대해서 반응하기 때문이다.

따라서 filterChain1로 요청이 전달되었고 filterChain1 내부의 인가 설정에서는 "/admin"에 대한 설정이 없기 때문에 거부가 발생된다.

 

따라서 SecurityFilterChain에 대한 경로 매핑이 필요하다.

이는 securityMatchers를 통해 가능하다. 

 

@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").permitAll());
                    
    return http.build();
    
}

멀티 SecurityFilterChain 순서 설정을 할 수 있다

 

N개의 SecurityFilterChain을 등록 한 뒤 등록되는 순서를 직접 선정하고 싶은 경우 @Order() 어노테이션에 값을 명시할 수 있다.

 

특정 요청은 필터를 거치지 않도록 함으로써 자원을 아낄 수 있다.

 

-> SecurityFilterChain을 거치게 된다면 내부적으로 여러 가지 필터를 거치게 된다. 이때 서버의 자원을 사용하고 상주 시간이 발생하기 때문에 원하는 값은 필터를 통과하지 못하도록 설정할 수 있다.

 

보통 정적 자원(이미지 , CSS)의 경우 필터를 통과하지 않도록 아래 구문을 통해 설정할 수 있다.

 

설정시 하나의 SecurityFilterChain이 0번 인덱스로 설정되며 해당 필터 체인 내부에는 필터가 없는 상태로 생성된다.

 

예를 들어 

 

@Bean

public WebSecurityCustomizer webSecurityCustomizer() {

   return web -> web.ignoring().requestMatchers("/img/**);

}


SecurityFilterChanin은 스프링 시큐리티의 보안, 인증/인가와 같은 주요로직을 담당한다.

하나의 SecurityFilterChain 내부에 N개의 필터를 구성하여 각각의 필터가 하나의 로직(로그아웃, 로그인,인가 등등) 수행의 시작점이 된다. ->하나의 필터가 맡게 구성하는게 중요하다!

로그인 관련 처리되고 돌아오고, 그냥 시큐리티 Bean까지 안가고 처리될 수 도

 

스프링 시큐리티 제공 필터 간단 설명

  • 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)

SecurityFilterChain에 필터 활성화 및 비활성화

  • CORS 필터 설정
http
        .cors((cors) -> cors.disable());JAVA

 

  • CSRF 필터 설정
http
        .csrf((csrf) -> csrf.disable());JAVA

 

  • UsernamePasswordAuthenticationFilter 활성 비활성
//활성
http
        .formLogin(withDefaults());

//비활성
http
        .formLogin((login) -> login.disable());JAVA

 

  • 로그인/로그아웃 페이지 필터 활성 비활성
//formLogin 메소드 설정시 기본적으로 활성화

//커스텀시 기본 필터 비활성
http
        .formLogin((login) -> login.loginPage("/login").permitAll());JAVA

 

  • BasicAuthenticationFilter 활성 비활성
//활성
http
        .httpBasic(withDefaults());
        
//비활성
http
        .httpBasic((basic) -> basic.disable());JAVA

 


SecurityFilterChain에 커스텀 필터 등록

//특정필터이전
http
        .addFilterBefore(추가할필터, 기존필터.class);

//특정필터위치
http
        .addFilterAt(추가할필터, 기존필터.class);

//특정필터이후
http
        .addFilterAfter(추가할필터, 기존필터.class);

 

 

SecurityFilterChain에 필터 활성화 및 비활성화

- CORS 필터 설정

https://www.devyummi.com/page?id=6696a0da5021fc8699c46b53

 

개발자 유미 | 커뮤니티

 

www.devyummi.com