이 블로그는 개인의 공부 목적으로 작성된 블로그입니다. 왜곡된 정보가 포함되어 있을 수 있습니다.
SecurityContext
https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html
인증된 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
@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 |