Spring Security 의 다양한 Filter

김종하·2020년 12월 20일
1

Spring boot booster

목록 보기
7/13

다루는 내용
1. WebAsyncManagerIntegrationFilter
2. @Async 와 SpringSecurity
3. SecurityContextPersistenceFilter
4. 요청을 처리하고 SecurityContextHolder 를 비워주는 이유

WebAsyncManagerIntegrationFilter

SpringMVC Async Hanlder 를 지원하기 위한 필터이다.
SecurityContextHolder 는 thread local 을 사용해 securityContext를 관리하는 것은 앞서서 살펴보았다.
동일한 thread 에서만 securityContext 가 공유되기 때문에 callable 을 사용한 async 한 handler 가 핸들러를 사용할 경우, 내부에서 사용되는 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("===================");
    }
}

------------------------------------------------------

<<<<Spring MVC Controller  구현한 클래스에서 aysncHanlder 핸들러만 뽑아왔습니다. >>>>


    @GetMapping("/async-handler")
    @ResponseBody
    public Callable<String> aysncHanlder(){
        SecurityLogger.log("MVC");
        /* callable 을 사용하면 call() 을 처리하기전에 Request 를 처리하던 thread 를 반환한다.
           그리고 call() 의 동작이 완료되면 응답을 보내준다.
         */
        return new Callable<String>() {
            @Override
            public String call() throws Exception {
                SecurityLogger.log("Callable");
                return "Aysnc Handler";
            }
        };
    }
    
위 코드의 결과는 다음과 같다 

MVC
Thread : http-nio-8080-exec-1
Principal : org.springframework.security.core.userdetails.User [Username=jaden, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]]
===================
Callable
Thread : task-3
Principal : org.springframework.security.core.userdetails.User [Username=jaden, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]]
===================

task-3 thread 에서도 principal 이 공유되고 있음을 확인할 수 있다. 이것이 가능하게 하는 필터가 바로 WebAsyncManagerIntegrationFilter 이다.

Security와 @Async

필터와 관련된 이야기는 아니지만, 위에서 비동기처리와 관련된 문제를 다뤘음으로, 이어서 설명하도록 하겠다.

    @Async 
    public void asyncService() {
        SecurityLogger.log("Async Service");
        System.out.println("Async Service is called");
    }

다음과 같이 @Aysnc 어노테이션을 통해 해당 메서드가 호출될때, 별도의 thread 를 생성해 비동기적으로 동작하게 한다고 생각하자.
물론 @Async 만 붙인다고 비동기적으로 동작하진 않음으로, @EnableAsync 를 설정 클래스에 추가해주었다.

그리고 다음과 같은 핸들러를 생성하여 확인해보도록 하자.
(여기서 사용한 SecurityLogger.log()는 바로 위에 사용한 것과 같은것임)

    @GetMapping("/async-service")
    @ResponseBody
    public String asyncService(){
        SecurityLogger.log("MVC, before async service");
        sampleSerivce.asyncService();
        SecurityLogger.log("MVC, after async service");
       return "Async Service";
    }
    
결과는
MVC, before async service
Thread : http-nio-8080-exec-5
Principal : org.springframework.security.core.userdetails.User [Username=jaden, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]]
===================
MVC, after async service
Thread : http-nio-8080-exec-5
Principal : org.springframework.security.core.userdetails.User [Username=jaden, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]]
===================
Async Service
Thread : task-1
2020-12-20 15:49:27.174 ERROR 47831 --- [         task-1] .a.i.SimpleAsyncUncaughtExceptionHandler : Unexpected exception occurred invoking async method: public void me.summerbell.springsecurity.form.SampleSerivce.asyncService()

java.lang.NullPointerException: null

SecurityLogger.log("MVC, before async service");
sampleSerivce.asyncService();
SecurityLogger.log("MVC, after async service");

에서 sampleSerivce.asyncService(); 가 비동기적으로 동작함으로 위 코드에서

sampleSerivce.asyncService();
SecurityLogger.log("MVC, after async service");

의 순서는 보장할 수 가 없다 그래서
SecurityLogger.log("MVC, after async service");
가 먼저 찍히는 것을 확인할 수 있다.
그런데 async 한 동작을 위해 생성된 쓰레드 내부에서 Principal 이 null 이라
NPE 가 발생한것을 확인할 수 있다. 기본적으로 thread local 한 전략을 사용하기 때문에 발생한 이슈인데 이를 해결하기 위해선 스프링 시큐리티 설정을 바꿔주면 된다.

protected SecurityConfig() {
        SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
    }

MODE_INHERITABLETHREADLOCAL 이름에서 알 수 있듯이 쓰레드에서 생겨난 쓰레드에 대해서도 SecurityContext 를 공유하게 해주는 전략을 제공한다.
위와 같은 설정으로 변경하고 실행해보면

MVC, before async service
Thread : http-nio-8080-exec-5
Principal : org.springframework.security.core.userdetails.User [Username=jaden, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]]
===================
MVC, after async service
Thread : http-nio-8080-exec-5
Principal : org.springframework.security.core.userdetails.User [Username=jaden, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]]
===================
Async Service
Thread : task-1
Principal : org.springframework.security.core.userdetails.User [Username=jaden, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]]
===================
Async Service is called

다음과 같이 잘 task-1 thread 에서도 principal 을 공유할 수 있음을 확인할 수 있다.


SecurityContextPersistenceFilter

이 필터는 여러 요청간 SecurityContext 를 공유할 수 있도록 하는 filter 이다. 쉽게 말해, 로그인 이후에는 요청마다 또 인증을 하지 않고 기존에 로그인된 정보를 활용해 인증할 수 있게끔 도와주는 filter 이다.
SecurityContextRepository 를 활용하여 이와같은 일을 할 수 있도록 해주는데, 기본 구현체로 HttpSessionSecurityContextRepository 를 사용하고 있다.
HttpSessionSecurityContextRepository 는 이름처럼 Http Session을 활용하는데 최초요청에 의해 인증된 SecurityContext 정보를 session 에 저장해두었다가 다음 요청이 들어오면 session의 SecurityContext 을 이용하여 인증을 처리하고 이 과정이 바로 SecurityContextPersistenceFilter 에서 이루어 지는 것이다.
또 한, 요청처리가 끝난 이후에 이 필터에서 SecurityContextHolder 를 비워주는 역할도 하고 있다.

다음은 SecurityContextPersistenceFilter 의 doFilter() 내부의 일부 코드이다.

SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();		
// Crucial removal of SecurityContextHolder contents before anything else.
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());

---------- 코드 분석 
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
--> 사용된 SecurityContext를 SecurityContextHolder 에서 가져오고 

SecurityContextHolder.clearContext();
--> SecurityContextHolder 를 비워준 다음

this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
--> 가져온 securityContext를 SecurityContextRepository 에 저장한다. 

여기서, 잠시 왜 SecurityContextHolder 를 비워주는지에 대해 생각해보자

요청을 처리하고 SecurityContextHolder 를 비우는 이유?

SecurityContextHolder 는 ThreadLocal로 SecurityContext를 관리한다고 했다. 서블릿 기반의 웹프로그램은 요청마다 쓰레드를 생성하게 된다.
요청이후 다음 요청에서는 이전 쓰레드에서 만든 SecurityContext를 사용할 수 없다는 뜻이다.
그럼 쓰레드마다 SecurityContext가 생겨날텐데, 이 자원을 반납하지 않으면 쓰레드는 사라졌는데 해당자원은 남아 메모리 누수가 생기게 될 것이라는 것을 예상할 수 있다. 그럼으로 SecurityContextHolder 는 요청에 대한 작업이 끝나면 비워줘야한다.


*참고: 이 포스팅은 백기선님의 스프링시큐리티 강의를 듣고, 제 나름대로 다시 공부하고 정리한 포스팅 입니다. 만약, 조금 더 자세한 내용을 듣고 싶으시다면 이곳의 강의를 참고해보시기 바랍니다

0개의 댓글