DelegatingProxy
DelegatingFilterProxy 은 서블릿 필터에서 서블릿 필터를 구현한 스프링 빈에게 요청을 위임해주는
대리자 역할의 서블릿 필터입니다.
DelegatingFilterProxy 존재 이유
1. 서블릿 필터는 스프링에서 정의 된 빈을 주입해서 사용할 수 없습니다.
왜냐하면 Spring Bean 은 스프링 컨테이너에서 생성 및 관리하는 컴포넌트이고,
ServletFilter 는 서블릿 컨테이너에서 생성및 관리하는 필터들이기 때문에 서로 실행되는 위치가 다르기 때문입니다.
하지만 스프링 시큐리티로 사용자의 요청을 인증, 인가 처리 할 때는 필터 기반으로 처리하므로
서블릿 필터와 스프링 빈들을 서로 호출하며 사용해야 합니다.
이 때 서블릿 필터가 DelegatingFilterProxy 클래스를 사용해서 스프링 빈에게 요청을 위임하면
서블릿 필터를 구현한 스프링 빈을 이용해 책임을 수행하게 됩니다.
간단하게 정리하자면 클라이언트 요청이 들어오면 서블릿 컨테이너에 있는 필터가 먼저 받게 되는데
이 필터중에 DelegatingFilterProxy 가 요청을 스프링 시큐리티에서 생성된 필터에게도 전달해주는 것입니다.
2. 특정한 이름을 가진 스프링 빈을 찾아 그 빈에게 요청을 위임합니다.
springSecurityFilterChain 이름으로 생성된 빈을 ApplicationContext 에서 찾아 요청을 위임합니다.
springSecurityFilterChain 이름으로 생성된 빈은 FilterChainProxy 입니다.
DelegatingFilterProxy 는 실제 보안처리를 하지 않습니다.
FilterChainProxy
FilterChainProxy 는 스프링 시큐리티 초기화 시 생성되는 필터들을 관리하고
제어하며 스프링 시큐리티가 기본적으로 생성하는 필터입니다. 사용자의 요청을
필터 순서대로 호출하여 전달하고 인증/ 인가처리 및 각종 요청에 대한 처리를 수행합니다.
마지막 필터까지 인증 및 인가 예외가 발생하지 않으면 보안이 통과되서 서블릿에 접근하게 됩니다.
springSecurityFilterChain 의 이름으로 생성되는 필터 빈이며
DelegatingFilterProxy 로 부터 요청을 위임 받고 실제 보안 처리를 합니다.
스프링 시큐리티 설정 클래스에서 API 추가하면 필터를 추가할 수 있고
사용자정의 필터를 생성해서 기존의 필터 전,후로 추가 할 수도 있습니다.
DelegatingFilterProxy 와 FilterChainProxy 흐름
1. 사용자가 url 요청을 합니다.
2. 서블릿 컨테이너의 필터들이 처리를 하게 되고 그 중 DelegatingFilterProxy 가 요청을 받게 될 경우
FilterChainProxy 에게 요청을 위임합니다.
3. FilterChainProxy 는 등록되어 있는 필터들을 하나씩 호출하며 보안 처리를 진행합니다.
4. 보안 처리가 완료되면 요청을 서블릿에게 전달합니다.
필터 초기화와 다중 보안 설정
스프링 시큐리티에서는 보안 설정을 여러 개의 설정을 만들어서 동시에 사용할 수 있습니다.
설정클래스 별로 보안 기능이 작동, RequestMatcher 가 설정, 필터가 생성됩니다.
FilterChainProxy 가 각 필터들을 가지고 있으며
요청에 따라 RequestMatcher 와 매칭되는 필터가 작동합니다.
다중 보안 설정시 흐름 예시
1. GET방식으로 /admin 주소로 자원 요청합니다.
2. FilterChainProxy 에서 요청을받아 요청을 처리할 필터를 선택합니다.
3. 요청 URL과 matches를 하여 true가되는 Filter를 선택해야합니다.
FilterChainProxy가 저정하고있는 각각의 SecurityConfig 객체들에서
RequestMacher 의 정보와 매치되는 정보를 찾습니다.
4.일치하는 객체의 필터를 수행하여 인증/인가 처리를 합니다.
@Configuration
@EnableWebSecurity
@Order(0)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/admin/**")
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic();
}
}
@Configuration
@Order(1)
class SecurityConfig2 extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().permitAll()
.and()
.formLogin();
}
}
@Controller
public class SecurityControllerV3 {
@GetMapping("/")
@ResponseBody
public String hello() {
return "hello";
}
@GetMapping("admin/**")
@ResponseBody
public String admin() {
return "admin";
}
}
예제 코드입니다. securityFilterChain() 메서드는 httpBasic 인증 방식이며 /admin url 요청이 들어오면
인증된 사용자만 접근이 가능하도록 설정했습니다. securityFilterChain2() 메서드는 formLogin() 인증 방식이며
모든 url 요청을 인증받지 않은 사용자와 인증된 사용자가 접근할 수 있도록 설정했습니다.
@Configuration
public class SecurityConfiguration3 {
@Bean
@Order(0)
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.requestMatchers("/admin/**").authenticated()
.and()
.httpBasic();
return http.build();
}
@Bean
@Order(1)
public SecurityFilterChain securityFilterChain2(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().permitAll()
.and()
.formLogin();
return http.build();
}
}
만약 Spring Security 5.7.1이상 또는 Spring Boot 2.7.0 이상이라면
스프링 시큐리티 설정 클래스를 위 코드로 작성해야 합니다.
admin url 요청을 하면 securityFilterChain() 의 securityFilterChain 이 동작하고 인증되지 않은 사용자로
판단해 httpBasic() 의 로그인 창을 띄워줍니다.
/ url 요청을 하면 securityFilterChain2() 의 securityFilterChain 이 동작해 인증되지 않은 사용자여도
보안을 통과시켜줍니다. securityFilterChain2() 에서 모든 url 요청을 인증받지 않은 사용자와 인증된 사용자가
접근할 수 있도록 설정했는데 admin url 요청을 했을 때는 왜 보안에 통과되지 않을까요?
이건 @Order 어노테이션으로 securityFilterChain() 에 더 높은 우선순위를 줘서
securityFilterChain() 의 securityFilterChain 이 먼저 동작해 보안 검사를 했기 때문입니다.
우선순위는 @Order 에 주는 숫자가 더 작은 숫자일수록 높습니다.
@Configuration
public class SecurityConfiguration3 {
@Bean
@Order(1)
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.requestMatchers("/admin/**").authenticated()
.and()
.httpBasic();
return http.build();
}
@Bean
@Order(0)
public SecurityFilterChain securityFilterChain2(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().permitAll()
.and()
.formLogin();
return http.build();
}
}
위 코드처럼 securityFilterChain2() 의 우선순위가 더 높다면 admin url 요청을 했을 때
securityFilterChain2() 의 securityFilterChain 이 먼저 동작해 보안 검사를 통과합니다.
@Order 로 우선순위를 설정할 때는 더 넓은 범위의 요청을 검사하는 securityFilterChain 이
순위가 더 낮아야 합니다. 현재처럼 접근을 막아야하는 url 요청을 통과시켜버리는 상황이 생길 수 있기 때문입니다.
̱ Authentication 인증 객체
Authentication 은 사용자의 인증 정보를 저장하는 토큰 개념이라고 할 수 있습니다.
인증 시 id 와 password 를 담고 인증 검증을 위해 전달되어 사용됩니다.
인증 후 최종 인증 결과 (user 객체, 권한정보) 를 담고 SecurityContext 에 저장되어 전역적으로 참조가 가능합니다.
Authentication authentication = SecurityContexHolder.getContext().getAuthentication()
구조
principal : 사용자 아이디 혹은 User 객체를 저장
credentials : 사용자 비밀번호
authorities : 인증된 사용자의 권한 목록
details : 인증 부가 정보
Authenticated : 인증 여부
1. 사용자가 로그인을 시도합니다.(username, password 입력 및 전달)
2. UsernamePasswordAuthenticationFilter(인증 필터) 가 요청 정보를 받아 정보를 추출해
인증 객체 (Authentication)를 생성합니다.
3. AuthenticationManager가 인증객체를 가지고 인증처리를 수행합니다. 인증이 실패 하게 되면 예외를 발생시킵니다.
4. 인증 성공 후 Authentication 인증객체를 만들어서 내부의
Principal, Credentials, Authorities, Authenticated 들을 채워넣습니다. Credentials 은 보안상 비워둘 수 있습니다.
5. SecurityContextHolder 객체 안의 SecurityContext 에 저장하여 인증 객체를 전역적으로 사용할 수 있게 됩니다.
SecurityContext, SecurityContextHolder
SecurityContext
SecurityContext 는 Authentication 객체가 저장되는 보관소로 필요 시
언제든지 Authentication 객체를 꺼내어 쓸 수 있도록 제공되는 클래스입니다.
ThreadLocal에 저장되어 아무 곳에서나 참조가 가능하도록 설계되었습니다.
ThreadLocal: Thread마다 할당된 고유 공간입니다. 서로 공유하지 않기 때문에 다른 Thread로부터 안전합니다.
get, set, remove api가 있으며 set 한 이후 get할 때 장소의 제약이 없습니다
ex) A메서드에서 set한 내용을 B메서드에서 get하는것이 가능합니다.
인증이 완료되면 HttpSession에 저장되어 어플리케이션 전반에 걸쳐 전역적인 참조가 가능합니다.
SecurityContextHolder
SecurityContextHolder 은 SecurityContext 객체 저장 방식입니다.
MODE_THREADLOCAL : 스레드당 SecurityContext 객체를 할당합니다. (기본값)
MODE_INHERITABLETHREADLOCAL : 메인 스레드와 자식 스레드에 관하여 동일한 SecurityContext 를 유지합니다.
MODE_THREADLOCAL 방식 같은 경우에는 각 스레드마다 THREADLOCAL 이 있어 데이터 공유가 되지 않습니다.
MODE_GLOBAL : 응용 프로그램에서 단 하나의 SecurityContext를 저장합니다.
SecurityContextHolder.clearContext() : SecurityContext 기존 정보를 초기화합니다.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication()
이 코드는 어떤 메서드에서도 참조가 가능하며, 인증 객체를 어디서든 꺼내어 사용할 수 있습니다.
SecurityContext, SecurityContextHolder 흐름
1. 사용자가 로그인을 합니다.
2. Server 가 요청을 받아서 Thread를 생성합니다. (ThreadLocal 할당)
3. thread 가 인증 처리를 시도합니다. 인증 객체(Authentication) 생성합니다.
4. 인증에 실패하면 SecurityContextHolder.clearContext() 인증객체 초기화
5. 인증에 성공하면 SecurityContextHolder 안의 SecurityContext 에 인증 객체를 저장합니다.
ThreadLocal이 SecurityContextHolder를 담습니다.
6. SecurityContext에서 최종적으로 HttpSession에 SPRING_SECURITY_CONTEXT라는 이름으로 저장 됩니다.
SecurityContextHolder 의 SecurityContext 저장 방식에 따른 차이점
1. MODE_THREADLOCAL
@RestController
public class SecurityControllerV4 {
@GetMapping("/")
public String index(HttpSession session){
//SecurityContextHolder 에서 인증 객체 꺼내오기
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println("SecurityContextHolder 에서 꺼낸 인증 객체 = " + authentication + "\n");
//세션에서 인증 객체 꺼내 오기
SecurityContext context = (SecurityContext) session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
Authentication authentication1 = context.getAuthentication();
System.out.println("세션 에서 꺼낸 인증 객체 = " + authentication1 + "\n");
return "home";
}
@GetMapping("/thread")
public String thread(){
new Thread(
new Runnable() {
@Override
public void run() {
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
System.out.println("자식 스레드에서 꺼낸 인증 객체 = " + authentication);
}
}
).start();
return "thread";
}
}
메인 스레드와 자식 스레드간의 securityContext 객체가
공유가 안 되는지 확인하기 위해 thread() 메서드를 생성했습니다.
콘솔에 찍힌 결과를 보면 세션에서 꺼낸 인증 객체와 SecurityContextHolder 에서 꺼낸 인증 객체는 동일하지만
자식 스레드에서 가져온 인증 객체는 null 인 것을 확인할 수 있습니다. 로그인을 성공하면 메인 스레드의
ThreadLocal 에 담았기 때문에 자식 스레드의 ThreadLocal 에서 securityContext 객체를 꺼내면 null 일 수 밖에
없습니다.
2. MODE_INHERITABLETHREADLOCAL
@Configuration
public class SecurityConfiguration4 {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().permitAll()
.and()
.formLogin();
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
return http.build();
}
}
THREADLOCAL 전략을 MODE_INHERITABLETHREADLOCAL 로 변경하고 다시 실행해보겠습니다.
이번에는 자식 스레드에서도 같은 인증 객체가 꺼내진걸 확인할 수 있습니다.
̱ SecurityContextPersistenceFilter
SecurityContextPersistenceFilter 는 SecurityContext 객체를 생성, 저장, 조회하는데 사용됩니다.
익명 사용자
새로운 SecurityContext 객체를 생성하여 SecurityContextHolder 에 저장합니다.
SecurityContext 에는 인증 객체가 아닌 AnonymousAuthenticationFilter 에서
AnonymousAuthenticationToken 객체를 저장합니다.
인증 시
새로운 SecurityContext 객체를 생성하여 SecurityContextHolder 에 저장합니다.
UsernamePasswordAuthenticationFilter 에서 인증 성공 후 SecurityContext 에
UsernamePasswordAuthentication 객체를 SecurityContext 에 저장합니다.
인증이 최종 완료되면 Session 에 SecurityContext 를 저장합니다.
인증 후
Session 에서 SecurityContext 꺼내어 SecurityContextHolder 에서 저장합니다.
SecurityContext 안에 Authentication 객체가 존재하면 계속 인증을 유지합니다.
최종 응답 시 공통으로 처리하는 로직
SecurityContextHolder.clearContext() 메서드를 호출해 인증 정보를 초기화합니다.
매 요청마다 새로운 SecurityContext 객체를 생성하여 SecurityContextHolder 에 저장하기
때문에 웅답할 때는 SecurityContextHolder 안의 SecurityContext 객체에서 보관하던 인증정보를
반드시 초기화 해줘야 합니다.
SecurityContextPersistenceFilter 흐름
1. 사용자가 서버에 요청합니다.
2. SecurityContextPersistenceFilter 는 인증 받기 전 사용자이든, 인증 받은 후 사용자이든,
익명 사용자이든 관계없이 매 번 요청마다 수행됩니다.
3. SecurityContextPersistenceFilter 내부적으로 HttpSecurityContextRepository 가 로직을 수행합니다.
HttpSecurityContextRepository 는 SecurityContext 객체를 생성, 조회 하는 역할을 하는 클래스입니다.
인증 전
1. 새로운 컨텍스트 생성합니다. 이 때 SecurityContext 안에 인증 객체는 null 입니다.
2. 그 다음 필터로 이동합니다.
3. 인증 필터가 인증을 처리 합니다.
4. 인증이 완료되면 인증 객체(Authentication) 생성 후 SecurityContext 객체안에 저장됩니다.
5. 다음 필터를 수행합니다.
6. Client 에게 응답하는 시점에 SecurityContextPerstenceFIlter 가 Session에 SecurityContext 를 저장합니다.
7. SecurityContext 제거합니다.
인증 후
1. Session에서 SecurityContext가 있는지 확인하고 꺼냅니다.
2.꺼낸 SecurityContext 를 SecurityContextHolder에 집어넣습니다.
3. 다음 필터를 수행합니다.
SecurityContextPersistenceFIlter는 인증 전(좌측) 인증 후(우측)으로 나뉩니다. 인증 후에는 Session에서
SecurityContext를 꺼내 SecurityContextHolder에 집어넣은 뒤 다음 필터들을 수행하고
인증 전이라면 꺼낼 SecurityContext는 없기 때문에 SecurityContextHolder를 생성한 뒤
인증필터(AuthFilter)를 수행 후 생성된 인증객체(Authentication)을 SecurityContex t에 담은 뒤 다음 로직을 수행합니다.
마지막으로 Client 에게 응답하기 전 Session 에 SecurityContext 를 담고
SecurityContext 를 초기화한 후 응답합니다.
Authentication Flow
Authentication 흐름
1. Client 에서 로그인 요청을 합니다.
2. UsernamePasswordAuthenticationFIlter에서 ID + PASSWORD 를 담은 인증객체(Authentication)를 생성합니다.
3. AuthenticationManager에게 인증객체를 넘기며 인증 처리를 위임합니다.
내부에 한 개 이상의 Provider를 담은 List 를 가지고 있고
그 List 에서 인증 처리를 할 수 있는 적절한 Provider를 찾습니다.
4. AuthenticationManager 가 적절한 Provider 에게 인증처리를 위임합니다.
5. 해당 Provider는ID + PASSWORD 를 가지고 실제 인증 처리를 합니다.
lUserDetailsService 인터페이스에게 loadUserByUsername(username)메서드를 호출해서 유저객체를 요청합니다.
Repository 에 findById() 메서드로 유저 객체를 조회를 합니다. (DB 사용을 가정)
만약, 해당 사용자 객체가 존재하지 않으면 UsernameNotFoundException 을 발생시키고
UsernamePasswordAuthenticationFIlter 에서 예외를 처리합니다. (FailHandler() 에서 후속처리)
사용자 객체가 존재한다면 UserDetails 타입으로 반환됩니다.(Member 객체도 UserDetails 객체로 변환되어 반환)
6. 인증관리자는 이제 Password 검증을 시작합니다.
인증객체의 Password 와 반환받은 UserDetails 의 Password 를 비교합니다.
일치하지 않을 경우 BadCredentialException 예외를 발생시키고 인증을 실패 처리합니다.
일치한다면 성공한 인증 객체(UserDetals와 authorities를 담은 인증 후 토큰 객체 Authentication)를
UsernamePasswordAuthenticationFilter 에 전달합니다.
7. SecurityContext에 저장합니다.
8. 이후 전역적으로 SecurityContextHolder 에서 인증 객체를 사용 가능하게 됩니다.
AuthenticationManager
인증 관리자(AuthenticationManager)는 필터로부터 인증 처리를 지시받으면 가지고 있는인증 처리자(AuthenticationProvider)들 중에서 현재 인증 처리를 할 수 있는 Provider 에게 인증처리를 위임하여 인증처리 수행 후 인증 성공을 한다면 반환받은 인증객체를 필터로 전달합니다. 만약, 적절한 Provider 를 못찾는다면 자신의 부모 객체에 저장된 ProviderManager 도 검색하여 해당 인증을 처리할 수 있는 Provider 가 있으면 인증처리를 위임하여 반환합니다.
위 그림에서 Form 인증의 인증 처리를 할 수 있는 Provider 는 DaoAuthenticationProvider 이고
Remember Me 인증의 인증 처리를 할 수 있는 Provider 는 RememberMeAuthenticationProvider 입니다.
OAuth 인증 같은 경우에는 처리할 수 있는 Provider 가 없기 때문에 parent 속성에 있는
또 다른 ProvideManager 객체에서 검색하여 OauthAuthenticationProvider 를 찾습니다.
AuthenticationProvider
AuthenticationProvider 도 AuthenticationManager 처럼 인터페이스입니다.
AuthenticationProvider 에는 두 개의 메서드가 제공되는데요.
authenticate(authentication): 실제적인 인증처리를 위한 검증 메서드입니다.
supports(authentication): 인증처리가 가능한 Provider인지 검사하는 메서드입니다.
두 개의 메서드는 사용자가 입력한 아이디와 패스워드가 들어간 authentication 객체를 가지고 로직을 수행합니다.
1. 아이디 검증
UserDetailsService 인터페이스에서 인증을 요구하는 사용자 정보를 조회합니다.
사용자가 존재할 경우 UserDetails 타입의 객체를 반환받습니다.
사용자가 존재하지 않을 경우 UserNotFoundException 예외를 발생시킵니다.
2. 패스워드 검증
반환된 UserDetails 에 저장된 password 와
로그인시 입력한 패스워드(authentication.password)가 일치하는지 비교합니다.
일치하지 않을 경우 BadCredentialException 예외를 발생시킵니다.
일반적으로 패스워드를 저장할 때 Password Encoder를 이용해 암호화 하여 저장하기 때문에
해당 클래스(PasswordEncoder)를 이용해 저장되어 있던 사용자의 비밀번호를 복호화한 후
두 암호를 비교합니다.
3. 추가 검증
추가적으로 사용자가 정의한 검증 조건을 검증합니다.
autheticate(authentication) 에서 검증이 모두 성공하면 최종적으로 인증객체(Authentication(user,authorities))를
생성하여 AuthenticationManager에 전달합니다.
Authorization, FilterSecurityInterceptor
Authorization
Authorization 은 인가 처리를 의미합니다. 여기서 인가란 인증 된 사용자가 특정 자원에 접근하고자 할 때 접근 할 자격이 되는지 증명하는 것을 말합니다. 사용자가 특정 자원에 접근하고자 요청(Request)을 하면 그 사용자가 인증을 받았는지 확인합니다. 인증을 받은 사용자라면 해당 사용자의 자격(권한)이 해당 자원에 접근할 자격이 되는지 확인합니다.위 그림에서 사용자는 Manager 이기 때문에 인증은 된 상태입니다.
인가 처리 부분에서 사용자는 Manager이기 때문에 User Section, Manager Section까지는 접근이 가능합니다.(Manager 가 User 권한보다 높기 때문에 User Section 에도 접근이 가능합니다.)하지만 Admin Section의 Resources는 권한 부족으로 접근이 허용되지 않습니다.
스프링 시큐리티가 지원하는 권한 계층
1. 웹 계층
URL 요청에 따른 메뉴 혹은 화면단위의 레벨 보안입니다. /user 경로로 자원 접근을 할 때 그 자원에 설정된 권한(ROLE_USER)과 사용자가 가진 권한을 서로 심사해서 결정하는 계층입니다.
2. 서비스 계층
화면 단위가 아닌 메소드 같은 기능 단위의 레벨 보안입니다. user() 라는 메소드에 접근하고자 할 때 해당 메소드에 설정된 권한과 사용자가 가진 권한을 서로 심사해서 결정하는 계층입니다.
3. 도메인 계층
FilterSecurityInterceptor
FilterChainProxy 에 등록된 필터중 마지막에 위치한 필터로써
인증된 사용자에 대해 특정 요청의 승인및 거부 를 최종적으로 결정합니다.
권한 제어 방식 중 HTTP 자원의 보안을 처리하는 필터로 URL방식으로 접근할 경우 동작합니다다.
인증객체 없이 보호 자원에 접근을 시도하면 AuthenticationException 예외를 발생시키고
인증 후 자원에 접근 가능한 권한이 존재하지 않을 경우 AccessDeniedException 예외를 발생시킵니다.
권한 처리는 AccessDecisionManager 에게 맡깁니다.
FilterSecurityInterceptor 의 흐름
1. 사용자가 자원 접근합니다.
2. FilterSecurityInterceptor 에서 요청을 받아서 인증여부를 확인합니다.
인증객체를 가지고 있는지 확인해 인증객체가 없으면 AuthenticationException 예외를 발생시킵니다.
ExceptionTranslationFilter 에서 해당 예외를 받아서 다시 로그인 페이지로 이동하던가 후처리를 해줍니다.
(ExceptionTranslationFilter 는 FilterChainProxy 가 가지고있는 필터중 마지막에서 2번째로 위치합니다.
즉 FilterSecurityInterceptor 앞에 있는 필터가 ExceptionTranslationFilter 입니다.)
3. 인증객체가 있을 경우 SecurityMetadataSource 는
자원에 접근하기 위해 설정된 권한정보를 조회해서 전달해줍니다.
권한정보가 없으면 권한 심사를 하지 않고 자원 접근을 허용합니다.
4. 권한 정보가 있을 경우 AccessDecisionManager 에게 권한 정보를 전달하여 위임합니다.
(AccessDecisionManager는 최종 심의 결정자입니다.)
5. AccessDecisionManager가 내부적으로 AccessDecisionVoter(심의자)를 통해서 심의 요청을 합니다.
6. 반환된 승인/거부 결과를 가지고 사용자가 해당 자원에 접근이 가능한지 판단합니다.
접근이 거부되었을 경우 AccessDeniedException 예외를 발생시킵니다.
ExceptionTranslationFilter 에서 해당 예외를 받아서 다시 로그인 페이지로 이동하던가 후처리를 해줍니다.
7. 접근이 승인되었을 경우 자원 접근이 허용됩니다.
AccessDecisionManager, AccessDecisionVoter
AccessDecisionManager 는 인증, 요청, 권한 정보를 이용해서 사용자의 자원접근을 허용/거부 여부를 최종 결정하는
주체입니다. 여러 개의 Voter 들을 가질 수 있고, Voter들로부터 접근허용, 거부 ,보류에 해당하는 각각의 값을 반환받아
판단,결정합니다. 최종 접근 거부시 예외를 발생시킵니다.
접근결정의 세가지 유형
AffirmativeBased : 여러개의 Voter 클래스 중 하나라도 접근 허가로 결론을 내면 접근 허가로 판단합니다
ConsensusBased : 다수표(승인 및 거부)에 의해 최종 결정을 판단합니다. 동수 일경우 기본은 접근 허가
이지만, allowIfEqualGrantedDeniedDecisions 을 false 로 설정할 경우 접근거부로 결정됩니다
UnanimousBased : 모든 Voter 가 만장일치로 접근을 승인해야 하며 그렇지 않은 경우 접근을 거부합니다.
AffirmativeBased 와 반대되는 유형이라고 생각하면 될 거 같습니다.
AccessDecisionVoter
AccessDecisionVoter 는 판단을 심사합니다. (위원)
각각의 Voter 마다 사용자의 요청마다 해당 자원에 접근할 권한이 있는지 판단 후
AccessDecisionManager 에게 반환하는 역할을 합니다.
Voter가 권한 부여 과정에서 판단하는 자료
Authenticaion - 인증정보(user)
FilterInvocator - 요청 정보(antMatcher("/user"))
ConfigAttributes - 권한 정보(hasRole("USER"))
결정 방식
AccessDecisionManager 은 반환 받은 결정 방식을 사용해서 후처리를 합니다.
ACCESS_GRANTED: 접근 허용(1)
ACCESS_DENIED: 접근 거부(-1)
ACCESS_ABSTAIN: 접근 보류(0), Voter 가 해당 타입의 요청에 대해 결정을 내릴 수 없는 경우입니다.
AccessDecisionManager 와 AccessDecisionVoter 의 인가 처리 흐름
1. FilterSecurityInterceptor 가 AccessDecisionManager 에게 인가 처리를 위임힙니다.
2. AccessDecisionManager는 자신이 가지고 있는 Voter 들에게
정보인 decide(authentication, object, configAttributes)를 전달합니다.
3. Voter들은 정보들을 가지고 권한 판단을 심사합니다.
4. 승인,거부,보류 결정방식을 반환하면 AccessDecisionManager에서는
반환받은 결정 방식을 가지고 후처리를 합니다.
승인할 경우 FilterSecurityInterceptor에 승인여부를 반환하고
거부할 경우 AccessDeniedException 예외를 ExceptionTranslationFilter 로 전달합니다.
스프링 시큐리티 필터 및 아키텍쳐 정리
스프링 시큐리티의 각 기능을 한 곳에 모아서 초기화 과정이 일어나고
초기화 과정이 끝난 후 사용자가 인증 요청을 하거나 자원에 접근할 경우에
어떻게 시작하고 중간에 어떤 처리 과정을 거쳐서 최종적으로 완료가 되는지 정리해보겠습니다.
스프링 시큐리티가 초기화되면 현재 사진에 보여지는 부분부터 동작하게 됩니다.
WebSecruityConfigurerAdapter 를 상속한 스프링 시큐리티 설정 클래스를 생성하고 configure() 메서드로
API 를 생성하면 초기화 과정 때 그 구성대로 HttpSecurity 가 필터들을 생성합니다.
(Spring Security 5.7.1이상 또는 Spring Boot 2.7.0 이상이라면
WebSecruityConfigurerAdapter 를 상속하지 않고 SecurityFilterChain 타입의 빈을 등록합니다.)
ex) formLogin() 는 UsernamePasswordAuthenticationFilter
생성된 필터들은 WebSecurity 로 전달됩니다. WebSecurity 는 FilterChainProxy 타입의 빈 객체를 생성합니다.
생성할 때 생성자에 전달받은 필터 목록들을 인자값으로 넣어줍니다. 이렇게 FilterChainProxy 는 초기화되면서
생성된 필터 목록들을 모두 갖게 됩니다. DelegatingFilterProxy 는 springSecurityFilterChain 이라는 이름으로
등록된 빈을 찾는데 이게 바로 FilterChainProxy 입니다. DelegatingFilterProxy 는 사용자가 요청을 했을 때
그 요청을 받아 FilterChainProxy 에게 위임합니다.
초기화 과정이 끝난 후 사용자가 실제로 요청할 때
로그인 인증 시도(인증 전)
SecurityContextPersistenceFilter
1. DelegatingFilterProxy 에서 FilterChainProxy 에게 인증 요청을 위임합니다.
2. SecurityContextPersistenceFilter 에서 loadContext() 메서드를 호출해 SecurityContext 가 있는지 확인합니다.
내부적으로 HttpSessionSecurityContextRepository 클래스가 SecurityContext의 생성, 저장, 조회, 참조합니다.
3. 새로운 SecurityContext를 저장해서 SecurityContextHolder 에 저장한 다음 다음 필터로 이동합니다.
LogoutFilter
1. Logout요청을 한 것이 아니므로 다음 필터로 통과됩니다.
UsernamePasswordAuthenticationFilter
1. 전달받은 Username, Password 를 가지고 인증 객체(Authentication)을 생성합니다.
2. AuthenticationManager 에게 인증 처리를 위임합니다
3. AuthenticationManager 는 AuthenticationProvider 에게 실제 인증처리를 위임합니다/
4. AuthenticationProvider 는 UserDetailsService 를 활용해서 아이디와 패스워드를 검증합니다.
5. 인증이 성공했다면 SecurityContextHolder 안에 있는 SecurityContext 에
인증에 성공한 인증객체(Authentication)를 생성 및 저장합니다. 여기서 SecurityContext은
SecurityContextPersistenceFilter 에서 만들어진 SecurityContext 를 참조한 것입니다.
6. 인증 후 후속처리를 동시에 하게되는데, 이 과정이 SessionManagementFilter 안의 3가지 과정입니다.
ConcurrentSession 에서는 동시적 세션체크를 하는데 두 가지 전략을 가지고 있는데,
현재 상황에서는 첫 로그인이므로 통과합니다.
이전 사용자 세션 만료 전략 : session.expireNow 로 이전 사용자의 세션을 만료시킵니다.
현재 사용자 인증 시도 차단 전략: SessionAuthenticationException 예외를 발생시켜 인증을 차단합니다.
SessionFixation(세션 고정 보호) 에서 인증에 성공을 한 시점에서 새롭게 쿠키를 발급합니다.
이 사용자 정보를 SessionInfo 를 만들어 저장합니다.
7. 인증 성공 후 후처리 로직을 수행하는데, 이 시점에서 동시에 수행되는 로직이 있습니다.
SecurityContextPersistenceFilter 가 최종적으로 Session 에
인증객체(Authentication) 을 담은 SecurityContext를 저장하고
SecurityContext 를 Clear 해줍니다.
인증 성공 후 리다이렉트되는 페이지로 이동합니다.
인증 후 특정 페이지 이동(자원 요청(Request))
SecurityContextPersistenceFilter
1. loadContext() 로 Session 에서 SecurityContext 를 꺼내옵니다.
2. SecurityContextHolder 에 꺼내온 SecurityContext 를 저장합니다.
3. 다음 필터로 이동합니다.
자원에 접근하는 상황이니 LogoutFilter, UsernamePasswordAuthenticationFilter 는 패스합니다.
ConcurrentSessionFilter
1. 이 필터는 최소 두 명 이상이 동일한 계정으로 접속을 시도하는 경우에 동작합니다.
2. Session 이 만료되었는지 isExpired() 를 통해 확인합니다.
3. 만료되지 않았기 때문에 다음 필터로 이동합니다.
RememberMeAuthenticationFilter
현재 사용자가 세션이 만료되었거나 무효화되어
세션 내부의 인증 객체(Authentication)가 비어있을 경우 동작합니다.
1. 사용자의 요청 정보(header)에 remember-me cookie 값을 확인합니다.
(없으면 동작하지 않습니다.)
AnonymousAuthenticationFilter
사용자가 인증 시도가 없고 권한 없이 특정 자원에 접근 시도시 동작합니다.
1. 인증되어있는 상태기 때문에 그냥 통과합니다.
SessionManagementFilter
Session 에 SecurityContext 가 없는 경우나 Session 이 없는 경우 동작합니다.
1. 인증 후 접근이기에 다음 필터로 이동합니다.
ExceptionTranslationFilter
Try Catch 문으로로 다음 필터 동작을 감싸서
FilterSecurityInterceptor 의 로직을 수행 중 일어나는 예외를 받아서 동작합니다.
FilterSecurityInterceptor
1. AccessDecisionVoter 에게 인가 처리를 위임합니다. 인증객체가 없을경우 AuthenticationException 예외를
발생시켜 ExceptionTranslationFilter 에게 전달합니다. 사용자가 접근하려는 자원의 권한을 갖고있지 않거나
접근할 수 있는 권한이 아니라면 AccessDeniedException 예외를 발생시켜
ExceptionTranslationFilter 에게 전달합니다.
동일 계정 다른기기 중복 로그인 - 최대 1개 세션 허용 정책
SecurityContextPersistenceFilter 첫 번째 인증 사용자의 로직과 동일하게 수행합니다.
LogoutFilter 는 인증 시도이기 때문에 통과합니다.
UsernamePasswordAuthenticationFilter
1. 인증 성공 후 SecurityContextHolder 안에
인증 객체(Authentication)가 저장된 SecurityContext 를 저장합니다.
2. ConcurrentSession 에서 정책을 확인합니다.
현재 사용자 인증 시도 차단 정책: SessionAuthenticationException 예외를 발생시켜 인증을 실패 처리합니다.
이전 사용자 세션 만료 정책: session.expireNow() 를 사용해
이전 사용자의 세션을 만료시킨 후 자신의 인증객체를 저장합니다.
이전 사용자 세션 만료 정책일 경우 이 다음부터는 기존 인증 요청과 동일한 로직을 수행합니다.
동일 계정이 다른기기에 로그인 되어 세션이 만료된 계정에서 자원을 요청합니다.
ConcurrentSessionFilter
1. session.isExpired() 를 통해 현재 세션이 만료되었는지 확인합니다.
2. 만료되었기 때문에 Logout 로직을 수행합니다.
3. error 응답후 다음 필터 수행 없이 종료합니다.
'Spring Security' 카테고리의 다른 글
[Spring Security] JWT (1) | 2023.02.22 |
---|---|
[Spring Security] 인증 부가 기능 (0) | 2023.01.11 |
[Spring Security] PasswordEncoder (0) | 2023.01.11 |
[Spring Security] WebIgnore 설정 (0) | 2023.01.11 |
[Spring Security] 스프링 시큐리티 기본 API 및 Filter 이해 (0) | 2023.01.09 |
댓글