Spring Security Filter
- 스프링 시큐리티에서 기본적으로 제공하는 15개의 필터에 대해서 알아보려 한다.
- 아래와 같은 순서로 15개의 필터가 구성되어 있다.
WebAsyncManagerIntegrationFilter
- SpringMVC Async Hanlder 를 지원하기 위한 필터다.
- SecurityContextHolder 는 thread local 을 사용하기 때문에 aysnc 한 핸들러(컨트롤러) 를 사용할 경우 같인 thread 를 사용하지 않아도 SecurityContext 를 공유할 수 있게 도와주는 필터이다.
- 역할을 아래와 같이 정리할 수 있다.
- preprocessing 과정에서 새로만든 thread 에 SecurityContext를 공유하는 작업이 일어난다.
- postprocessing 과정에서 SecurityContext 를 비워주는 작업이 일어난다.
- 아래와 같은 테스트해볼 수 있다.
public class SecurityLogger {
public static void log(String message){
System.out.println(message);
Thread thread = Thread.currentThread();
System.out.println("Thread : " + thread.getName());
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
System.out.println("Principal : " + principal);
System.out.println("===================");
}
}
- 위와 같이 현재의 thread 와 principal 정보를 보여주는 역할을 하는 클래스를 정의했다.
- 아래와 같이 2가지 경우로 나눠서 테스트 해볼 수 있다.
- aysnc 한 controller 일 경우
@GetMapping("/async-handler")
@ResponseBody
public Callable<String> aysncHanlder(){
SecurityLogger.log("MVC"); //thread 1
return new Callable<String>() {
@Override
public String call() throws Exception {
SecurityLogger.log("Callable"); //thread 2
return "Aysnc Handler";
}
};
}
- 로그 결과를 보면 thread1, thread2 가 다른 스레드인 것을 확인할 수 있다.
- 하지만 둘 모두 같은 Principal 가 같다. -> WebAsyncManagerIntegrationFilter 덕분이다.
- aysnc 한 service 일 경우 (@EnableAsync 를 설정 클래스에 추가해 줘야한다.)
@GetMapping("/async-service")
@ResponseBody
public String asyncService(){
SecurityLogger.log("MVC, before async service"); //thread1
sampleSerivce.asyncService();
SecurityLogger.log("MVC, after async service");
return "Async Service";
}
@Async
public void asyncService() {
SecurityLogger.log("Async Service"); //thread2
System.out.println("Async Service is called");
}
- 위의 경우 처럼 service 가 async 할 경우에는 SecurityContext를 공유할 수 있을까?
- 공유하지 못해서 Principal 값이 다르다. -> 공유하기 위해서 아래와 같은 설정을 추가해 줘야한다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new LoggingFilter(), WebAsyncManagerIntegrationFilter.class);
http.authorizeRequests()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.mvcMatchers("/", "/info", "/account/**", "/signup").permitAll()
.mvcMatchers("/admin").hasRole("ADMIN")
.mvcMatchers("/user").hasRole("USER")
.anyRequest().authenticated()
.expressionHandler(expressionHandler());
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
- 위와 같이
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
를 설정해주면 같은 Principal 값을 확인할 수 있다. - MODE_INHERITABLETHREADLOCAL 인 이유는 thread1 인 nio 안에서 만들어진 하위 스레드 이기 때문이다.
SecurityContextPersistenceFilter
- 15개의 필터 중에서 2번째에 위치하는 필터다.
- SecurityContextRepository 를 사용해서 기존의 SecurityContext 가 있다면 읽어오고, 없다면 비어있는 SecurityContext를 만든다.
- SecurityContext 는 Http Session 에서 읽어 온다.
- Spring-Session 과 연동하여 세션 클러스터를 구현할 수 있다.
- 이 필터는 인증을 처리하기 전에 거치는 것이 맞다. -> 이미 인증된 SecurityContext 를 다시한번 인증 하기위해 다른 필터들을 거칠 필요는 없기 때문이다. -> 그렇기 때문에 2번째에 위치한 것이다.
- 그렇기 때문에 커스텀한 인증 필터를 만들고 싶다면 이 필터 뒤에 위치시켜야 한다.
- 정리하면 아래와 같다.
- 익명사용자의 경우
- 새로운 SecurityContext 객체를 생성하여 SecurityContextHolder 에 저장한다.
- AnonymousAuthenticationFilter 에서 AnonymousAuthenticationToken 객체를 SecurityContext 에 저장한디.
- 인증 시점
- 새로운 SecurityContext 객체를 생성하여 SecurityContextHolder 에 저장한다.
- UsernamePasswordAuthenticationFilter 에서 인증 성공 후 SecurityContext 에 UsernamePasswordAuthentication 객체를 SecurityContext 에 저장한다.
- 인증이 최종 완료되면 Session 에 SecurityContext 를 저장한다.
- 인증 후
- Session 에서 SecurityContext 꺼내어 SecurityContextHolder 에서 저장한다.
- SecurityContext 안에 Authentication 객체가 존재하면 계속 인증을 유지한다.
- 최종 응답이 완료된 후
- SecurityContextHolder.clearContext()
- 익명사용자의 경우
- SecurityContextPersistenceFilter 의 처리흐름은 아래와 같다.
- 위의 사진처럼 인증 된 사용자와 인증 되지 않은 사용자의 로직이 다른것을 확인할 수 있다.
- 왼쪽의 SecurityContextHolder 가 인증전이고 오른쪽의 SecurityContextHolder 가 인증 후이다.
- 인증 전일 경우 Authentication 객체가 null 인 것을 확인 할 수 있다.
HeaderWriterFilter
- 15개의 필터 중에서 3번째에 위치하는 필터이다.
- 응답 헤더에 시큐리티 관련 헤더를 추가해주는 필터다.
- XContentTypeOptionsHeaderWriter: 마임 타입 스니핑 방어.
X-Content-Type-Options: nosniff
- XXssProtectionHeaderWriter: 브라우저에 내장된 XSS 필터 적용.
X-XSS-Protection: 1; mode=block
- CacheControlHeadersWriter: 캐시 히스토리 취약점 방어.
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
- HstsHeaderWriter: HTTPS로만 소통하도록 강제.
- XFrameOptionsHeaderWriter: clickjacking 방어.
X-Frame-Options: DENY
- XContentTypeOptionsHeaderWriter: 마임 타입 스니핑 방어.
- 위와 같이 설정되어 있기때문에 헤더에는 아래와 같이 표시된다.
Cache-Control: no-cache, no-store, max-age=0, must-revalidate Content-Language: en-US Content-Type: text/html;charset=UTF-8 Date: Sun, 04 Aug 2019 16:25:10 GMT Expires: 0 Pragma: no-cache Transfer-Encoding: chunked X-Content-Type-Options: nosniff X-Frame-Options: DENY X-XSS-Protection: 1; mode=block
CsrfFilter
- CSRF 어택 방지 필터이다.
- CSRF 는 인증된 유저의 계정을 사용해 악의적인 변경 요청을 만들어 보내는 기법을 말한다.
- CORS를 사용할 때 특히 주의 해야 한다. -> 타 도메인에서 보내오는 요청을 허용하기 때문이다.
- csrfFilter 는 모든 요청에 랜덤하게 생성된 토큰을 HTTP 파라미터로 요구한다.
- 요청 시 전달되는 토큰 값과 서버에 저장된 실제 값과 비교한 후 만약 일치하지 않으면 요청은 실패하게 된다.
- 하지만 폼기반의 애플리케이션이 아니고, rest api 인 경우는
http.csrf().disable();
로 설정해서 이 필터를 거치지 않게 하는게 낫다. -> 요청을 보낼 때마다 CSRF 토큰을 보내줘야하기 때문이다. - 폼 전송일 경우에는 무조건 하는게 좋다.
LogoutFilter
- 여러 LogoutHandler를 사용하여 로그아웃시 필요한 처리를 하며 이후에는 LogoutSuccessHandler를 사용하여 로그아웃 후처리를 한다.
- LogoutFilter 는 크게 LogoutHandler 와 LogoutSuccessHandler 로 나뉘어진다.
- LogoutHandler 여라가지 핸들러를 가지고있는 Composite 객체이다.
- LogoutHandler
- CsrfLogoutHandler
- SecurityContextLogoutHandler
- LogoutSuccessHandler -> LogoutSuccessHandler 는 로그아웃이 성공했을 때의 처리를 컨트롤 한다.
- SimplUrlLogoutSuccessHandler
- 아래와 같이 로그아웃 필터를 설정할 수 있다.
http.logout()
.logoutUrl("/logout") // 로그아웃 url 설정 -> 커스텀 하려면 바꿀 수 있다.
.logoutSuccessUrl("/") // 로그아웃되면 루트 페이지로 보내겠다.
.logoutRequestMatcher()
.invalidateHttpSession(true) // 로그아웃한다음 세션을 invalid 처리할 것이냐
.deleteCookies("") // 해당 쿠키 삭제
.addLogoutHandler() // 핸들러를 추가해서 부가적인 작업을 할 수 있다.
.logoutSuccessHandler(); // 로그아웃이 성공적으로 이뤄진다음에 부가적인 작업을 하고싶을때 사용할 수 있다.
UsernamePasswordAuthenticationFilter
- 폼에서 아이디 비밀번호를 눌렀을때 처리되는 필터이다.
- AuthenticationManager 를 이용해서 Authenticate(안증) 를 처리한다.
- AuthenticationManager 는 구현체인 ProviderManager 를 사용하는데, ProviderManager는 여러개의 AuthenticationProvider 를 사용한다. -> AuthenticationProvider 중 DaoAuthenticationProvider 는 UserDetailsService 를 사용한다.
- UserDetails 정보를 가져와 사용자가 입력한 password 와 비교한다.
DefaultLogin/LogoutPageGeneratingFilter
- 기본 로그인 폼 페이지를 생성해주는 필터다.
- /login 요청을 처리하는 필터다.
http.formLogin()
.usernameParameter("my-username")
.passwordParameter("my-password");
- 위와 같이 폼으로 넘어오는 변수값을 바꿔줄 수 있다.
http.formLogin()
.loginPage("/signin);
- 이렇게 설정하면 DefaultLoginPageGeneratingFilter,DefaultLogoutPageGeneratingFilter 가 등록되지 않는다.
- 위와 같이 설정된 순간 커스텀한 로그인, 로그아웃 페이지를 쓴다고 가정하고, 위의 두 필터를 제공하지 않는다.
- 그렇다면 커스텀한 로그인 페이지를 아래와 같이 만들 수 있다.
BasicAuthenticationFilter
- 요청 헤더에 username와 password를 실어 보내면 브라우저 또는 서버가 그 값을 읽어서 인증하는 방식을 처리한다.
- 보통 브라우저 기반 요청이 클라이언트의 요청을 처리할 때 자주 사용한다.
- 보안에 취약하기 때문에 반드시 HTTPS를 사용할 것을 권장한다. -> 예를들어 스니핑을 당할 수 있기 때문이다.
- UsernamePasswordAuthenticationFilter 와 비슷하지만, UsernamePasswordAuthenticationFilter 는 폼에서 읽어온다.-> 이 경우는 정보가 Security Context 에 저장되고, 그값을 SecurityContextPersistenceFilter 가 가져오므로 stateful하다. -> 폼인증인 경우 이렇게 하는게 바람직하다.
- 하지만, BasicAuthenticationFilter 는 헤더에서 읽어온다. -> Security Context 에 저장되는 로직이 없다. -> stateless하다. -> 그렇기 때문에 매 요청시 헤더에 보내줘야한다. -> 폼인증이 아닌경우 이렇게 쓰는게 바람직하다.
RequestCacheAwareFilter
- 현재 요청과 관련 있는 캐시된 요청이 있는지 찾아서 적용하는 필터이다.
- 만약 어떤 페이지에 접근하면 accessDecisionManager 접근가능한지 체크한다. -> 접근이 불가능하다면, 로그인페이지(다른페이지로 설정할 수 있다.) 로 가는게 맞다고 판단한다. -> 캐시에 원래 요청을 저장해두고 -> 로그인 페이지로 이동한다. -> 로그인 처리가 끝난 후 캐시에 저장해둔 원래 요청을 처리한다.
- 정리하면 아래와 같다.
- 캐시된 요청이 없다면, 현재 요청 처리한다.
- 캐시된 요청이 있다면, 해당 캐시된 요청 처리한다.
SecurityContextHolderAwareRequestFilter
- 시큐리티 관련 서블릿 API를 구현해주는 필터이다
- 아래와 같은 메서드를 구현한다.
- HttpServletRequest#authenticate(HttpServletResponse)
- HttpServletRequest#login(String, String)
- HttpServletRequest#logout()
- AsyncContext#start(Runnable)
AnonymousAuthenticationFilter
- 익명사용자에 대한 인증 필터이다.
- 익명사용자와 인증 사용자를 구분해서 처리하기 위한 용도로 사용된다.
- 인증이 안된 사용자를 익명 Authentication 객체를 만들어서 security context holder 에 넣어주는 역할을 한다.
- 화면에서 인증 여부를 구현할 때 isAnonymous() 와 isAuthenticated() 로 구분해서 사용한다.
- 인증객체를 세션에 저장하지 않는다.
- 현재 SecurityContext에 Authentication이 null이면 “익명 Authentication”을 만들어 넣어주고, null이 아니면 아무일도 하지 않는다.
- 아래와 같이 기본으로 만들어 사용할 “익명 Authentication” 객체를 설정할 수 있다.
http.anonymous()
.principal()
.authorities()
.key()
SessionManagementFilter
- 세션을 관리해주는 필터이다.
- 아래와 같이 세션 전략을 설정할 수 있다.
http.sessionManagement()
.sessionFixation()
.changeSessionId();
http.sessionManagement()
.sessionFixation()
.migrateSession();
- migrateSession() 의 경우, 인증이 됐을 때 새로운 세션을 만들고, 기존 세션의 정보를 복사해온다.
- changeSessionId() 의 경우, 세션 ID 만 바꾸기 때문에 migrateSession 보다 빠르다.
http.sessionManagement()
.sessionFixation()
.invalidSessionUrl("/login");
- 위와 같이 유효하지 않은 세션을 리다이렉트 시킬 URL 설정할 수 있다.
http.sessionManagement()
.sessionFixation()
.maximumSessions(1)
.maxSessionsPreventsLogin(true);
- maximumSessions(1) 를 1로 설정함으로써 동시 로그인 유저 수를 1명으로 제한 할 수 있다.
- maxSessionsPreventsLogin 는 추가 로그인을 막을지 여부 설정
- default 값은 false 이다. -> 기존 로그인 세션을 만료 시키고 새로운 로그인 인증만 사용한다.
- maxSessionsPreventsLogin(true); -> 존 로그인 세션만 유지하고, 새로운 로그인 세션을 막아준다.
- 세션의 생성 전략은 아래와 같다.
- IF_REQUIRED -> 기본값이다. -> 필요하면 만든다.
- NEVER -> 시큐리티에서는 생성 X, 기존의 세션을 활용한다.
- STATELESS -> 세션을 사용하지 않는다. -> stateless 방식이기 때문에 폼인증에는 저정하지 않는다.
- ALWAYS -> 세션을 항상 사용한다.
ExceptionTranslationFilter
- FilterSecurityInterceptor 와 밀접한 관계가 있다.
- ExceptionTranslationFilter 가 FilterSecurityInterceptor 보다 이전에 있어야 한다. -> 그래서 14번째에 위치하는 것이다.
- ExceptionTranslationFilter 가 FilterSecurityInterceptor 를 감싸고 실행되어야 한다.
- Authentication 에러 발생시 -> AuthenticationEntryPoint 를 활용해서 처리한다. -> 로그인 페이지로 이동시킨다.
- AccessDeniedException 에러 발생시 -> AccessDeniedHandler를 활용해서 처리한다. -> 403 Error 로 처리한다.
- 아래와 같이 에러를 로깅함으로써 백엔드 사이드에 확실하게 에러를 전달할 수 있다.
- 당연히 Exception에 대한 처리 방식은 변경할 수 있다.
http.exceptionHandling()
.accessDeniedHandler((request, response, accessDeniedException) -> {
UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username = principal.getUsername();
System.out.println(username = " 이 " + request.getRequestURI()+ "에 접근하려다 실패했습니다.");
response.sendRedirect("/access-denied");
})
- security context 에서 principal 정보를 가져와서 로그를 남긴다.
- /access-denied 페이지로 이동시킨다.
FilterSecurityInterceptor
- 15개중 가장 마지막에 위치한 필터로써, 인증된 사용자에 대하여 특정 요청의 승인/거부 여부를 최종적으로 결정한다.
- 인증객체 없이 보호자원에 접근을 시도할 경우 AuthenticationException 을 발생한다.
- 인증 후 자원에 접근 가능한 권한이 존재하지 않을 경우 AccessDeniedException 을 발생한다.
- HTTP 리소스 시큐리티 처리를 담당하는 필터이다.
- 권한 처리를 AccessDecisionManager에게 맡기고, AccessDecisionManager를 사용하여 인가를 처리한다.
- 인증이 된 상태에서 특정 리소스에 접근할 수 있는지 Role을 확인한다.
- 다시말해서, AccessDecisionManager를 사용해서 Access Control 또는 예외 처리하는 필터다.
- 아래와 같이 설정할 수 있다.
http.authorizeRequests()
.mvcMatchers("/", "/info", "/account/**", "/signup").permitAll()
.mvcMatchers("/admin").hasRole("ADMIN")
.mvcMatchers("/user").hasRole("USER")
.anyRequest().authenticated()
.expressionHandler(expressionHandler());
REFERENCES
- 백기선님의 스프링 시큐리티
- 정수원님의 스프링 시큐리티
- 안성훈님의 스프링 시큐리티
'Spring Security' 카테고리의 다른 글
AccessDesicionManager 와 AccessDecisionVoter (0) | 2022.02.28 |
---|---|
ExceptionTranslationFilter,RequestCacheAwareFilter (0) | 2022.02.28 |
CustomUserDetailService (0) | 2022.02.28 |
AuthenticationSuccessHandler, AuthenticationFailureHandler, AccessDeniedHandler 커스텀하기 (0) | 2022.02.28 |
UsernamePasswordAuthenticationFilter 커스텀 하기 (0) | 2022.02.28 |