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