[8/4 TIL] SPRING SECURITY(인증 이벤트, HeaderWriterFilter, CsrfFilter, BasicAuthenticationFilter, WebAsyncManagerIntegrationFilter)

yumyeonghan·2023년 8월 7일
0

🍃프로그래머스 백엔드 데브코스 4기 교육과정을 듣고 정리한 글입니다.🍃

Spring Security 인증 이벤트

  • 인증 성공 또는 실패가 발생했을 때 관련 이벤트(ApplicationEvent)가 발생하고, 이에따라 동작하는 이벤트 리스너를 추가할 수 있다.

AuthenticationEventPublisher

public interface AuthenticationEventPublisher {

	void publishAuthenticationSuccess(Authentication authentication);

	void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication);

}
  • 인증 성공 또는 실패가 발생했을 때 이벤트를 전달하기 위한 인터페이스
  • 기본 구현체로 DefaultAuthenticationEventPublisher 클래스가 사용됨

이벤트의 종류

  • AuthenticationSuccessEvent: 로그인 성공 이벤트
  • AbstractAuthenticationFailureEvent: 로그인 실패 이벤트
    • 실패 이유에 따라 다양한 구체 클래스가 정의되 있기 때문에 추상 클래스

이벤트 리스너

@Component
public class CustomAuthenticationEventHandler {

    private final Logger log = LoggerFactory.getLogger(getClass());

    @Async
    @EventListener
    public void handleAuthenticationSuccessEvent(AuthenticationSuccessEvent event) throws InterruptedException {
        //@Async, 쓰레드 분리 실습
        Thread.sleep(5000l);

        Authentication authentication = event.getAuthentication(); //인증이 성공적으로 완료된 사용자 객체
        log.info("Successful authentication result: {}", authentication.getPrincipal());
    }

    @EventListener
    public void handleAuthenticationFailureEvent(AbstractAuthenticationFailureEvent event) {
        Exception e = event.getException();
        Authentication authentication = event.getAuthentication();
        log.warn("Unsuccessful authentication result: {}", authentication, e);
    }
}
  • @EventListener 어노테이션을 이용하여 리스너 등록
  • Spring의 이벤트 모델이 동기적이인 방식으로 작동
    • 로그인 성공 시 인증 이벤트를 발행
    • 해당 이벤트를 처리하는 리스너에서의 로직이 오래 걸림
    • 해당 리스너의 작업이 끝날 때까지 로그인 후의 리다이랙트 같은 작업이 실행되지 못하고 대기
  • @EnableAsync로 비동기 처리를 활성화하고, @Async 어노테이션을 사용해 이벤트 리스너를 비동기로 변경할 수 있음
    @EnableAsync //@Async 사용을 위해 설정
    @Configuration
    public class WebMvcConfigure implements WebMvcConfigurer {
    }

HeaderWriterFilter

  • 응답 헤더에 보안 관련 헤더를 추가함
    • XContentTypeOptionsHeaderWriter: MIME sniffing 공격 방어
    • XXssProtectionHeaderWriter: XSS 공격 방어
    • CacheControlHeadersWriter: 캐시를 사용하지 않도록 설정
    • XFrameOptionsHeaderWriter: clickjacking 공격 방어
    • HstsHeaderWriter: HTTP 대신 HTTPS만을 사용하여 통신해야함을 브라우저에 알림

CsrfFilter

그림 참조

  • CSRF (Cross-site request forgery): 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위를 특정 웹사이트에 요청하게 하는 공격을 말함

예방 방법

  • Referrer 검증

    • Request의 referrer를 확인하여 domain이 일치하는지 확인
  • CSRF Token 활용(권장)

    1. CSRF 토큰의 사용: 로그인 완료 여부와 상관없이 사용자의 세션에 임의의 토큰 값을 저장하여, 모든 리소스 변경 요청(POST, PUT, DELETE 등)을 위한 요청마다 이 토큰 값을 요청 파라미터로 함께 전송

      • 이렇게 하면 서버는 요청을 처리하기 전에 해당 토큰 값을 검증
    2. 토큰 값의 일치 검증: 서버는 클라이언트로부터 받은 요청의 토큰 값과 사용자의 세션에 저장된 토큰 값이 일치하는지 검증

      • 이렇게 하면 공격자가 클라이언트에서 임의로 생성한 토큰 값을 사용해도 검증에 실패
  • CsrfFilter는 요청이 리소스를 변경해야 하는 요청인지 확인하고, 맞다면 CSRF 토큰을 검증함 (기본적으로 활성화됨)

    • CsrfTokenRepository — CSRF 토큰 저장소 인터페이스이며 기본 구현체로 HttpSessionCsrfTokenRepository 클래스가 사용됨

BasicAuthenticationFilter

  • Basic 인증을 처리하는 필터
  • HTTP 요청 헤더에 username과 password를 Base64 인코딩하여 포함
    • Authorization: Basic dXNlcjp1c2VyMTIz
  • HTTPS 프로토콜에서만 제한적으로 사용해야 함
  • Form 인증과 동일하게 UsernamePasswordAuthenticationToken을 사용함
  • http.httpBasic() 활성화 시킴 (기본값은 비활성화)

WebAsyncManagerIntegrationFilter

  • WebAsyncManagerIntegrationFilter는 MVC Async Request가 처리될 때, 쓰레드간 SecurityContext를 공유할수 있게 해줌
    • Spring MVC Async Request는 컨트롤러 반환타입이 Callable임
    • 원래 SecurityContext는 ThreadLocal 변수를 이용하고 있고, 따라서 다른 쓰레드에서는 SecurityContext를 참조할수 없어야 함

@GetMapping(path = "/asyncHello")
@ResponseBody
public Callable<String> asyncHello() {
  log.info("[Before callable] asyncHello started.");
  Callable<String> callable = () -> {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    User principal = authentication != null ? (User) authentication.getPrincipal() : null;
    String name = principal != null ? principal.getUsername() : null;
    log.info("[Inside callable] Hello {}", name);
    return "Hello " +  name;
  };
  log.info("[After callable] asyncHello completed.");
  return callable;
}
  • 위 코드를 실행해보면, Callable 실행 로직이 다른 쓰레드에서 실행되었음에도 SecurityContext를 참조하는것을 알 수 있음
  • 단, 위 기능은 Controller 메서드인 Spring MVC Async Request 처리에서만 적용되며 @Async 어노테이션을 추가한 Service 레이어 메소드에는 해당 안됨

@Async 어노테이션을 추가한 Service 레이어 메소드에는 해당 안되는 문제를 어떻게 해결할까?

DelegatingSecurityContextAsyncTaskExecutor 사용

	@Bean
    @Qualifier("myAsyncTaskExecutor")
    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(3); //풀 내 초기 스레드 수 설정
        executor.setMaxPoolSize(5); //풀 내 허용되는 최대 스레드 수 설정
        executor.setThreadNamePrefix("my-executor-"); //풀 내 스레드 이름에 붙을 접두사 설정
        return executor;
    }

    @Bean
    public DelegatingSecurityContextAsyncTaskExecutor taskExecutor(@Qualifier("myAsyncTaskExecutor") AsyncTaskExecutor delegate) {
        return new DelegatingSecurityContextAsyncTaskExecutor(delegate);
    }
  • DelegatingSecurityContextAsyncTaskExecutor는 Spring Security 프레임워크에서 제공하는 비동기 작업(TaskExecutor)을 위한 래퍼(wrapper) 클래스
  • 비동기 작업을 처리하는 동안 Spring Security의 SecurityContext를 유지하기 위한 목적으로 사용
  • 비동기 작업이 수행되는 동안 SecurityContext를 변경하지 않고, 비동기 작업이 완료되면 원래의 SecurityContext로 복원
  • 비동기 작업을 처리할 때 내부적으로 DelegatingSecurityContextRunnable 객체를 생성하여 사용
    • 이 객체는 생성자에서 SecurityContextHolder.getContext() 메소드를 호출하여 현재 SecurityContext를 얻어옴
profile
웹 개발에 관심 있습니다.

0개의 댓글