인증 저장소
SecurityContextHolder, SecurityContext
SecurityContext
- Authentication 객체가 저장되는 보관소로 필요 시 언제든지 Authentication 객체를 꺼내어 쓸 수 있도록 제공되는 클래스
- ThreadLocal에 저장되어 아무 곳에서나 참조가 가능하도록 설계함
- ThreadLocal : Thread마다 할당도니 고유 공간(공유X)
- 다른 Thread로부터 안전하다
- get, set, remove api가 있다.
- set 한 이후 get할 대 장소의 제약이 없다 (ex : A메서드에서 set한 내용을 B메서드에서 get하는것이 가능하다)
- 인증이 완료되면 HttpSession에 저장되어 어플리케이션 전반에 걸쳐 전역적인 참조가 가능하다
- HttpSession은 처음 접속시에만 생성되며, 클라이언트의 웹 브라우저와 웹 서버의 연결 상태를 관리하는 객체이다.
- session : 웹 브라우저와 웹 서버의 연결 상태(세션 유지)를 의미
- ex 1.로그인이 성공한 후 아이디를 HttpSession 객체에 저장하면 이후 이메일 페이지에 들어갈때 객체에 저장된 아이디를 확인하고 아이디에 맞는 이메일 페이지를 열어준다.
- ex 2. 쇼핑물에서 여러 페이지에서 물건을 장바구니에 담으면 물건들이 HttpSession 객체에 누적되어 물건 목록을 구매할 수 있음.
SecurityContextHolder
- SecurityContext 객체를 저장하고 감싸고 있는 wrapper 클래스
- SecurityContext 객체를 바로 저장하는 것은 아니고 3가지의 저장방식에 따라 저장한다.
- MODE_THREADLOCAL : 스레드당 SecurityContext 객체를 할당(default)
- MODE_INHERITABLETHREADLOCAL : 메인 스레드와 자식 스레드에 관하여 동일한 SecurityContext를 유지 -> Parent, Child thread에서 동일한 SecurityContext를 가진다
- MODE_GLOBAL : 응용 프로그램에서 단 하나의 SecurityContext를 저장한다 -> Memory에서 단 하나의 SecurityContext를 가지고 참조
- SecurityContextholder.clearContext(): SecurityContext 기본 정보 초기화 메서드
Authentication authentication = SecurityContextHolder.getContext().getAuthentication()
- 어떤 메서드에서든지 참조가 가능하며, 인증객체를 어디서든 꺼내어 사용할 수 있다.
- Login을 사용자가 시도
- Server가 요청을 받아서 Thread를 생성(ThreadLocal 할당)
- process : 실행 중인 하나의 어플리케이션(운영체제로부터 할당 받는 작업 단위)
- Thread : 한 프로세스 내에서 동작 되는 여러 실행 흐름(프로세스가 할당 받은 자원을 이용하는 실행의 단위, 추가 가능)
- 즉 자바의 JVM위에서 하나의 프로세스가 돌아가고, 프로세스 안에서 main문 하나의 스레드가 실행되는 것이다.
- 스레드 & 멀티스레드 참고
- ThreadLocal : 자바의 class이며 오직 한 Thread에 의해서 읽고 쓰여질 수 있는 변수이다.
- thread가 인증 처리 시도 -> 인증 객체(Authentication) 생성
- (인증 실패) SecurityContextHolder.clearContext() 인증객체 초기화
- (인증 성공) SecurityContextHolder안의 SecurityContext에 인증객체(Authentication) 저장 -> ThreadLocal이 SecurityContextHloder를 담고 있는 것이다.
- SecurityContext에서 최종적으로 HttpSession에 저장된다 -> SPRING_SECURITY_CONTEXT라는 이름으로 저장된다.
SecurityContextHolder의 SecurityContext저장 전략별 차이점 살펴보기
- 기본전략::MODE_THREADLOCAL
- MODE_INHERITABLETHREADLOCAL 전략
- 흔히 위의 두 가지 방식을 사용한다. 멀티스레드 일 경우는 MODE_INHERITABLETHREADLOCAL를 사용하면 좋을 것 같고 멀티스레드를 구현할 필요성이 없는 경우에는 기본전략(MODE_THREADLOCAL)으로 구현하는게 편리할 것 같다.(주로 기본전략 사용할듯..)
인증 저장소 필터
- SecurityContextPersistenceFilter
SecurityContext 객체의 생성, 저장, 조회
- 익명 사용자
- 새로운 SecurityContext 객체를 생성하여 SecurityContextHolder에 저장한다.
- AnonymouseAuthenticationFilter에서 AnonymousAuthenticationToken 객체를 SecurityContext에 저장한다.
- 인증 시
- 새로운 SecurityContext객체를 생성하여 SecurityContextHolder에 저장한다.
- AnonymouseAuthenticationFilter에서 AnonymousAuthenticationToken 객체를 SecurityContext에 저장한다.
- 인증 후
- Session에서 SecurityContext를 꺼내어 SecurityContextHolder에서 저장한다.
- SecurityContext안에 Authentication 객체가 존재하면 계속 인증을 유지한다.
- 최종 응답 시 공통
- SecurityContextHolder안의 SecurityContext객체에서 보관하던 인증정보를 반드시 초기화 해줘야 한다.
- SecurityContextHolder.clearContext() 메서드를 호출해 인증 정보를 초기화 한다.
Flow
- 사용자가 Request 요청
- SecurityContextPersistenceFilter는 매 번 요청마다 수행된다.
- SecurityContextPersistenceFilter내부적으로 HttpSecurityContextRepository가 로직을 수행
- HttpSecurityContextRepository: SecurityContext객체를 생성, 조회 하는 역할을 하는 클래스
- 인증 전
- 새로운 컨텍스트 생성(SecurityContextHolder) -> 이 때는 SecurityContext 객체는 null이다.
- 그 다음 필터로 이동한다(chain.doFilter), 필터체인으로 연결하여 줍니다. 체인의 경우 순서를 지정할 수 있습니다. 체인의 가장 마지막에는 클라이언트가 요청한 최종 자원이 위치합니다.
- 인증 필터(AuthFilter)가 인증을 처리한다.
- 인증이 완료되면 인증객체(Authenticaion)생성 후 SecurityContext 객체 안에 저장된다.
- 다음 필터 수행(chain.doFilter)
- Client에게 응답하는 시점에서 Session에 SecurityContext저장 -> SecurityContextPerstenceFilter가 하는 것이다.
- SecurityContext 제거(Clear())
- 응답(Response)
- 인증 후
- Session에서 SecurityContext가 있는지 확인 -> 인증이 된 이후이기에 존재한다.
- SecurityContext를 꺼내어 SecurityContextHolder에 집어넣는다.
- 다음 필터 수행(chain.doFilter)
Flow - Simple
- SecurityContextPersistenceFIlter는 인증 전(좌측) 인증 후(우측)으로 나뉘는데, 인증 후에는 Session에서 SecurityContext를 꺼내 SecurityContextHolder에 집어넣은 뒤 다음 필터들을 수행한다. 하지만, 아직 인증 전이라면 꺼낼 SecurityContext는 없기 때문에 SecurityContextHolder를 생성한 뒤 인증필터(AuthFilter)를 수행 후 생성된 인증객체(Authentication)을 SecurityContext에 담은 뒤 다음 로직을 수행한다.
그리고 마지막으로 Client에게 응답(Response)하기 전 Session에 SecurityContext를 담은 뒤 SecurityContext는 초기화 해준 다음 응답을 합니다.
인증 흐름 이해
- Authentication Flow
- Client에서 로그인 요청
- UsernamePasswordAuthenticationFIlter에서 ID + PASSWORD를 담은 인증 객체(Authentication)를 생성한다.
- AuthenticationManager에게 인증객체를 넘기며 인증처리를 위임한다.
- 내부에 한 개 이상의 Provider를 담은 List를 가지고 있고 그 List에서 적절한 Provider를 찾는다.
- 인증관리자(AuthenticationManager)는 적절한 Provider(AuthenticationProvider)에게 인증처리를 위임한다.
- 해당 Provider는 input 정보(id, password)를 가지고 실제 인증 처리 역할을 한다.
- loadUserByUsername(username)메서드를 호출해서 유저객체를 요청한다.
- UserDetailsService 인터페이스에게 loadUserByUsername(username) 요청
- Repository에 findById() 메서드로 유저 객체 조회를 한다.
- 만약, 해당 유저객체가 존재하지 않으면 UsernameNotFoundException 이 발생하고 UsernamePasswordAuthenticationFIlter에서 예외를 처리한다. -> FailHandler()에서 후속처리
- 존재한다면 UserDetails 타입으로 반환된다.(Member 객체도 UserDetails 객체로 변환되어 반환)
- 인증관리자(AuthenticationManager)는 이제 Password 검증을 시작한다.
- 인증객체의 Password와 반환받은 UserDetails의 Password를 비교한다.
- 일치하지 않을 경우 BadCredentialException 발생 후 인증 실패
- 성공한 인증객체(UserDetals와 authorities를 담은 인증 후 토큰 객체 Authentication)를 UsernamePasswordAuthenticationFilter에 전다한다.
- 인증객체의 Password와 반환받은 UserDetails의 Password를 비교한다.
- SecurityContext에 저장한다.
- 이후 전역적으로 SecurityContextHolder에서 인증객체를 사용가능하게 된다.
인증 관리자
- AuthenticationManager
인증 관리자(AuthenticationManager)는 필터로부터 인증처리를 지시받으면 가지고 있는 인증 처리자(AuthenticationProvider)들 중에서 현재 인증처리를 할 수 있는 Provider에게 인증처리를 위임하여 인증처리 수행 후 인증 성공을 한다면 반환받은 인증객체를 필터로 전달해준다.
만약, 적절한 Provider를 못 찾는다면 자신의 부모 객체에 저장된 ProviderManager도 검색하여 해당 인증을 처리할 수 있는 Provider가 있으면 인증처리를 위임하여 반환한다.
- AuthenticationManager는 인터페이스, ProviderManager는 구현체
- ProviderManager는 AuthenticationProvider 목록 중 인증처리 요건에 맞는 적절한 Provider를 찾아 인증처리를 위임하는 클래스
- 적절한 AuthenticationProvider를 찾지 못하면 부모 ProviderManager에서 AuthenticationProvider 탐색 가능
인증 처리자
- AuthenticationProvider
- AuthenticationProvider 는 인터페이스다. 두 개의 메서드가 제공된다.
- authenticate(authentication) : 실제적인 인증처리를 위한 검증 메서드
- supports(authentication): 인증처리가 가능한 Provider인지 검사하는 메서드
- 두 개의 메서드는 사용자가 입력한 아이디와 패스워드가 들어간 authentication객체를 가지고 로직을 수행한다.
- 아이디 검증
- UserDetailsService 인터페이스에서 인증을 요구하는 사용자 정보를 조회한다.
- 존재할 경우 UserDetails 타입으로 반환한다.
- 존재하지 않을 경우 UserNotFoundException 발생
- UserDetailsService 인터페이스에서 인증을 요구하는 사용자 정보를 조회한다.
- 패스워드 검증
- 반환된 UserDetails에 저장된 password와 로그인시 입력한 패스워드 (authentication.password)가 일치하는지 비교한다.
- 일치하지 않을 경우 BadCredentialException 발생
- 일반적으로 패스워드를 저장할 때 Encoder를 이용해 암호화 하여 저장하기 때문에 해당 클래스(PasswordEncoder)를 이용해 두 암호를 비교한다.
- 반환된 UserDetails에 저장된 password와 로그인시 입력한 패스워드 (authentication.password)가 일치하는지 비교한다.
- 추가 검증
- 추가적으로 사용자가 정의한 검증 조건 검증
- autheticate(authentication) 에서 검증이 모두 성공하면 최종적으로 인증객체(Authentication(user,authorities))를 생성하여 AuthenticationManager에 전달한다.
인가 개념 및 필터 이해
- Authorization, FilterSecurityInterceptor
Authorization - 인가 처리
- 인증 된 사용자가 특정 자원에 접근하고자 할 때 접근 할 자격이 되는지 증명하는 것을 인가(Authorization)이라 한다.
- 사용자가 특정 자원에 접근하고자 요청(Request)을 하면 그 사용자가 인증을 받았는지 확인한다.
- 인증을 받은 사용자라면 해당 사용자의 자격(권한)이 해당 자원에 접근할 자격이 되는지 확인한다.
- 위 그림에서 사용자는 Manager이기 때문에 인증은 허가된 상태이다.
- 인가 처리 부분에서 사용자는 Manager이기 때문에 User Section, Manager Section까지는 접근이 가능하다.
- Admin Section의 Resources는 권한 부족으로 접근이 허용되지 않는다.
스프링 시큐리티가 지원하는 권한 계층
1. 웹 계층
- URL 요청에 따른 메뉴 혹은 화면단위의 레벨 보안
- /user 경로로 자원 접근을 할 때 그 자원에 설정된 권한(ROLE_USER)과 사용자가 가진 권한을 서로 심사해서 결정하는 계층
2. 서비스 계층
- 화면 단위가 아닌 메소드 같은 기능 단위의 레벨 보안
- user()라는 메소드에 접근하고자 할 때 해당 메소드에 설정된 권한과 사용자가 가진 권한을 서로 심사해서 결정하는 계층
3. 도메인 계층(Access Control List, 접근 제어 목록)
- 객체 단위의 레벨 보안
- 객체(user)를 핸들링 하고자 할 때 도메인에 설정된 권한과 사용자가 가진 권한을 서로 심사해서 결정하는 계층
FilterSecurityInterceptor - 인가 처리 담당 필터
- 마지막에 위치한 필터로써 인증된 사용자에 대해 특정 요청의 승인 및 거부를 최종적으로 결정
- 인증객체 없이 보호자원에 접근을 시도하면 AuthenticationException 발생
- 인증 후 자원에 접근 가능한 권한이 존재하지 않을 경우 AccessDeniedException 을 발생
- 권한 제어 방식 중 HTTP 자원의 보안을 처리하는 필터 -> URL방식으로 접근할 경우 동작한다
- 권한 처리를 AccessDecisionManager에게 맡긴다
FlOW
- 사용자가 자원 접근(Request)
- FilterSecurityInterceptor에서 요청을 받아서 인증여부를 확인한다.
- 인증객체를 가지고 있는지 확인한다
- 인증객체가 없으면(null) AuthenticationException 발생
- ExceptionTranslationFilter에서 해당 예외를 받아서 다시 로그인 페이지로 이동하던가 후처리를 해준다.
- 인증객체가 있을 경우 SecurityMetadataSource는 자원에 접근하기 위해 설정된 권한정보를 조회해서 전달해준다.
- 권한 정보를 조회한다
- 권한 정보가 없으면(null) 권한 심사를 하지 않고 자원 접근을 허용한다.
- 권한 정보가 있을 경우 AccessDecisionManager 에게 권한 정보를 전달하여 위임한다.
- AccessDecisionManager는 최종 심의 결정자다.
- AccessDecisionManager가 내부적으로 AccessDecisionVoter(심의자)를 통해서 심의 요청을 한다.
- 반환된 승인/거부 결과를 가지고 사용자가 해당 자원에 접근이 가능한지 판단한다.
- 접근이 거부되었을 경우 AccessDeniedException이 발생한다
- ExceptionTranslationFilter에서 해당 예외를 받아서 다시 로그인 페이지로 이동하던가 후처리를 해준다.
- 접근이 승인되었을 경우 자원 접근이 허용된다.
인가 결정 심의자
- AccessDecisionManager, AccessDecisionVoter
AccessDecisionManager::인터페이스
- 인증, 요청, 권한 정보를 이용해서 사용자의 자원접근을 허용/거부 여부를 최종 결정하는 주체다.
- 여러 개의 Voter들을 가질 수 있고, Voter들로부터 접근허용, 거부, 보류에 해당하는 각각의 값을 리턴받아 판단&결정한다,
- 최종 접근 거부시 예외 발생
AccessDecisionVoter
- 판단을 심사하는 것(위원개념)
- 각각의 Voter에서 사용자의 요청마다 해당 자원에 접근할 권한이 있는지 판단 후 AccessDecisionManager에게 반환하는 역할
Voter가 권한 부여 과정에서 판단하는 자료 - 아래 정보를 전달받아 판단
- Authenticaion : 인증정보(user)
- FilterInvocator : 요청정보(antMatcher("/user"))
- ConfigAttributes : 권한 정보(hasRole("USER"))
결정 방식 - AccessDecisionManager 은 반환 받은 결정 방식을 사용해서 후처리를 한다.
- ACCESS_GRANTED : 접근 허용(1)
- ACCESS_DENIED : 접근 거부(-1)
- ACCESS_ABSTAIN : 접근 보류(0)
- Voter가 해당 타입의 요청에 대해 결정을 내릴 수 없는 경우
스프링 시큐리티 필터 및 아키텍처 정리
- 사용자가 자원요청을 할 때 이루어지는 SpringSecurity의 보안과정 전체 Flow이다.
'Spring' 카테고리의 다른 글
JsonTypeInfo와 함께하는 다형성 구현 (0) | 2024.05.11 |
---|---|
BlockHound - Blocking 코드 존재여부 확인 (0) | 2023.12.24 |
Spring Security 주요 아키텍처 이해( Part 1) (0) | 2022.05.30 |
Annotation && AOP란? (0) | 2022.05.11 |
Spring IoC 컨테이너 및 Bean소개(1) [with spring docs] (0) | 2022.05.11 |