[Spring Security in action] SecurityContext

2024. 1. 18. 13:34·보안

이 블로그는 개인의 공부 목적으로 작성된 블로그입니다. 왜곡된 정보가 포함되어 있을 수 있습니다.

SecurityContext

https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html

 

Servlet Authentication Architecture :: Spring Security

ProviderManager is the most commonly used implementation of AuthenticationManager. ProviderManager delegates to a List of AuthenticationProvider instances. Each AuthenticationProvider has an opportunity to indicate that authentication should be successful,

docs.spring.io

인증된 Authentication 인스턴스에 대해서 SecurityContext가 이를 관리한다. 따라서 우리는 인증 정보에 대해서 SecurityContext에 접근하기만 하면 알 수 있다. 

public interface SecurityContext extends Serializable {

	/**
	 * Obtains the currently authenticated principal, or an authentication request token.
	 * @return the <code>Authentication</code> or <code>null</code> if no authentication
	 * information is available
	 */
	Authentication getAuthentication();

	/**
	 * Changes the currently authenticated principal, or removes the authentication
	 * information.
	 * @param authentication the new <code>Authentication</code> token, or
	 * <code>null</code> if no further authentication information should be stored
	 */
	void setAuthentication(Authentication authentication);

}

SecurityContext에도 Authentication 객체에 대한 getter, setter를 확인 할 수 있다. 

공식문서에서 확인 할 수 있듯이 SecurityContext는 SecurityContextHolder에 의해 관리되는데, SecurityContextHolder는 다음 옵션을 지정하여 관리 할 수있다.

  • MODE_THREADLOCAL : 각 쓰레드가 보안 컨텍스트를 관리 한다.
  • MODE_INHERITABLETHREADLOCAL : 비동기의 메서드의 경우 요청 쓰레드와 응답 쓰레드가 서로 다른 쓰레드그 이기 때문에 이때 응답 응답 쓰레드가 요청 쓰레드를 복사한다.(상속한다)
  • MODE_GLOBAL : 모든 쓰레드가 같은 보안 쓰레드를 볼 수 있다.

MODE_THREADLOCAL 가 가장 일반적인 접근법이라고 한다. 인터페이스를 보면 getter, setter가 본인이 가지고 있는 Authentication을 반환하는 것을 확인 할 수 있다. 그러면 각각의 요청에 대해서 별개의 SecurityContext가 생성되고 SecurityContextHolder도 여러개 있는 건가? 아니면 하나의 SecurityContextHolder가 여러개의 SecurityContext를 관리하는 것인가?

public class SecurityContextHolder {
	//중략
	private static String strategyName = System.getProperty(SYSTEM_PROPERTY);

	private static SecurityContextHolderStrategy strategy;

	private static int initializeCount = 0;

	static {
		initialize();
	}

	private static void initialize() {
		initializeStrategy();
		initializeCount++;
	}

	private static void initializeStrategy() {
		if (MODE_PRE_INITIALIZED.equals(strategyName)) {
			Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
					+ ", setContextHolderStrategy must be called with the fully constructed strategy");
			return;
		}
   		if (!StringUtils.hasText(strategyName)) {
			// Set default
			strategyName = MODE_THREADLOCAL;
		}
		if (strategyName.equals(MODE_THREADLOCAL)) {
			strategy = new ThreadLocalSecurityContextHolderStrategy();
			return;
		}
		//중략
		try {
			Class<?> clazz = Class.forName(strategyName);
			Constructor<?> customStrategy = clazz.getConstructor();
			strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
		}
		catch (Exception ex) {
			ReflectionUtils.handleReflectionException(ex);
		}
	}

	public static SecurityContext getContext() {
		return strategy.getContext();
	}
    //중략
}

위 코드는 SecurityContextHolder 클래스이다. SecurityContextHolderStrategy를 static 맴버로 가지고 있고 initialize()에 의해 SecurityContextHolderStrategy인스턴스가 설정한 옵션에 의해 생성된다(위에 언급된 3개의 옵션)

코드에보면 new ThreadLocalSecurityContextHolderStrategy();에 의해 SecurityContextHolderStrategy형 인스턴스가 생성됨을 확인할 수 있다. SecurityContextHolderStrategy를 보자

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

	private static final ThreadLocal<Supplier<SecurityContext>> contextHolder = new ThreadLocal<>();

	@Override
	public void clearContext() {
		contextHolder.remove();
	}
    //중략
    
}

SecurityContextHolderStrategy은 SecurityContextHolderStrategy의 구현체로 쓰레드 마다 SecurityContext를 제공하는(공급자를 사용하고 있음) contextHolder를 맴버로 가지고 있다!

 

정리하자면 그러면 각각의 쓰레드에 대해서 별개의 SecurityContext가 생성되고 SecurityContextHolder가 이를 관리하는 형태이다. (결국 위그림의 형태)


MODE_THREADLOCAL 실습

    @GetMapping("/hello")
    public String hello() {
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication a = context.getAuthentication();

        return "hello "+a.getName()+"!";
    }

따로 옵션을 설정하지 않았기 때문에 MODE_THREADLOCAL로 설정된다. 


MODE_INHERITABLETHREADLOCAL  실습(비동기)

    @GetMapping("/hello")
    @Async
    public void hello() {
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication a = context.getAuthentication();
        String username=context.getAuthentication().getName();
    }

hello 메소드를 void 로 바꾸고, @Async 어노테이션을 추가 했다. (해당클래스에 @EnableAsync 을 추가)

getName()에서 NullPointException이 발생한다. (비동기는 요청과 응답이 다른 쓰레드 이기 때문에) 

심지어 성공(200)코드가 나오는 모습이다...(응답 객체를 주지 않아서 그런 것 같은데, 실제 개발할때 주의해야겠다.)

    @Bean
    public InitializingBean initializingBean(){
        return ()-> SecurityContextHolder.setStrategyName(
                SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
        );
    }

옵션 설정 정보를 config 파일에 추가하자!

 


SecurityContext는 요청 쓰레드 대해서만 접근이 보장되어 비동기와 같은 새로 생성된 쓰레드의 경우 별도로 처리를 해주어 SecurityContext가 관리할 수 있도록 명시해야한다고 한다. DelegatingSecurityContextRunnable을 이때 사용할 수 있다고 하는데 위처럼 MODE_INHERITABLETHREADLOCAL의 경우는 SecurityContext가 잘관리 될 것이고 그외의 경우가 아직 떠오르지 않아 이부분은 직접 개발을 해봐야 알 것 같다.


AuthenticationEntryPoint를 활용한 응답 실패

public class CustomEntryPoint implements org.springframework.security.web.AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.addHeader("message","i want to play tennis");
        response.sendError(HttpStatus.UNAUTHORIZED.value());
    }
}

AuthenticationEntryPoint 을 의 commence를 재정의함으로서 에러 header를 추가할 수 있다.

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                    .anyRequest().authenticated()
            )
            .authenticationProvider(customAuthenticationProvider())
            .httpBasic(c->{
                c.authenticationEntryPoint(new CustomEntryPoint());
            });
        return http.build();
    }

Config 의 filterChain에 httpBasic에 엔트리 포인트 선언

 


로그인 구현

방법1

https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/form.html

 

Form Login :: Spring Security

When the username and password are submitted, the UsernamePasswordAuthenticationFilter authenticates the username and password. The UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter, so the following diagram should look pr

docs.spring.io

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                    .anyRequest().authenticated()
            )
            .authenticationProvider(customAuthenticationProvider())
            .httpBasic(c->{
                c.realmName("OTHER");
                c.authenticationEntryPoint(new CustomEntryPoint());
            })
                .formLogin(formLogin->formLogin
                        .loginPage("/login")
                        .permitAll());

        return http.build();
    }
    //    @GetMapping("/home")
    //public String helloView() {
    //    return "home.html";
    //}

formLogin으로 로그인이 안되있는경우 login으로 redirct

controller에 어노테이션 @RestController -> @Controller 변경 (html 파일을 렌더링하기 위함)

방법2

    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new CustomAuthenticationSuccessHandler();
    }

    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler() {
        return new CustomAuthenticationFailureHandler();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests((authz) -> authz
                        .anyRequest().authenticated()
                )
                .authenticationProvider(customAuthenticationProvider())
                .httpBasic(c -> {
                    c.realmName("OTHER");
                    c.authenticationEntryPoint(new CustomEntryPoint());
                })
                .formLogin(forLogin -> forLogin
                        .successHandler(authenticationSuccessHandler())
                        .failureHandler(authenticationFailureHandler())
                );

        return http.build();
    }

successHandler, failureHandler을 구현하여(각각 AuthenticationSuccessHandler, AuthenticationFaliureHandler의 구현체)처리 

@Component
public class CustomAuthenticationSuccessHandler implements org.springframework.security.web.authentication.AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        Optional<? extends GrantedAuthority> auth = authorities.stream()
                .filter(a -> a.getAuthority().equals("read"))
                .findFirst();
        if(auth.isPresent()){
            response.sendRedirect("/home");
        }
        else{
            response.sendRedirect("/error");
        }

    }
}
@Component
public class CustomAuthenticationFailureHandler implements org.springframework.security.web.authentication.AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setHeader("failed", LocalDateTime.now().toString());
    }
}

/login 에 SpringSecurity에 내장된 기본 loginform제공

 

'보안' 카테고리의 다른 글

[Spring Security in action] 인증 필터  (0) 2024.01.20
[Spring Security in action] 권한부여  (0) 2024.01.20
[Spring Security in action] AuthenticationProvider을 사용한 인증  (0) 2024.01.16
[Spring Security in action] 사용자 관리  (0) 2024.01.15
[Spring Security in action] 스프링 시큐리티 시작하기  (0) 2024.01.11
'보안' 카테고리의 다른 글
  • [Spring Security in action] 인증 필터
  • [Spring Security in action] 권한부여
  • [Spring Security in action] AuthenticationProvider을 사용한 인증
  • [Spring Security in action] 사용자 관리
bluesparrow
bluesparrow
개인 공부 목적으로 작성된 블로그 입니다.
  • bluesparrow
    Bluesparrow
    bluesparrow
  • 전체
    오늘
    어제
    • 분류 전체보기 (91)
      • 회고 (4)
      • CS (17)
        • 운영체제 (1)
        • 컴퓨터구조 (2)
        • 데이터베이스 (5)
        • 네트워크 (9)
      • PS (7)
        • 백준 (7)
      • 사이드 프로젝트 (12)
      • AI (6)
        • 강화학습 (0)
        • 기계학습 (3)
      • 보안 (13)
      • Java (11)
        • 스프링 부트 (6)
      • 인프라 (4)
        • 도커 (3)
        • AWS (4)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 회고
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    강화학습
    트러블슈팅
    이펙티브 자바
    보안
    컴퓨터구조
    게임이론
    도커
    BFS
    회고
    JPA
    SpringSecurity
    논문
    그리디
    이분탐색
    그래프
    Spring
    자바
    조합론
    a
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
bluesparrow
[Spring Security in action] SecurityContext
상단으로

티스토리툴바