spring securty 2

고라니·2023년 12월 6일
0

스프링 시큐리티의 의존성 추가 시 일어나는 일들

  • 서버가 기동되면 스프링 시큐리티의 초기화 작업 및 보안 설정이 이루어진다
  • 별도의 설정이나 구현을 하지 않아도 기본적인 웹 보안 기능이 현재 시스템에 연동되어 작동함
  • 모든 요청은 인증이 되어야 자원에 접근이 가능하다
  • 인증 방식은 폼 로그인 방식과 httpBasic 로그인 방식을 제공한다
  • 기본 로그인 페이지를 제공한다
  • 기본 계정 한 개 제공한다 – username : user / password : 랜덤 문자열 ( server 가동 시 console 에 찍히는 문자 )

Spring Security 핵심 클래스 2개

WebSecurityConfigurerAdapter (deprecated)

  • 스프링 시큐리티의 웹 보안 기능 초기화 및 설정하는 클래스이다.
    이 클래스는 HttpSecurity 클래스 (세부적인 보안 기능을 설정할 수 있는 API 제공함) 를 생성한다.
  • 그래서 보통 WebSecurityConfigurerAdapter 를 상속받은 SecurityConfig.java 라는 보편적인 이름의 config 파일을 만들고 이 안에서
    HttpSecurity 를 활용하여 보안 기능을 custom 하게 된다.

HttpSecurity

인증 API / 인가 API 제공


Http Basic 인증

말 그대로 Http 프로토콜에서 정의한 기본 인증입니다.

사용자가 인증을 받지 않은 상태로 요청을 하게 되면 서버에서는 사용자에게 401 Unauthorized 응답과 함께 WWW-Authenticate 헤더를 기술해서 인증을 어떤 방식으로 해야 하는지에 대한 설명을 동봉하여 보내게 됩니다.

그러면 사용자는 아이디와 패스워드를 Base64 로 인코딩한 문자열을 Authorization 헤더에 담아서 요청하게 됩니다.

그러면 서버에서 인증을 수락하게 되고 정상적인 상태 코드를 반환하게 됩니다.

이때 아이디와 패스워드는 암호화가 안되어 있기 때문에 보안에 취약하므로 ssl 통신같은 것을 반드시 고려해야 합니다.

form 인증도 아이디와 패스워드를 평문으로 전송하면 안되기 때문에 ssl 로 반드시 통신해야 합니다.

form 인증과의 차이점이라면 말씀하신대로 Http Basic 은 세션방식의 인증이 아닌 서버로부터 요청받은 인증방식대로 구성한 다음 헤더에 기술해서 서버로 보내는 방식을 취한다는 점입니다.

form 인증방식은 서버에 해당 사용자의 session 상태가 유효한지를 판단해서 인증처리를 한다는 점입니다.


Login Form 인증

서버에서, 인증 성공한 사용자에게 JSESSIONID 를 응답 헤더에 넣어 전달하고, Client는 이 JSESSIONID를 브라우저에 쿠키로 가지고 있다가,
재요청 시 인증을 다시 받지 않아도 됨. 아래에 remember-me 를 활성화하면 추가로 remember-me cookie도 발급해줘서 브라우저에서 갖고있다면 자동로그인 및 자동 인증 방식을 취할 수 있다.

UsernamePasswordAuthenticationfilter.java
AbstractAuthenticationProcessingFilter.java
로그인 폼 인증처리를 하는 UsernamePasswordAuthenticationFilter
의 가장 큰 역할 :
username/pwd검증해서
실제 인증 (Authentication) 객체에 값을 넣는 인증처리를 담당하게 된다.

Filter는 Authentication 객체에 인증에 필요한 정보(Username+Pwd)를 셋팅해서 AuthenticationManager에게 전달한다.

AuthenticationManager는 필터로부터 인증객체를 전달받고, 이것을 가지고 여러 처리를 하는 역할을 한다.

AuthenticationManager는 AuthenticationProvider 객체를 선택하여 실질적 인증 역할을 부여한다.
인증 성공 시 Provider 에서 Authentication 객체에 인증정보+권한을 셋팅해서 Manager에게 다시 넘기고,
Manager가 Filter에게 Authentication 를 리턴하게 된다.
필터는 최종적으로 SecurtyContext에 Authentication을 저장한다.
SecurityContext는 인증 객체를 저장하는 보관하는 저장소 또는 보관소임. (세션에도 저장이 돼서 전역적으로 참조가 가능하게 됨)
이후 SuccessHandler 가 작동하게 된다.

실패(Exception))할 경우 AuthenticationProvider에서 예외(AuthenticationException)를 발생시키고, Filter에 후속 처리를 맡긴다.

FilterChainProxy.java는 여러 Filter들을 갖고있는 Bean인데
(SpringSecurity가 초기화 되면서 생성되는 필터들도 갖고있고, SecurityConfig에서 우리가 설정해준 값에 맞는 필터도 생성이 된다. .formLogin() 걸어주니까 UsernameAuthenticationFilter 인스턴스가 생성)
이 여러개의 필터들을 순서대로 처리하는 역할을 한다.

그래서 FilterChainProxy가 먼저 용자의 요청을 가장 먼저 받고 각각의 필터들을 호출하면서 호출하면서 인증 또는 인과 처리를 하고 있는것이다.


번외 : FilterChainProxy와 DelegatingFilterProxy

내부 동작 : 기본적으로 서블릿(Tomcat) 에서 서블릿 필터인 DelegatingFilterProxy 에서 FilterChainProxy 를 찾아 실행하게 된다.
서블릿/스프링 컨테이너에 대한 자세한 내용은 꽤 괜찮은 정리내용이 있음
서블릿 컨테이너와 스프링 컨테이너
https://12bme.tistory.com/555

세션, 쿠키 생성되는 조건


logout()


세션 무효화, 인증토큰 삭제, 쿠기정보 삭제, 로그인 페이지로 리다이렉트 등의 처리를 한다.
(기본 설정된 logoutHandler 역할이다.) 그러나 추가로 처리가 필요하면 addLogoutHandler() 에 별도 구현한 핸들러를 add해주어야한다.

logoutFilter.java


LogoutFilter에 logout request 요청이 들어오면 먼저 AntPathRequestMatcher 에서 로그아웃 url 여부를 체크 후 불일치 하면 다음 필터로 이동하고,
매칭이 된다면 SecurityContext로부터 인증 객체를 꺼내온다.
로그아웃 필터가 가지고 있는 로그아웃 핸들러가 몇 개가 존재하는데 이중 SecurityContextLogoutHandler() 는
세션무효화 / 쿠키삭제 / SecurityContext Clear / Authentication 인증객체 null 로 초기화 등을 진행한다.
위 logout 처리가 성공적으로 종료되면 LogoutFilter 는 SimpleUrlLogoutSuccessHandler 통해서 redirect 후 마친다.

번외 : SecurityContext 는 요청 스레드별로 ThreadLocal 에 저장되는 개념이다.

그래서 securityContextHolder 에 SecurityContext 가 사용자별로 저장되어 있고 각 SecurityContext 는 서로간 간섭이 없는 독립적인 공간입니다.

그렇기 때문에 사용자 A의 /logout 처리 중에 securityContextHolder.clearContext() 를 실행하게 되면
요청 스레드 즉 사용자 A 의 요청 전용공간인 ThreadLocal 에 저장되어 있는 SecurityContext 만 삭제가 되는 원리로 되어 있습니다.

SecurityContext 는 매 요청마다 새롭게 생성되거나 이미 생성되어 있다면 세션에서 가져오게 됩니다. 즉 사용자별로 할당이 되는 객체라 볼 수 있습니다.

스프링 컨테이터의 ApplicationContext 는 어플리케이션 내 생성된 빈들을 모두 담아 놓는 저장소이지만 SecurityContext 는 그렇지 않고 각 요청별로 생성된 스레드마다 할당되는 저장소라 보시면 됩니다.

그리고 SecurityContext 는 각 사용자의 세션에 저장됩니다.

RememberMe 인증

사용자 아이디 기억하기 또는 자동로그인 설정 등을 가능하게 한다.
SecurityConfig 에서 API 추가 시 활성화 되며, 활성화 된 상태에서 인증 진행 및 성공하면 서버가 그 사용자에게 Remember Me 쿠키를 응답 헤드에 실어서 보내게 됨.

특징

  • 세션이 만료되고 웹 브라우저가 종료된 후에도 어플리케이션이 사용자를 기억하는 기능
  • Remember-Me 쿠키에 대한 Http 요청 을 확인한 후 토큰 기반 인증을 사용해 유효성을 검사하고 토큰이 검증되면 사용자는 (자동적으로)로그인 된다.
  • 사용자 라이프 사이클
    -- 인증 성공(Remember-Me쿠키 설정)
    -- 인증 실패(쿠키가 존재하면 쿠키 무효화)
    -- 로그아웃(쿠키가 존재하면 쿠키 무효화)

세부동작
만약 remmeber-me 활성화 후 인증에 성공하게 되면 JSESSIONID 뿐 아니라 remember-me 쿠키까지 응답헤드에 발급이 돼서 client가 갖고 있게 되는데 (expire-date 당연히 있음), 사용자가 JSESSIONID 쿠키를 없애고 다시 인증이 필요한 url에 접근하게 되면
RememberMeAuthenticationFilter 에서 JSESSIONID가 없어도 리퀘스트 헤드에 remember-me 쿠키가 있다면 이 값을 decode/parsing/추출해서 userId/Pwd 통해 유저 객체를 다시 얻어서 다시금 인증을 시도하게 된다. 인증에 성공하면 다시 JSESSIONID 발급이 됨. 그리고 동일하게 인증객체 생기고 SecurityContext 객체에도 담기게 됨.

RememberMeAuthenticationFilter.java

Authentication 인증 객체는 SecurityContext에 저장이 되어 있다.
인증에 성공하면 인증필터가 최종적으로 성공한 인증의 결과를 담은 인증 객체를 생성 후 SecurityContext에 담게 되므로 인증을 받은 사용자는 무조건 SecurityContext에 인증 객체가 늘 존재하기 때문에, 만약 Authentication 객체가 Null이 아니면 이 필터가 동작하지 않는다.
따라서 Authentication 객체가 Null일때, 즉 그 사용자의 Session 이 만료되었거나 (Session Timeout), 사용하는 브라우저가 종료되어 Session이 끊겨져서 더이상 Session 안에서 SecurityContext를 찾지못하고, 존재하지 않을 때 자동적으로 그 사용자의 인증을 유지하기 위해서 이 필터가 동작하고 인증을 시도하게 된다.

요약
필터 동작 조건
1. 인증 객체가 없는 경우
2. remember-me 쿠키 값을 담아서 서버에 접속한 경우

위 조건에 부합하여 필터 동작 시
0. RememberMeServices 서비스 인터페이스가 동작하면서 처음에 remember-me token 쿠키를 추출하고, 토큰이 존재하지 않으면 다음 필터로 넘어감.

  1. 존재하면 아래 두개 구현체를 통해 인증 처리를 한다.
  • TokenBasedRememberMeServices : 메모리에서 실제 토큰과 사용자가 요청 시 들고 온 remember-me 토큰과 비교하여 인증 처리를 한다. 기본적으로는 14일 만료 기간이 있음
  • PersistentTokenBasedRememberMeServices : 영구적인 방식, DB에 서버에서 발급한 토큰을 저장하고, 사용자 요청 시 들고 온 remember-me 토큰과 비교하여 인증 처리를 한다.
    각각 구현체가 실제로 rememberme 인증처리를 하게 된다.
  1. 아래와 같은 여러 토큰 검증 후에
    구현체가 Remember Me Authentication Token 이라는 인증 객체를 생성 후 AuthenticationManager에게 전달해서 실질적 인증 처리를 하게 된다.

rememeber-me 여담

remember-me 방식은 해시로 만든 암호화된 문자열을 사용해서 인증을 자동으로 해 주는 기능입니다.

물론 인증을 한 후에 세션 방식으로 인증을 유지하지만 remember-me 방식 자체는 쿠키로 만든 암호화 토큰이 맞습니다.

그래서 remember-me 에서 암호화 된 문자열을 서버에서 생성하고 클라이언트로 전달된 후 어떤 이유로 세션이 만료되거나 소멸되었을 때 remember-me 토큰을 가지고 인증을 시도하는 방식이라 JWT 처럼 완전 세션을 배제한 토큰과는 차이가 있지만,
세션과 상관없이 토큰만으로도 인증에 성공하기 때문에 토큰을 이용한 인증방식이라 해석할 수도 있습니다.

AnomymousAuthenticationFilter

그렇게 많은 역할을 하진 않고 중요하진 않다.

  • 인증 객체 존재하면 특별한 처리 하지 않고 다음 필터로 넘어간다.
  • 만약 존재하지 않는다면 인증을 거치지 않은 사용자로 판단하고 인증 객체를 생성하는데, AnonymousAuthenticationToken 을 생성함.
    일반적으로 null 로 처리하는 것이 아니라 익명 사용자용 토큰을 SecurityContext에 저장함. (위의 일반 인증 방식과 동일한 방식으로 저장함)
  • S.C으로 인증이 안된 모든 사용자는 익명사용자
  • 단순히 User가 Null로 처리해도 무방하지만 스프링 시큐리티를 사용하여 통일화하기 위해 사용
    ex) 해당 코드내에 isAnonymouse() 와 isAuthenticated()
  • 인증객체를 세션에 저장하지 않는다 : 이 사용자가 지금 인증 객체를 가지고 있어도 실제로 인증을 받지 않는 사용자이기 때문이다.
http.authorizeRequest()
    .anyRequest().authenticated();

이런식으로 설정하면 우리는 어떤 요청에도 인증을 받아야된다 라는 정책이므로 anonymousAuthentication 을 활용할 수 있는 것임.

실제 코드에서는
principle= "anonymousUser"
authorities = "ROLE_ANONYMOUS"
요런식으로 Token 정보를 구성한다.

굳이 anonymous를 왜 사용하나요?

Spring Security 에서의 익명사용자 개념 정리

  • 인증이 되기 전이나, 이후 의 사용자 모두
    유효한 인증토큰을 갖고있지 못하면 익명 사용자이다

  • 익명 사용자는 로그인이 가능한 경로를 통해 인증허가를 을 받게 될 경우, 일반 사용자로 등극하여, 로그인 접속 및 향후 접속유지가 가능하게된다.

  • 인증을 받지 못한 사용자는 익명 사용자로 분류되어, 익명 사용자 인증 토큰이(인증 객체) 익명 사용자 관리 명목으로 생성되지만, 로그인과 관련된 접근 권한은 없다(세션 생성이 되지않음) -> redirect /login page

  • 익명 사용자 전용으로 발급된 인증토큰을 통해, 향후 익명 사용자 접근 여부를 관리 할 수 있다.

스프링 시큐리티에서 익명사용자의 개념을 별도의 기능으로 구조화 했다고 보시면 될 것 같습니다.

그렇지만 로그인 인증을 받지 못한 사용자이기 때문에 실질적으로 아무런 권한을 갖지 못하는 사용자로 보셔도 무방합니다.

그래서 스프링 시큐리티 내부적으로 익명사용자를 인증사용자와 구분하기 위해 독립적인 객체를 만들어 익명사용자임을 나타내는 여러 군데의 코드가 존재합니다.
그리고 익명사용자의 역할과 기능이 스프링 시큐리티에서 큰 비중을 차지하는 것은 아니기 때문에 간단한 개념정도 이해하시고 사용하시면 될 것 같습니다.


동시 세션 제어 및 세션 고정 보호


동시 세션 제어 설정을 security 에서 제공하며,
최대 허용 가능 세션 수를 정할 수 있다.

공격자가 미리 심어놓은 세션을 통해 사용자가 로그인하면, 공격자의 쿠키 값으로 인증이 되어 있어서 공격자와 사용자가 정보를 공유할 수 있는 세션 고정 공격이 있는데, 이를 방지 하기 위해
인증이 성공할때마다 새로운 Session이 생성되고 새로운 SessionId가 발급되도록 그렇게 처리를 해주는게 Session 고정보호 이다.

http.sessionManagement() : 세션 관리 기능이 작동함

protected void configure(HttpSecurity http) throws Exception {
	http.sessionManagement()
                .sessionFixation().changeSessionId() // 기본값
							    // none, migrateSession, newSession
}

우리가 이렇게 설정을 하지 않더라도 Spring Security가 기본적으로 초기화되면서 속성이 작동하고 있다.


세션 정책 설정


SessionCreationPolicy.Stateless 이게 JWT 등을 사용할 때 정책으로 설정함.


세션 제어 필터 : SessionManagementFilter, ConcurrentSessionFilter

SessionManagementFilter
세션 관리
인증 시 사용자의 세션정보를 등록, 조회, 삭제 등의 세션 이력을 관리

동시적 세션 제어
동일 계정으로 접속이 허용되는 최대 세션수를 제한

세션 고정 보호
인증 할 때마다 세션쿠키를 새로 발급하여 공격자의 쿠키 조작을 방지

세션 생성 정책
Always, If_Required, Never, Stateless

ConcurrentSessionFilter

  • 매 요청 마다 현재 사용자의 세션 만료 여부 체크
  • 세션이 만료로 설정되었을 경우 즉시 만료 처리


인가 설정

인가 API – 권한 설정
선언적 방식

  • URL
    http.antMatchers("/users/**").hasRole(“USER")
  • Method
    @PreAuthorize(“hasRole(‘USER’)”)
    public void user(){ System.out.println(“user”)}

동적 방식 – DB 연동 프로그래밍

  • URL
  • Method

ExceptionTranslationFilter, RequestCacheAwareFilter


Form 인증 – CSRF, CsrfFilter

CSRF(사이트 간 요청 위조) 공격을 막기위해 Spring Security는 CSRFFilter를 거쳐서 취약점을 방지한다.

  • 모든 요청에 랜덤하게 생성된 토큰을 HTTP 파라미터로 요구
  • 요청 시 전달되는 토큰 값과 서버에 저장된 실제 값과 비교한 후 만약 일치하지 않으면 요청은 실패한다
    -> csrfFilter가 csrf 일치 여부 검사 후 AccessDeniedHandler가 작동하여 403 Forbidden Error로 클라이언트가 응답받은것을 확인 가능

Client

  • <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
  • HTTP 메소드 : PATCH, POST, PUT, DELETE

Spring Security

  • http.csrf() : 기본 활성화되어 있음
  • http.csrf().disabled() : 비활성화 , 이 CSRF 필터가 아예 필터 목록에 생성이 되지 않기 때문에 아예 이 필터를 거치지 않는다.

jwt 를 사용할 경우에는 대부분 세션은 사용하지 않기 때문에 csrf 기능은 비활성화 하는 것이 맞다.


Spring Architecture

Servlet Filter /

spring 2.3 부터 도입된 Servlet filter

사용자 리퀘스트 -> 서블릿으로 가기 전에 filter가 먼저 받아서 작업 처리 후 서블릿에 전달하게 된다.
서블릿에서 작업을 끝내면 필터가 최종적으로 사용자에게 resposne 을 하게 된다
이 필터는 Servlet 스펙에 정의된 기술이므로 Servlet Container에서 생성이 되고 실행이 된다.
그렇기 때문에 이 필터는 스프링에서 만든 Bean Injection 사용하거나 스프링에서 사용하는 기술을 Servlet Filter에서는 사용할 수 없다.
그런데 필터에서도 스프링에서 사용하는 기술을 사용하고자 하는 요구사항이 생길 수있으며,
그렇기 때문에 스프링 시큐리티는 스프링 빈을 만들고 그리고 스프링 필터의 필터를 구현한다. 그런데 사용자 요청은 WAS(톰캣)에서 올라가 있는 서블릿 컨테이너가 먼저 받기 떄문에
먼저 받은 요청을 스프링 에서 구현한 필터에 넘겨주고, 전달받은 요청을 처리하는 것을 이 중간에서 바로 DelegatingFilterProxy가 동작하면서 가능하게 해준다.
그렇기 때문에 요청을 받아서 DelegatingFilterProxy 이 실제적인 보안 처리를 하지 않겠지만 그 요청을 스프링에서 관리하는 필터(빈)에게 전달만 하면 스프링 시큐리티 에서 Filter 기반으로 보안 처리를 할 수 있게 되는 것이다.
요약

  • 서블릿 필터는 스프링에서 정의된 빈을 주입해서 사용할 수 없음
  • DelegatingFilterProxy이 특정한 이름을 가진 스프링 빈을 찾아 그 빈에게 요청을 위임
    -- springSecurityFilterChain 이름으로 생성된 빈을 ApplicationContext 에서 찾아 요청을 위임
    --실제 보안처리를 하지 않음

FilterChainProxy
springSecurityFilterChain
스프링 시큐리티가 초기화 될 때 스프링 빈으로 생성되는 클래스

각 필터들은 순서대로 체인으로 다 연결되어 있으므로 필터의 요청이 끝나면 다시 FilterChainProxy 를 호출하여 순서대로 필터를 처리한다.

요청을 인과 처리 또는 인정 처리 할 수 있도록 각 필터들을 순서대로 처리하므로,
순서를 그 필터의 역할에 맞게끔 그 기준에 필터가 처리하는 그 어떤 처리 역할에 맞는 그 데이터 처리를 우리가 그 전 후로 필터를 만들어서 추가해야 된다.
이 모든 필터가 요청에 대한 처리가 다 완료가 되면 그 다음 최종적으로 서블릿에 접근하게된다.

이 순서대로 그렇게 해서 사용자의 요청이 처음 필터부터 마지막 필터까지 특별한 인증 예외 또는 인과 예외가 발생하지 않으면 보안이 통과되는 것이다.
우리 프로젝트 켜서 FlterChianProxy 구성 한번 보고 자료에 넣자



Http 요청 -> WAS -> 서블릿 -> 필터1 -> 필터2 ... -> DelegatingFilterProxy -> 스프링 시큐리티 관련 필터... -> 다시 돌아와서 WAS 등록 필터 처리 이후 ->디스패처 서블릿 -> 인터셉터1 -> 인터셉터2 ... -> 컨트롤러
FilterChainProxy 의 모든 필터가 통과되면 DispatcherServlet 로 간다고 했는데 그 의미는 FilterChainProxy 에서 처리하는 필터를 통과하지 않으면 DispatcherServlet 로 갈수 없음을 강조하기 위한 설명이라고 이해해 주시면 될 것 같습니다. 당연히 FilterChainProxy 내 필터 처리 이후에는 was 등록 필터로 가는 것이 맞습니다.

이쪽은 Sub-Lit 스펙을 지원하는 컨테이너고 이쪽은 Spring 컨테이너에서 생성되는 빈드를 관리하는 영역입니다.
Sub-Lit 자원으로 가기 전에 그래서 요청에 대해서 각각의 필터들이 치를 하게 되고요 필터. 필터가 그 역할을 하는 거죠.
Sub-Lit 자원으로 가기 전에 그래서 요청에 대해서 각각의 필터들이 치를 하게 되고요 그 중에서 DelegatedFieldProxy가 있죠
이 클래스가 요청을 받게 되면 이 클래스는 자기가 전달받은 요청 객체를 특정한 이름을 가진 빈을 찾아서 DelegatedRequest 요청을 위임하게 되는데 그 springSecurityFilterChain
실제로는DelegatedFieldProxy가가 필터로 등록될 때 springSecurityFilterChain 으로 (동일한) 이름으로 등록해요. 내부적으로는 자신의 이름을 찾는것입니다
그래서 이 이름을 가진 빈이 바로 FlterChianProxy 이다. (스프링에서는 필터 체인 프락시 이 클래스를 Bean로 등록할 때 이름을 설정해줄 수 있음)
그러면 이 필터 체인 프락시는 요청에 대해서 각각의 필터, 각각의 필터별로 호출을 해서 자기가 관리하고 있는 모든 필터들을 보안 처리를 하게 되겠죠.

보안 처리 다 끝난 이후 DispatcherServlet , 스프링 MVC와 같은 여기로 요청을 전달해서 스블릿의 실제 요청에 대한 처리를 하게 된다.


(DelegatedFieldProxy가 등록되는 코드 : SecurityFilterConfiguration.java)



DelegatingFilterProxy 에 실제 사용자 요청이 왔을 때 위와 같이 WebApplicationContext 에서 FilterChainProxy를 찾음
이후 찾은 FilterChainProxy로 delegate.doFilter 호출하며 요청 위임하는 것을 확인
그러면 이제 DelegatingFilterProxy의 역할은 끝



FlterChianProxy 여기서는 그 요청을 받아서 보안처리를 할 때 그 필터에서 지금 스프링 필터가 초기화되면서 생성되는 필터들을 갖고온 후 처리한다.

그 바로 아래 코드에는 Virtual Filter Chain 가 있는데, 그 Chain을 연결하는 어떤 별도의 가상 클래스를 만든 거에요.
여기서 실제로 전달받은 자기가 목록을 관리하고 있는 필터들을 각 순서대로 각각 호출하면서 doFilter로 처리중입니다.


필터 초기화와 다중 설정 클래스

자 그러면 그러면 초기화 될 때 스프링 시큐리티 초기화 될 때 우리가 이렇게 만약에 두개의 설정 클래스를 만들어서 이렇게 구성을 했다면 초기화 2개의 필터체인이 생기고,
각 Config에서 생성된 Filter는 SecurityFilterChain 객체 안의 Filter 객체에 생성이 됨,
antMatcher에서 설정해준 url 정보가 SecurityFilterChain 객체 안의 RequestMacher 객체에 생성됨,
이렇게 각각의 생성된 2개 객체를 FilterChainProxy가 또 SecurityFilterChains라는 List 변수에 제장을 하게 됩니다.

그래서 FilterChainProxy가가 request url 에 따라 나눠서 설정 클래스 여러 개 만들더라도 리퀘스트 매치만 다르게 구분하면 된다.

코드 동작
WebSecurity.javaFilterChainProxy 객체를 만드는 과정을 진행하는데, 초기화 시 SecurityFieldChains라는 이 변수를 넘겨주고, 변수는 설정한 두 개의 DefaultSecurityFieldChains라는 객체가 담겨있고, 여기에 Filter (List) , RequestMatcher (객체) 가 담겨있다.

실제로 요청이 들어오면 아래와 같이 FilterChainProxy는 어떤 filterChains 의 필터를 선택할지 url 정보에 따라서 매칭하고 있음.
추가로 2개의 SecurityConfig 클래스에 @Order(0) / @Order(1) 이런식으로 순서 줘야 서버 기동 시 오류 안난다. (어떤 게 우선순위인지 알아야함, ex anyRequest 가 0순위면 antMatcher 줘도 0순위 필터가 작동, 그 말은 어떤 의미냐면 우리 오더를 줄 때는 넓은 범위의 넓은 범위의 요청, 요청 방식이 후순위로 가야한다는 말이다.)

다중 보안 방식은 인증방식을 다르게 가져가거나 도메인별로 나누어서 보안 환경을 구성할 경우 고려사항이라 보시면 됩니다.
제 같은 경우 사용자 보안과 관리자 보안을 별도로 구성해서 사용한 적은 있습니다.
예를 들자면 /admin/ url 패턴으로 접속하는 모든 요청에 대하여 AdminSecurityConfig 를 구성하고
/users/
url 패턴으로 접속하는 모든 요청에 대하여 UserSecurityConfig 를 구성하는 식입니다.
물론 하나의 SecurityConfig 로도 구성은 가능합니다만 두개로 분리할 시 확장성 면에서 이점이 있습니다.
인증 방식을 별도로 가져갈 수 있고(한쪽은 Form 방식, 한쪽은 Rest 방식) 거기에 따른 여러가지 필터나 보안 옵션들도
각각 다르게 구성이 가능할 것 입니다.
http.antMatcher("/api/**) 로 접속하는 요청에 대해서는 Rest 방식의 보안 처리를 하고 그 외의 요청은 Form 방식으로 처리하는 식으로 생각해 볼 수 있습니다. AuthenticatinEntryPoint 같은 경우도 인증에 실패할 경우 ExceptionTranslationFilter 에서 호출해 주는데 Form 방식은 LoginUrlAuthenticationEntryPoint 를 사용하고 Rest 라면 BasicAuthenticationEntryPoint 혹은 별도의 AuthenticationEntryPoint 를 구현해서 사용할수도 있을 것입니다.
요약하면 하나의 보안 설정으로 가면 관리하기는 쉬운면은 있으나 인증방식을 두가지 동시에 가지고 가기가 어렵고 설정들도 분리하기 힘든데 반해 다중 보안 설정으로 가면 관리하는부분은 상대적으로 비용이 더 들어도 여러 인증 방식들을 조합해서 사용하거나 도메인별로 분리해서 사용할 수 있는 잇점이 있는 것 같습니다.
상황과 필요에 맞게 정책을 잘 세워서 진행하면 될 것 같습니다.


Authentication

  • 누구인지 증명하는 것
  • 사용자의 인증 정보를 저장하는 토큰 개념
  • 인증 시 id 와 password 를 담고 인증 검증을 위해 전달되어사용된다
  • 인증 후 최종 인증 결과 (user 객체, 권한정보) 를 담고 SecurityContext 에 저장되어 전역적으로 참조가 가능하다
  • Authentication authentication = SecurityContexHolder.getContext().getAuthentication()

구조

  • principal : 사용자 아이디 혹은 User 객체를 저장 (type : Object형)

  • credentials : 사용자 비밀번호 (자격 증명)

  • authorities : 인증된 사용자의 권한 목록

  • details : 인증 부가 정보

  • Authenticated : 인증 여부

  • 이 authentication 인터페이스를 구현해서 구현체를 만든 다음에 우리만의 어떤 인증 처리를 위해서 또는 인증 정보 결과를 담는 그 역할을 하기 위해 우리가 직접 사용이 가능하다.

    시큐리티에서는 기본적으로 Authentication 인터페이스를 구현한 클래스들이 있음
    예를 들어서,
    UsernamePasswordAuthenticationToken 생성자가 2개 있는데
    하나는 인증처리를 Provider에게 넘기기 위해 생성하는 생성자 한개, (request 시 id/pwd setAuthenticated(false))
    다른 하나는 Provider 에서 인증 완료 처리 후 생성하는 생성자 한개 (실제 정보 UserDetails / Credential, authorities,setAuthenticated(true)) 이렇게 있는것이다.
    Provider가 인증완료 후 자신을 호출한 UsernamePasswordAuthenticationFilter에게 인증객체를 전달을 하게 되는 구조
    Filter에서SecurityContexHolder.getContext().setAuthentication(authResult) 전역 셋팅 해줘서
    인가 처리나 이런 시큐리티 다른 부분에서 (ex 인가 처리 Interceptor) SecurityContexHolder.getContext().getAuthentication() 이 존재하면 이 자체가 이 사용자가 인증을 받은 사용자로 인식하기 때문에 인증의 과정을 따로 동일하게 거치지 않아도 되게 함. (=인증을 유지함)


SecurityContextHolder, SecurityContext

SecurityContext

Authentication 객체가 저장되는 보관소로 필요 시 언제든지 Authentication 객체를 꺼내어 쓸 수 있도록 제공되는 클래스
ThreadLocal 에 저장되어 아무 곳에서나 참조가 가능하도록 설계함
인증이 완료되면 HttpSession 에 저장되어 어플리케이션 전반에 걸쳐 전역적인 참조가 가능하다

SecurityContextHolder

SecurityContext 객체 저장 방식

  • MODE_THREADLOCAL : 스레드당 SecurityContext 객체를 할당, 기본값
  • MODE_INHERITABLETHREADLOCAL : 메인 스레드와 자식 스레드에 관하여 동일한 SecurityContext 를 유지
  • MODE_GLOBAL : 응용 프로그램에서 단 하나의 SecurityContext를 저장한다

자 사용자가 로그인을 합니다 로그인을 시도하면 서브가 그 로그인 요청을 받구요.
그러면 서브에서 하나의 Thread를 생성하겠죠.
Thread는 ThreadLocal 이라는 Thread의 전역 저장소가 Thread마다 할당이 되겠죠.

SecurityContext가 저장되는 곳

SecurityContext라는 객체는 HTTPSession에도 저장이 되고 인증된 사용자가 인증 이후에 사이트에 접속을 할 때는 실제로 세션에 저장된 SecurityContet 객체를 가지고 와서 다시금 그 객체를 ThreadLocal에 저장하는 식으로 처리가 되고 있습니다
인증이 된 후 Authentication 이 저장되는 곳은 SecurityContext 입니다.

그리고 SecurityContext 가 저장되는 곳이 ThreadLocal 이고 이 역할을 하는 클래스가 SecurityContextHolder 클래스입니다.

그래서 SecurityContextHolder 는 SecurityContext 를 감싸고 있는 클래스이지 저장한다는 개념은 아닙니다.

그리고 HttpSession 은 인증 후 결과 정보가 있는 SecurityContext 를 담아놓고 사용자가 계속 인증을 유지하기 위한 목적으로 사용되고 있지만 SecurityContextHolder 가 HttpSession 에서 SecurityContext 를 꺼내어 다시 ThreadLocal 에 저장하고 있습니다.

그래서 어디에서나 Authentication 을 참조할 수 있도록

SecurityContextHolder.getContext().getAuthentication() 와 같은 구문을 사용할 수 있게 됩니다.

요약하자면

SecurityContext 가 최종 저장되는 곳은 ThreadLocal 이라고 보시면 됩니다.
다만 HttpSession 이 인증에 성공할 경우에 SecurityContext 를 저장하는 것은 맞지만 스프링 시큐리티가 결국은 SecurityContextHolder 를 사용해서 HttpSession 에서 SecurityContext 를 꺼내어 ThreadLocal 에 다시 저장하고 있기 때문에 HttpSession 이 ThreadLocal 과 비슷한 역할을 한다고 볼 수는 없을 것 같습니다.

Authentication authentication = SecurityContextHolder.getContext().getAuthentication() 또는 HttpSession에서 SecurityContet 을 꺼내와서 사용 가능

사용자가 서버에 접속하면 서블릿 컨테이너의 쓰레드 풀에서 쓰레드 하나를 할당받아 사용하는 걸로 알고 있는데, 시큐리티에서 사용되는 쓰레드가 풀에서 받은 쓰레드랑 동일한 것일까요?

네 동일한 스레드가 맞습니다.
스레드는 보통 WAS 에서 생성되어 요청을 처리하게 되는데 시큐리티도 스레드의 요청을 받아서 인증 및 인가처리를 하기 때문에 동일한 스레드 안에서 진행됩니다.

톰캣의 쓰레드 풀 설정을 보통 몇십개에서 몇백개 사이로 하던데, 동시 접속자 수가 많아져 쓰레드 개수가 부족하면 인증 정보가 SecurityContext에 어떻게 관리될지가 궁금합니다.

SecurityContext 는 스레드별로 독립적으로 제어가 되기 때문에 스레드의 개수와는 상관이 없습니다.
만약 스레드 개수가 많아져서 더 이상 서버에서 처리가 불가능 하거나 스레드가 사용가능 할 때까지 대기하는 상황이 발생한다면 시큐리티도 더 이상 요청을 받을 수 없게 되고 현재 진행 중인 요청만을 처리하게 됩니다.
물론 대기 상태가 다시 해제되어 스레드가 가용이 된다면 시큐리티가 요청을 다시 받아 SecurityContext 에 인증정보를 담는 처리를 하게 됩니다.
즉 스레드의 일정양을 초과하는 수는 리소스 자원을 크게 소모하기 때문에 메모리 부족으로 인한 어플리케이션이 죽거나 요청을 받지 못하는 상태로 인해 네트워크 오류 등으로 문제가 발생하는 부분이지 시큐리티의 직접적인 내부 흐름과는 상관이 없습니다. 결국 시큐리티도 어플리케이션이 죽으면 아무런 의미가 없는 거라 할 수 있습니다.

SecurityContextPersistenceFilter

인증 전
SecurityContextPersistenceFilter는 새로운 SecurityContext를 생성하고 다음 필터로 이동하는 거죠.
그리고 실제로 인정필터가 그 Secret Context 새롭게 생성된 그 Context 안에 인정에 최종 성공한 인정 객체를 제정하는 거죠. 그러고 다음 필터로 넘어가서 자 그리고 이제 최종적으로 클라이언트에게 응답할 때 응답하는 그 시점에 SecurityContext를 Session에 저장한다 (물론 이 저장하는 역할은 AuthFilter가 마저 하는 것이 아니라 SecurityContextPersistenceFilter 가 한다.) Session에 저장하고 나서 SecurityContext를 SecurityContextHolder에서 제거시키고 최종적으로 클라이언트에게 응답합니다.

인증 후
Session에서 SecurityContext를 찾아서 있으면 SecurityContext를 SecurityContextHolder에 저장한다.
그리고 이미 SecurityContext 안에는 인증 객체가 제정되어 있는 상태가 되겠죠
그렇기 때문에 시크릿 컨텍스트 안에 인증 계획체가 계속적으로 유지가 되고 존재하기 때문에 이 사용자는 별도 인증 과정을 따로 거치지 않습니다

처음에 SecurityContext를 만들었을 경우에는 인증 객체가 없고, 인증 성공하게 되면 인증 객체가 새로 생성이 되고,
그 인증객체를 담은 SecurityContext 객체는 세션에 저장이 됩니다.
후에 그 세션에서 꺼내온 SecurityContext를 다시 ThreadLocal에 저장한 다음 Secret Context 홀드에 다시 저장하여 인증 상태 유지 및 판별을 하게 됩니다.

최신 버전에서는 사용자가 직접 정의한 인증 필터를 만들어서 인증처리를 할 경우 세션에 SecurityContext 를 저장할 것인지 아닌지를 선택할 수 있고 그 선택에 따라 인증 처리 방식을 유연하게 가져 갈 수 있습니다.
보통 JWT 토큰을 이용한 인증방식은 세션을 사용하지 않기 때문에 별도의 JWT 인증 필터를 만들더라도 기본적으로 세션을 사용하지 않도록 되어 있기 때문에 개발자가 명확하게 세션을 사용할 것인지 아닌지의 정책을 확실하게 정해야 한다는 의미이기도 합니다.

참고로 HTTP 세션을 사용하지 않는다면 SecurityContextPersistenceFilter 는 요청 스레드마다 SecurityContextHolder.getContext() 에서 항상 새로운 SecurityContext 를 반환하게 되고 인증 객체가 null 인 상태이기 때문에 매번 인증을 완료한 후 SecurityContext 에 저장하는 처리를 해 주어야 시큐리티가 인증 사용자로 인식하게 됩니다.


Authentication Flow

AuthenticationManager


ProviderManager.java : 가장 기본(deafult) 구현체 라고 보면 됨.
AuthenticationManagerBuilder.java

  • AuthenticationManager 클래스의 객체를 생성하는 역할
  • AuthenticationProvider를 추가할 수 있도록 API를 제공
  • 자식에서 적절한 AuthenticationProvider를 찾지 못할 경우 계속 부모로 탐색하여 찾는 과정을 반복하려면 스프링 시큐리티의 초기화 과정에서 설정한 기본 Parent 관계를 변경해야 권한 필터에서 재 인증 시 모든 AuthenticationProvider 를 탐색할 수 있다

custom Provider에 supports 구현하는 이유 아래 코드로 확인 가능하다. (인증처리 가능한 Provider인지 판별)

AuthenticationProvider

AuthenticationManager 가 현재 인증을 처리할 수 있는 가장 적절한 AuthenticationProvider를 선택을 해서 인증 처리를 맡긴다.
이 Provider가 최종 인증에 성공하게 되면 이 Provider 안에서 성공한 인증 객체 인증 객체를 생성해서
다시 자기를 호출한 그 ProviderManager 에게 인증 객체를 전달하는 역할까지 AuthenticationProvider 가 하게 된다.

UserDetailsService : 현재 ID에 해당하는 유저 개체가 유저 정보가 존재하지 않는지 처리


Authorization, FilterSecurityInterceptor

AccessDecisionManager, AccessDecisionVoter

인가 관련해서는 어떻게 할지 고민했는데, 인가 는 그냥 깊게 안가고 개념정도만소개하고 넘어가도될듯.

모노러틱 방식
session

마이크로 서비스의 인증 방식
JWT
OAuth2


OAuth2 JWT Security

Spring Security Fundamental

초기화

  • SecurityBuilder 는 빌더 클래스로서 웹 보안을 구성하는 빈 객체와 설정클래스들을 생성하는 역할을 하며 WebSecurity, HttpSecurity 가 있다
  • SecurityConfigurer 는 Http 요청과 관련된 보안처리를 담당하는 필터들을 생성하고 여러 초기화 설정에 관여한다
  • SecurityBuilder 는 SecurityConfigurer 를 포함하고 있으며 인증 및 인가 초기화 작업은 SecurityConfigurer 에 의해 진행된다.

그래서 실제로 초기화 작업이 진행되면AutoConfiguration(자동 설정)에서 SecurityBuilder 클래스에 build() 메소드가 실행이 되면 실행이 되는 과정 속에서 SecurityConfigurer 설정 클래스에 init과 configure 메소드 (여기서 이제 필터도 만들고 그 다음에 Authentication Provider/Manager 등 인증인가에 필요한 필터와 객체들을 만듦) 가 호출이 되고 이 두 개의 메소드 안에서 초기화 설정 작업이 이루어지는 구조로 되어있습니다

초기화 - 세부 구조


WebSecurity 가 반환한 FilterChianProxy 는 HttpSecurity 가 반환한 SecurityFilterChain 의 값을 가지고 있습니다.
그래서 FilterChianProxy가 SecurityFilterChain에 있는 여러 개의 필터들을 사용자요청을 처리할 때 실행시켜서 진행해야 됩니다.

실제 코드를 보면 WebSecurityConfiguration.java 에서 초기화가 진행되는데,

  • WebSecurity 는
    -> SecurityConfigurer 인터페이스 구현체인 설정 클래스 타입들을 가지고 와서 반복해서 webSecurity . apply() 으로 인자를 전달하면서 Apply가 되면 이 설정 클래스는 초기화 때 초기화 과정에서 Init과 Configure 메소드를 통해서 초기화 작업이 이루어집니다.

  • HttpSecurity 는
    -> 현재 이 API들은 각각의 시큐리티 컴퓨터 설정 클래스의 구인체를 생성하고 그리고 그 구현체 안에 있는 init과 configure를 실행할 수 있는 대상으로 지정하기 위한 구문이다.

초기화 과정 15:00/40:00부터 다시..
28:00/40:00 부터 내가 만들어서 적용하는것 내부 과정 나옴

profile
공부를 열심히 하는 학부생

0개의 댓글

관련 채용 정보