본문 바로가기
모각코

[모각코 7회차] Spring Security Internals

by moonstal 2022. 8. 19.

목표: 스프링 시큐리티 파트1

Thread Per Request 모델과 ThreadLocal

  • Thread Per Request 모델
    • 병렬처리 기법 중 하나
    • WAS는 ThradPool을 생성하는데 HTTP 요청이 들어오면 Queue에 적재되고, ThreadPool 내의 특정 Thread가 Queue에서 요청을 가져와 처리
    • WAS의 최대 동시 처리 HTTP 요청의 갯수는 ThreadPool의 갯수와 같음
  • ThreadLocal
    • 동일 Thread 내에서는 언제든 읽고 쓸 수 있는 변수
    final static ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();

        public static void main(String[] args) {
            System.out.println(getCurrentThreadName() + " ### main get value =  1");
            threadLocalValue.set(1);
            a();
            b();

            //다른스레드에서 람다식 실행하는 비동기 실행코드
            CompletableFuture<Void> task = runAsync(() -> {
                a();
                b();
            });

            //람다식 코드 끝날때까지 대기
            task.join();
        }

출력

main ### main get value = 1

main ### a() get value = 1

main ### b() get value = 1

ForkJoinPool.commonPool-worker-3 ### a() get value = null

ForkJoinPool.commonPool-worker-3 ### b() get value = null

스레드 풀과 ThreadLocal 변수 함께 사용할 때 주의 할 점

  • ThreadPool과 함께 사용하는 경우 Thread가 ThreadPool에 반환되기 직전 ThreadLocal 변수 값을 반드시 제거
  • 이전 요청 처리에 사용된 ThreadLocal 변수가 남아있으면 이를 참조하여 잘못된 동작을 수행할 수 있기 때문

SecurityContextHolder, SecurityContext, Authentication

  • SecurityContextHolder는 SecurityContext 데이터를 쓰거나 읽을수 있는 API를 제공 (

    • 기본 구현은 ThreadLocal를 이용

    • Thread Per Request 모델을 고려

    • SecurityContextHolder.clearContext()통해 변수 값 제거

        final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
      
            private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
      
            //구현
            clearContext()
            getContext()
            setContext(SecurityContext context)
            createEmptyContext()
      
        }
  • SecurityContext

    • SecurityContextHolder 클래스를 통해 코드 어느 부분에서든 SecurityContext에 접근
    • SecurityContextHolder.getContext();
    • SecurityContext 안에 getAuthentication();
  • Authentication

    • 사용자를 표현하는 인증 토큰 인터페이스

      jsp를 타임리프로 바꾸면서 스프링 시큐리티에서 객체를 어떻게 가져오는지 해결만하고 끝냈었는데 여기에 이렇게 구현되어 있었다니 너무 반갑다!! 문제를 해결만 할게 아니라 깊게 파는 것도 중요할 것 같다.

정리하면 이런 그림
출처

인증 처리

사용자가 주장하는 본인이 맞는지 확인하는 절차를 의미(아이디/비밀번호)

  • DefaultLoginPageGeneratingFilter
    • HTTP GET 요청에 대해 디폴트 로그인 페이지를 생성
    • 커스터마이징
      http
      .formLogin()
      .loginPage("/my-login")
      .usernameParameter("my-username")
      .passwordParameter("my-password")
  • AbstractAuthenticationProcessingFilter
    • 사용자 인증을 처리
    • 구현체 UsernamePasswordAuthenticationFilter
    • Authentication 객체 생성
    • UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
    • return this.getAuthenticationManager().authenticate(authRequest);
    • AuthenticationManager, ProviderManager
      • AuthenticationManager 인터페이스는 사용자 인증을 위한 API를 제공
      • 구현체 ProviderManager
  • RememberMeAuthenticationFilter
    • remember-me 쿠키를 갖고 있다면 사용자를 자동으로 인증
    • RememberMeServices로 사용자 인증
    • RememberMeAuthenticationToken
      • Authentication 인터페이스 구현체
    • RememberMeAuthenticationProvider: RememberMeAuthenticationToken 기반 인증 처리
    • remember-me 설정 시 입력한 key 값 검증

세션 처리

  • SecurityContextPersistenceFilter

    • 사용자의 SecurityContext를 가져오거나 갱신
    • HttpSessionSecurityContextRepository 클래스가 구현
  • SessionManagementFilter

    • 세션 고정 보호: session-fixation attack(정상 사용자의 세션을 탈취하여 인증 우회) 방지

    • 세션 생성 전략

      • IF_REQUIRED: 필요시 생성 (기본값)

      • NEVER: Spring Security에서는 세션을 생성x 세션 존재하면 사용

      • STATELESS: 세션 사용x (JWT 인증이 사용되는 REST API 서비스에 적합)

      • ALWAYS: 항상 세션 사용

        @Override
        protected void configure(HttpSecurity http) throws Exception {
        http
          .sessionManagement() 
        
          //새로운 세션을 만들지 않지만, session-fixation 공격 방어
          .sessionFixation().changeSessionId()
        
          //필요시 생성
          .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
        
          //유효하지 않은 세션 감지 시 지정된 URL로 리다이렉트
          .invalidSessionUrl("/")
        
          //동일 사용자의 최대 동시 세션 개수
          .maximumSessions(1)
              //최대 개수를 초과시 인증 시도 차단 여부(기본false)
              .maxSessionsPreventsLogin(false)
                      .and()
          .and()
        ;
        }

인가 처리

권한이 부여된 사용자들만 특정 기능 또는 데이터에 접근 허용

  • 인증된 사용자와 권한을 매핑(ROLE_USER)
  • 보호되는 리소스에 대한 권한 확인(관리자 페이지)

사진
출처

  • FilterSecurityInterceptor
    • 사용자의 권한과 리소스에서 요구하는 권한을 취합-> 접근 허용 결정(AccessDecisionManager 구현)
    • 익명 사용자(ROLE_ANONYMOUS)도 인증된것!
    • 권한 정보 SecurityMetadataSource 통해 ConfigAttribute 가져옴
  • AccessDecisionManager
    • 사용자의 권한과 리소스에서 요구하는 권한 확인후 접근처리(AccessDecisionVoter로 판단)
  • AccessDecisionVoter
    • 접근 승인, 거절, 보류 판단
    • WebExpressionVoter가 구현
      • WebSecurityExpressionRoot클래스 SpEL 표현식 사용해 접근 승인 여부 규칙 지정
        @Override
        protected void configure(HttpSecurity http) throws Exception {
        http
              .authorizeRequests()
              .antMatchers("/me").hasAnyRole("USER", "ADMIN") //인증 영역
              .antMatchers("/admin").access("isFullyAuthenticated() and hasRole('ADMIN')") //isFullyAuthenticated 리멤머미로 인증되지 않은 사용자
              .anyRequest().permitAll(); //익명영역
              }

인증 이벤트

  • 인증 성공 또는 실패가 발생했을 때 관련 이벤트(ApplicationEvent) 발생(AuthenticationEventPublisher)
  • 해당 이벤트에 관심있는 컴포넌트는 이벤트 구독
  • 컴포넌트 간의 느슨한 결합 유지 위해
      @Async //얘 오래걸릴 수 있음 -> 스레드 분리(비동기처리)
      @EventListener
      public void handleAuthenticationSuccessEvent(AuthenticationSuccessEvent event) {
          //Async 전: 5초 후 로그인
          //Async 후: 로그인 되고 5초 후 로그찍힘, 스레드 달라짐
          try {
              Thread.sleep(5000L);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          Authentication authentication = event.getAuthentication();
          log.info("Successful authentication result: {}", authentication.getPrincipal());
      }

그 밖의 필터

  • HeaderWriterFilter: 응답 헤더에 보안 관련 헤더 추가
    • XContentTypeOptionsHeaderWriter: MIME sniffing 공격 방어
    • XXssProtectionHeaderWriter: 브라우저에 내장된 XSS(Cross-Site Scripting) 필터 활성화
    • CacheControlHeadersWriter: 캐시 사용하지 않도록 설정
    • XFrameOptionsHeaderWriter: clickjacking(다른 것을 클릭하게 속임) 공격 방어
    • HstsHeaderWriter: HTTPS만을 사용하여 통신
  • CsrfFilter
    • CSRF (Cross-site request forgery): 공격자가 의도한 행위를 특정 웹사이트에 요청(사용자의 권한 도용 -> 중요 기능 실행)
    • Referrer 검증: Request의 referrer 확인
    • CSRF Token 활용: 세션에 임의의 토큰 값을 저장
    • 타임리프 th:action="@{/send}" 써야 인풋타입히든에 토큰 만들어줌!
  • WebAsyncManagerIntegrationFilter
    • MVC Async Request가 처리될 때, 쓰레드간 SecurityContext를 공유할수 있게 해줌
    • @Async 어노테이션을 추가한 Service 레이어 메소드에는 해당 안됨
    • MODE_INHERITABLETHREADLOCAL으로 변경하면 가능(ThreadLocal->InheritableThreadLocal) 비권장
    • ThreadLocal의 clear 처리가 제대로되지 않아 문제될 수 있음
  • DelegatingSecurityContextAsyncTaskExecutor
    • Pooling 되지않는 TaskExecutor와 함께 사용해야 함(SimpleAsyncTaskExecutor-매번 새로운 스레드 생성)
    • 스레드 풀 사용하면서 스프링시큐리티 참조 다른 스레드로전파 어떻게?
    • DelegatingSecurityContextRunnable 객체 생성자에서 SecurityContextHolder.getContext() 메소드를 호출하여 SecurityContext 참조 획득

@프로그래머스 미니 데브코스 & CNU SW Academy 강의 내용 정리