Spring 기반 프로그램의 보안을 담담하는 프레임워크로, 인증과 인가 기능을 제공해서 애플리케이션을 보호한다.
간단히 말하면 사용자 요청을 검사하는 다양한 Filter중 하나인 인증, 인가, 보안과 관련된 필터 하나를 상세히 다루는 프레임워크가 바로 Spring Security이다.
Security의 동작 방식을 알아야 하는 이유는?
특정 역할을 수행하는 필터의 내용을 우리 서비스가 사용하는 방식 JWT, DB에서 확인 등으로 내용을 수정하여 사용하거나, 아예 새로운 필터를 만들어 추가하여 사용할 때 적절한 위치에 알맞에 사용해서 전체 filter적용 흐름이 깨지지 않게 하기 위함이다.
인증
일치나 존재여부를 판단
인가
권한을 부여받는 것
DelegatingFilterProxy
Tomcat의 Servlet Context영역에서 HTTP요청에 대한 처리를 담당하는 전후처리 필터 중 하나로, 인증과 인가, 보안에 대한 처리를 수행하는 필터이다.
Spring Security는 해당 Filter를 스프링 영역에서 쉽고 편하게 다룰 수 있도록 도와주는 도구이다. (상세 설정, 커스텀설정도 가능)
직접 처리하지 않고 잠시 스프링 영역의 객체에 처리를 위임하며, 처리가 끝나면 기존의 Filter를 마저 통과하여 서비스에 접근한다.
FilterChainProxy
DelegatingFilterProxy에게 위임받아 인증, 인가, 보안을 처리하는 Spring 객체이다.
springSecurityFilterChain라는 이름의 Bean으로 저장, 관리되며 DelegatingFilterProxy가 호출되면 요청을 가로채서 내부 SecurityFilterChain중 하나에 매칭시켜 실질적인 처리를 위임한다.
SecurityFilterChain
실질적인 인증, 인가, 보안 처리를 담당하는 객체이다.
@Configuration, @EnableWebSecurity 등 Spring과 Spring Security의 어노테이션으로 설정한 설정파일의 변경된 내용을 참고하여 만들어진 여러 Filter들을 순차적으로 호출하여 처리를 시작한다.
Configuration파일 마다 생성되기 때문에 여러개 있을 수 있으며, 요청에 따라 적절한 SecurityFilterChain이 매칭되어 내부의 필터를 호출한다.
매핑 방법은 기본적으로 matcher를 통한 URL매핑이고 @Order로 우선순위 지정도 가능하다.
Spring Security 의존성만 추가해도 기본값을 바탕으로 SecurityFilterChain이 하나 생성된다.
HttpSecurity
설정을 바탕으로 Filter들을 생성하여 가지고 있는 객체로, @Configuration 어노테이션이 붙은 클래스의 메소드에 인자로 넘겨주어 설정을 추가하거나 변경할 수 있도록한다.
UsernamePasswordAuthenticationFilter
Spring Security가 생성하는 Filter중 하나로 세션방식의 로그인을 바탕으로 인증 인가 처리를 한다. 이때 username과 password를 통해 유저의 유효성을 판단한다.
처리 후 Authentication객체에 담긴 정보를 SecurityContext에 담아 저장하는 작업도 수행한다.
ExceptionTranslationFilter
인증 인가 과정에서 정해진 예외가 발생하면 호출되는 Filter로, 결과가 실패했음을 알리기 위해 응답 코드를 작성하고 반환한다.
FilterSecurityInterceptor
일반적으로 Filter 끝단에 위치하는 인터셉터로, 최종 인증 인가 처리를 위해 AccessDicisionManager를 호출하여 결과처리를 위임시킨다.
AccessDicisionManager
인증여부와 인가여부 등을 고려하여 Vote방식으로 최종 처리한다.
실패시 ExceptinTranslationFilter를 호출한다.
전체 웹 서비스에서 Spring Security의 영역
Spring 영역에서의 Spring Security 내부 구조
로그인 폼 제출
앞단의 Filter들 통과
UserNamePasswordAuthenticationToken을 발급받아 AuthenticationManager를 통해 인증 인가작업 실시(토큰 사용)
Manager의 구현체인 ProviderManager가 실제 인증 인가 작업처리 코드가 작성된 Provider들 중 적절한 Provider를 매핑시켜 호출
유저 상세정보를 Service를 통해 DB작업을 거쳐 가져와서 처리하고, Authentication객체에 담아 전달 (이때 UserDetailService를 상속받아 DB처리 작업을 커스텀 할 수 있다.)
최초 호출 지점에서 SecurityContext영역에 접근하여 처리(저장)하고 작업 종료
이후의 Filter들 통과
Config파일이 없는 경우
Spring Security에서 기본으로 설정한 세팅으로 Filter들을 생성하고 SecurityFilterChain에 담는다.
Config파일이 있는 경우
@Configuration, @EnableWebSecurity가 붙어있는 Config파일에서 설정한 값을 반영하여 filter들의 구성과 판단 값등 각종 설정을 변경하여 SecurityFilterChain에 담는다.
직접 Filter를 만들어 등록시킬수도 있는데, 이때 기존 Filter전이나 후 또는 교체까지도 시킬 수 있다.
즉, 하나의 Config파일은 하나의 FilterChain을 만들어내기 때문에, FilterChainProxy는 이 중 어떤 Chain을 사용할지 결정해야 한다.
필터의 순서가 고정된 불변의 값은 아니지만, 일반적으로 필터의 말단에는 FilterSecurityInterceptor가 위치해서 AccessDicisionManager에게 접근 허용여부를 판단시킨다.
여러개의 SecurityFilterChain가 있을 경우의 우선순위는?
@EnableWebSecurity를 추가하면 의존성 추가와 동시에 생성되었던 기본FilterChain보다 우선순위가 높아진다.
또한 matcher로 설정하는 URL을 기준으로도 매칭을 시도한다.
만약 더 디테일하게 설정하려면 @Order 어노테이션으로 우선순위를 지정한다.
Default SecurityFilterChain을 생성할 때 사용하는 @Order의 값이 int의 Max값으로 되어있기 때문에 우선순위가 매우 낮음을 알 수 있다.(값이 낮을수록 우선순위가 높음)
Spring Security 의존성 추가시 생성되는 SecurityFilterChain을 주입받은 상태로 Bean에 등록, 이때 Bean의 이름은 springSecurityFilterChain이다.
DelegatingFilterProxy가 호출될 때 위의 이름을 가진 Bean객체가 주입되어 SpringSecurity가 동작하게 된다.
인증과 인가를 위임받은 FilterChainProxy는 기본생성되거나 Config파일로 만들어진 여러 SecurityFilterChain들 중 요청과 매칭되는 Chain을 하나 선택해 내부 필터에 진입시켜 처리한다.
내부 순회가 종료되면 Intercept해왔던 OriginalChain으로 돌려보낸다.
Filter들은 doFilter(), doFilterInternal() 등의 메소드로 자신의 코드를 실행시키거나 chain에 연결된 다른 필터를 단계적으로 호출한다.
이때 해당 필터가 종료되지 않은 상태로 재귀함수처럼 다른 Filter로 이동하는 것이기 때문에 처리가 끝나거나 예외가 발생한 시점에 메소드가 종료되기 시작하면 연쇄적으로 종료하여 진입점으로 돌아간다.
정상적으로 Filter를 통과하다가 DelegatingFilterProxy에 도달하면 Spring Security가 가로채서 내부 필터로 인증 인가처리를 하고 다시 원래 FilterChain으로 복귀시켜 남은 작업을 수행시킨다.
기본적으로 필터는 전체를 순회할 때까지 계속되지만 문제가 발생하면 더 이상 이후의 필터를 순회하지 않고 응답을 생성하여 하나씩 종료하는 방식으로 진입점(HTTP요청)으로 돌아가 HTTP응답을 에러코드와 함께 반환한다.
doFilter코드 실행 시점에 기존 Filter가 종료되지 않고 재귀함수와 같은 실행 형태로 중첩되어 있기 때문에 하나씩 종료되면 역순으로 진입점에 도달할 수 있다.
SecurityFilterChain 내부에서 문제가 발생하면 FilterTranslationFilter가 commence명령어 등을 호출하여 403등과 같은 응답코드를 포함하여 응답을 작성하고 종료시킨다.
무조건 Filter가 FilterTranslationFilter를 통해 처리를 하는 것은 아니지만 비슷하게 동작하는 객체와 메소드를 통해 실패 처리한다.
config파일을 생성하여 적용하고자 하는 FilterChain의 설정값을 수정하거나, 직접 만든 CustomFilter를 삽입 및 대체시킬 수 있다.
@EnableWebSecurity
HttpSecurity 객체를 사용하여 다양한 보안 설정을 적용할 수 있다.
ex) URL 패턴별로 접근 권한을 설정하고, 폼 로그인, 로그아웃, CSRF 보호 등을 설정할 수 있다
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
//웹 소켓을 사용하면 요청1 - 응답1의 HTTP에선 할 수 없는
//요청1로 연결확보하여 지속적인 응답을 구현할 수 있다
//webflux -> 네티라는 서버쓰고 병렬처리에 특화
private final JwtUtil jwtUtil;
private final AuthenticationConfiguration authenticationConfiguration;
@Bean
BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)throws Exception{
return configuration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
//세션 로그인 방식일 때 사용하는 공격기법으로, jwt사용할거기 때문에 꺼둔다.
//스프링 시큐리티는 기본적으로 세샨로그인 방식을 디폴트로 사용하고 이에 대한 대책이 기본으로 구현되어있다.
//세션 안쓸거면 커스텀해서 바꿔줘야함
http.csrf((auth) -> auth.disable());
//http의 여러 기본 인증방식 중 하나
http.httpBasic((auth)-> auth.disable());
//기본페이지
http.formLogin(Customizer.withDefaults());
//*은각각 하위 한 단계를 의미 **는 하위에 있는 모든 것을 의미
//필요에 따라 여러개의 requestMathers를 추가할 수 있다.
//mathers안에는 여러개의 값(페이지)를 한번에 나열해서 할당할 수 있다.
//컨트롤러 단위, 메소드 단위로 제한을 전부 걸 수 있다.
http.authorizeHttpRequests((auth) ->
auth
.requestMatchers("/role/**").hasRole("ADMIN")
.requestMatchers("/test/**","/mypage").authenticated()
.requestMatchers("/member/**").permitAll()
.requestMatchers("/kakao/**").permitAll()
.anyRequest().authenticated()
);
http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration),jwtUtil), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(new JwtFilter(jwtUtil), LoginFilter.class);
//특정 페이지를 차단하고 나머지 다 허용 -> 블랙리스트
//특정 페이지를 허용하고 나머지 다 차단 -> 화이트리스트
return http.build();
}
}
스프링 시큐리티가 로그인과 같은 처리를 할 때 암호화된 값을 가지고 비교하는 로직을 수행하기 때문에 스프링 시큐리티가 사용하는 방식의 암호화를 사용해야한다.(해독할 수 있도록)
스프링시큐리티는 스프링 컨텍스트에 있는 빈에 자동으로 등록해서 객체를 관리하지 않고 자체적으로 객체를 생성하기 때문에 Bean등록을 하려면 수동으로 해줘야한다.
필터를 새로 만들때는 @Component 어노테이션을 굳이 붙이지 않고 수동으로 객체를 한번만 생성하게 해줘도 된다. (시큐리티 말고 쓸 일이 없음)
필터 위치에 따라 의도와 다르게 작동할 수 있기 때문에 FilterChain 내에서 위치는 잘 정해줘야한다.
Tomcat의 필터를 출력한 결과 (springSecurityFilterChain이라는 이름으로 Bean을 찾는 이유 확인 가능)
FilterChainProxy가 가지고 있는 Filter 리스트 만큼 다 돌면(끝까지 정상적으로 돌았으면) Intercept했던 상태를 종료하고 원래의 chain(Servlet영역 filter)의 다음필터.doFilter를 실행위의 이미지는 FilterChainProxy가 끝나고 원래 Servlet Filter의 Chain(this.originalChain)으로 돌아가는 코드