728x90

OAuth 2.0

애플리케이션에서 사용자의 인증을 직접 처리하지 않고

신뢰할 수 있는 써드 파티 애플리케이션이 대신 처리하게 한 후에

해당 써드 파티 애플리케이션의 서비스까지 사용할 수 있는 방식이다.

 

간단하게 말하자면 요새 자주 사용되는

구글/카카오/네이버 등을 이용한 로그인 방식이다.

 

기존 서버에서 인증을 하고 외부 애플리케이션의 서비스를 이용하려면

외부 애플리케이션에서도 인증을 받아야 해서

두 번의 인증 과정이 필요하지만

이 방식을 사용하면 한 번의 인증만으로도 가능해진다.

 

인증 방식

OAuth 2.0를 사용하지 않는 기존의 로그인 방식은

클라이언트 요청 → 웹 서버 인증 → 외부 애플리케이션 서버 인증 순서로 진행되었다면

OAuth 2.0를 사용하게 되면

클라이언트 요청 → 웹 서버가 써드 파티 애플리케이션이 인증 위임 순서로 진행된다.

 

자세하게 적어보자면 아래와 같다.

  1. 클라이언트의 인증 요청
  2. 웹 서버가 써드 파티 애플리케이션에 인증 위임
  3. 써드 파티 애플리케이션이 인증 처리 후 성공하면 액세스 토큰 전달
  4. 웹 서버가 액세스 토큰을 사용하여 외부 애플리케이션의 API 사용

 

사용 유형

써드 파티 애플리케이션의 API를 직접적으로 사용하는 경우처럼

캘린더나 지도 등의 외부 API를 사용하는 애플리케이션을 이용할 때 사용된다.

 

사용자의 크리덴셜 정보를 남기고 싶지 않은 경우에도

OAuth 2.0을 사용하여 추가적인 인증 서비스를 사용할 수 있다.

 

728x90

이전에 폼 로그인 방식의 로그인/로그아웃을 구현하면서 간단하게 구조를 분석해 보았지만 

시큐리티를 배우는 과정에서 좀 더 정확하게 배우고 넘어가야 할거 같아서

공식문서를 참고하며 구조를 처음부터 정확하게 분석해 보겠다.

 

전체적인 흐름

 

 

위의 이미지는 공식문서에서 제공해 주는 클라이언트의 요청부터 스프링 시큐리티의 필터가 처리되는 과정이다.

ServletFilterChain
	ServletFilter
		DelegatingFilterProxy
			FilterChainProxy
				SecurityFilterChain
					SecurityFilter (ExceptionTranslationFilter, etc...)

이미지에 나타난 구조를 계층으로 표현하면 위와 같다.

 

코드로 살펴보기 전에 위의 과정을 글로 간단하게 정리해보겠다.

 

클라이언트가 서버에 요청을 보내면 서블릿 컨테이너는 필터 체인과 서블릿 생성

  • 필터체인은 필터 인스턴스들을 포함한다.
  • 서블릿은 요청 URI 경로를 기반으로 HttpServletRequest를 처리한다.
  • Spring MVC에서 서블릿은 DispatcherServlet의 인스턴스를 의미한다.
  • 실제 필터의 동작은 서블릿 필터에 전달되는 스프링 시큐리티의 필터 체인이 수행한다.

 

 

DelegatingFilterProxy를 통해 서블릿 컨테이너와 스프링 컨테이너를 연결

  • 서블릿 컨테이너와 스프링 컨테이너는 서로 다른 영역이다.
  • 스프링 시큐리티의 필터 체인은 스프링 컨테이너에 존재한다.
  • 스프링 컨테이너에 빈으로 등록되어 있는 필터 체인을 사용하기 위한 다리 역할이다.
  • 즉, 스프링 컨테이너에서 필터의 빈을 조회하고 호출한다.

 

FilterChainProxy를 통한 스프링 시큐리티 필터 체인 관리 및 실행

  • 스프링 시큐리티에서 제공하는 특수한 필터다.
  • SecurityFilterChain을 통해 필터 인스턴스들을 위임한다.
  • FilterChainProxy는 빈이기 때문에 DelegatingFilterProxy에 래핑 된다.
  • 등록된 필터 체인을 순서대로 실행한다.

 

SecurityFilterChain을 통한 스프링 시큐리티 필터 선택

  • 현재 요청에 대해 FilterChainProxy가 어떤 스프링 시큐리티 필터를 사용할지 결정한다.

 

SecurityFilter 삽입

  • SecurityFilterChain API를 사용해 FilterChainProxy에 삽입한다.
  • 필터의 순서는 아래와 같다.
  • 더보기
    ForceEagerSessionCreationFilter
    ChannelProcessingFilter
    WebAsyncManagerIntegrationFilter
    SecurityContextPersistenceFilter
    HeaderWriterFilter
    CorsFilter
    CsrfFilter
    LogoutFilter
    OAuth2AuthorizationRequestRedirectFilter
    Saml2WebSsoAuthenticationRequestFilter
    X509AuthenticationFilter
    AbstractPreAuthenticatedProcessingFilter
    CasAuthenticationFilter
    OAuth2LoginAuthenticationFilter
    Saml2WebSsoAuthenticationFilter
    UsernamePasswordAuthenticationFilter
    DefaultLoginPageGeneratingFilter
    DefaultLogoutPageGeneratingFilter
    ConcurrentSessionFilter
    DigestAuthenticationFilter
    BearerTokenAuthenticationFilter
    BasicAuthenticationFilter
    RequestCacheAwareFilter
    SecurityContextHolderAwareRequestFilter
    JaasApiIntegrationFilter
    RememberMeAuthenticationFilter
    AnonymousAuthenticationFilter
    OAuth2AuthorizationCodeGrantFilter
    SessionManagementFilter
    ExceptionTranslationFilter
    AuthorizationFilter
    SwitchUserFilter

 

ExceptionTranslationFilter을 통한 예외처리

  • AuthenticationException과 AccessDeniedException를 HTTP 응답으로 변환 가능하다.
  • SecurityFilter 중 하나로 FilterChainProxy 삽입된다.
  • AuthenticationException라면 인증을 시작한다.
  • AccessDeniedException라면 접근을 거부한다.

 

인증 간의 요청 저장

요청에 인증이 없거나 필요한 경우 인증 성공 후 다시 요청을 하려면

기존 요청에 대한 정보를 가지고 있어야 한다.

 

스프링 시큐리티에서는 RequestCache를 구현하여 HttpServletRequest를 저장한다.

 

RequestCache에 HttpServletRequest 저장

HttpServletRequest가 저장된 후에 사용자가 인증에 성공하면

RequestCache에 저장된 HttpServletRequest가 재사용 된다.

 

기본적으로 RequestCacheAwareFilter가 RequestCache를 사용해

HttpServletRequest를 저장하는 역할을 수행한다.

'Back-End > Security' 카테고리의 다른 글

OAuth 2.0 동작 방식  (0) 2023.07.17
OAuth 2.0  (0) 2023.07.17
JWT  (0) 2023.07.12
토큰 기반 자격 증명  (0) 2023.07.11
[스프링 시큐리티] 접근 제어 표현식  (0) 2023.07.11
728x90

JWT (JSON Web Token)는 이름 그대로 JSON 포맷을 사용하여

데이터를 안전하고 간결하게 전송하는 표준 인증 방식이다.

 

JSON 포맷의 토큰 정보를 인코딩 하고, 인코딩 된 토큰 정보를

비밀키로 서명한 메시지를 Web Token으로 인증 과정에 사용한다.

 

JWT의 종류

Access Token

사용자의 이메일, 연락처, 사진 등과 같은

보호된 정보에 접근할 수 있는 권한 부여에 사용된다.

 

클라이언트가 처음 인증을 받으면 

Access Token과 Refresh Token을 모두 받지만

실제 권한을 얻는데 사용되는 토큰은 Access Token이다.

 

사실상 권한을 부여 받을 때는 해당 토큰만 있어도 상관 없지만

해당 토큰을 탈취 당하는 경우에는 탈취한 쪽에서도

해당 토큰을 사용해 권한을 얻을 수 있기 때문

이 토큰의 유효 기간은 짧게 지정해둔다.

 

Refresh Token

실제 권한을 얻을 때 사용되지는 않지만

유효 기간이 짧은 Access Token을 재발급 받을 때 사용한다.

 

별도의 재인증 없이 해당 토큰을 사용하여

Access Token 토큰을 재발급 받을 수 있다는 장점이 있지만

재인증 없이 다시 기존의 권한들을 사용할 수 있다는 것이

보안적으로는 좋지 않기도 하다.

 

그렇기 때문에 사용자의 편의를 중요하게 생각하면

Refresh Token을 사용하여 재발급을 편하게 할 수도 있고

보안을 더 중요하게 생각하면 해당 토큰을 사용하지 않기도한다.

 

JWT의 구조

aaaaaa.bbbbbb.cccccc
(Header).(Payload).(Signature)

JWT는 위와 같이 점으로 세 부분으로 구분된다.

JSON 객체를 base64 방식으로 인코딩하면 JWT의 각 부분이 완성된다.

 

Header

{
	"alg": "HS256",
  	"typ": "JWT"
}

해당 토큰이 어떤 종류의 토큰이고 어떤 알고리즘으로 Sign할지 정의하는 부분이다.

 

Payload

{
    "sub": "someInformation",
    "name": "phillip",
    "iat": 151623391
}

서버에서 활용 가능한 사용자의 권한이나 정보 같은 데이터 등을 담을 수 있다.

가급적 민감한 정보는 담지 않는 것이 좋다.

 

Signature

Signature에서 원하는 비밀키Header에서 지정한 알고리즘으로

Header와 Payload의 데이터를 단방향 암호화 한다.

 

암호환된 메시지는 토큰의 위변조 유무 검증에 사용된다.

 

토큰 기반 인증 절차

  1. 클라이언트가 서버에 로그인 정보를 담아 요청을 보냄
  2. 로그인 정보 일치 확인
  3. 일치하면 암호화된 토큰 생성 (Access Token, Refresh Token)
  4. Refresh Token은 Access Token을 재발급 하는 용도기 때문에 같은 정보를 담지 않음
  5. 토큰을 클라이언트에 전송
  6. 클라이언트는 로컬 저장소, 세션 저장소, 쿠키 등에 토큰을 저장
  7. 클라이언트는 이후 HTTP Header나 쿠키에 토큰을 담아 요청에 사용

 

장점

확장에 유리하다

서버가 클라이언트에 대한 정보를 저장하지 않고 검증만 한다.

 

세션은 여러 대의 서버를 이용한 서비스인 경우에는

서버마다 세션에 대한 정보를 저장하고 있어야 하지만

토큰을 사용하면 그럴 필요가 없다.

 

클라이언트의 요청마다 인증을 할 필요가 없다

세션 같은 경우는 요청을 전송할 때마다 쿠키에 인증 정보를 포함하지만

토큰은 만료되기 전까지 한 번의 인증만으로 충분하다.

 

인증 시스템을 서버와 분리할 수 있다

구글이나 카카오 같은 다른 플랫폼의 자격 증명 정보로 인증하는 것이 가능하다.

토큰 생성용 서버를 만들거나 다른 플랫폼에 토큰 관련 작업을 맡기는 등

다양한 활용이 가능하다.

 

권한 부여에 용이하다

Payload에 사용자의 권한 정보를 포함하는 것이 용이하다.

 

단점

디코딩이 용이하다

Payload는 base64로 인코딩 되어 디코딩하기도 쉽기 때문에

민감한 정보는 포함하지 않는 것이 좋다.

 

토큰의 길이가 네트워크에 부하를 준다

토큰에 저장하는 데이터가 많아질수록 토큰의 길이가 늘어나는데

요청을 전송할 때마다 토큰을 전송하기 때문에

길이가 긴 토큰을 전송하는 것은 네트워크에 부하를 준다.

 

자동으로 삭제되지 않는다

한 번 생성된 토큰은 자동으로 삭제되지 않아서 만료 시간을 반드시 지정해야 한다.

너무 길지도 짧지도 않은 적당한 기간을 지정하는 것이 좋다.

728x90

HTTP 프로토콜의 비상태성 특성의 단점을 해결하기 위해

기존에는 세션을 사용하여 상태를 유지하는 방법을 사용했다.

 

세션 기반 자격 증명

세션 기반 자격 증명 방식은 서버 측에 인증된 사용자의 정보를

세션 형태로 세션 저장소에 저장한 후에 클라이언트 측에서 요청을 하면

세션 저장소의 세션과 사용자가 제공하는 정보가 일치하는지 확인한다.

 

즉, 인증된 사용자의 정보를 서버 측에서 관리하고

클라이언트 쪽은 세션 ID만 사용하여 상대적으로 적은 네트워트 트래픽을 사용한다.

 

서버 측에서 세션 정보를 관리하여 보안성 측면에서도 유리하다고 볼 수 있지만

서버의 확장성 면에서 세션 불일치 문제가 발생할 수도 있고

세션의 데이터가 많아질수록 서버의 부담이 가중된다.

 

CSR 방식보다는 SSR 방식에 적합한 방식이다.

 

토큰 기반 자격 증명

토큰은 티켓이나 입장권을 떠올리면 이해하기 쉽다.

 

현실에서 어떤 무언가를 이용할 때 사용자는 돈을 지불하여

무언가를 이용할 권한(토큰)을 얻는다.

 

여기서 돈을 지불하는 것

사용자가 Credential로 로그인 정보를 주는 것에 해당하고

지불한 돈에 맞는 권한을 얻는 것을 토큰에 해당한다.

 

여기서 세션 기반 자격 증명과 다른 점은

인증 정보(세션)를 서버 측에서 갖고 있는 것이 아닌

사용자가 직접 토큰을 갖고 있는 것이다.

 

세션 기반 방식에서는 사용자는 세션 아이디만 가지고

서버 측에 요청을 했다면 토큰 방식은 이와 반대로

클라이언트가 토큰으로 모든 인증된 사용자 정보를 가지고 요청하기 때문에

세션 방식에 비해 많은 네트워크 트래픽을 사용한다.

 

서버 측에서 세션을 관리하지 않기 때문에 보안적으로는 불리하지만

세션처럼 인증된 사용자 요청 상태를 유지할 필요가 없어

서버의 확장에 유리하고 세션 불일치 같은 문제가 없다.

 

클라이언트 측에서 토큰에 암호화 되지 않은 상태의

사용자 정보를 가지고 있기 때문에 민감한 정보는 포함하지 않는 것이 좋다.

 

한 번 발급한 토큰은 만료되기 전까지 무효화가 불가능하다.

 

CSR 방식에 적합하다.

 

단순하게 세션 방식의 특징과 정반대라고 생각하면 편하다.

728x90
hasRole(Stirng role) 현재 보안 주체가 지정된 역할을 갖고 있는지 여부 확인
파라미터로 넘긴 role이 ROLE_로 시작하지 않으면 추가
DefaultWebSecurityExpressionHandler의 defaultRolePrefix를
수정하여 커스텀 할 수 있다.
hasAnyRole(String… roles) 지정한 역할 하나라도 갖고 있는지 여부 확인
hasAuthority(String authority) 지정한 권한을 갖고 있는지 여부 확인
hasAnyAuthority(String… authorities) 지정한 권한 중 하나라도 갖고 있는지 여부 확인
principal 현재 사용자를 나타내는 principal 객체에 직접 접근 가능
authentication encurityContext로 조회할 수 있는
Authentication 객체에 직접 접근 가능
permitAll 모든 권한 허용
denyAll 모든 권한 차단
isAnonymous() 현재 보안 주체가 익명 사용자인지 확인
isRememberMe() 현재 보안 주체가 Remember-Me 사용자인지 확인
isAuthenticated() 사용자가 익명이 아닌지 확인
isFullyAuthenticated() 사용자가 익명 혹은 Remember-Me 사용자가 아닌지 확인
hasPermission(Object target, Object permission) 사용자가 타겟에 해당 권한(permission)이 있는지 확인
hasPermission(Object targetId, String targetType, 
Object permission)

 

728x90

인증 처리 흐름

인증 방식은 여러 종류가 있지만 우선은 UsernamePasswordAuthenticationFilter에 대해 살펴보겠다.

1. 사용자의 로그인 요청

사용자가 로그인을 위해

Credential(로그인 정보)를 서버에 전달한다.

 

2. Authentication 객체 생성 및 전달

UsernamePasswordAuthenticationToken authRequest = 
		UsernamePasswordAuthenticationToken.unauthenticated(username, password);

UsernamePasswordAuthenticationFilter는 사용자 요청을 전달 받아

Authentication 인터페이스의 구현체인 AuthenticationToken을 생성하여

해당 객체를 AuthenticationManager에 전달한다.

 

현재까지의 Authentication은 인증이 된 상태가 아니다.

 

3. AuthenticationManager

AuthenticationManager는 말그래도 인증 관리자 역할을 하는 인터페이스고

이를 구현한 ProviderManager를 통해 전달 받은 Authentication을 사용해

사용자 인증 작업을 관리한다.

 

실제로 인증을 처리하는 역할이 아니라 단계별로 적절한 컴포넌트를 사용해

인증 처리 작업을 시키는 역할이다.

 

4. AuthenticationProvider

AuthenticationProvider는 AuthenticationManager로부터

Authentication을 전달 받아 UserDetailsService를 사용해 UserDetails를 조회한다.

 

UserDetails는 데이터베이스에서 조회한 사용자 인증에 사용할 정보와

사용자의 권한 정보를 포함하고 있는 컴포넌트다.

 

5. 인증된 Authentication 객체 생성 및 전달

AuthenticationProvider는 PasswordEncoder를 이용해

UserDetails와 Authentication의 패스워드를 검증하여

검증에 성공하면 인증된 Authentication 객체를 생성하고

실패한다면 예외를 발생시킨다.

 

검증에 성공하여 생성된 Authentication 객체를 AuthenticationManager에 전달하고

AuthenticationManager 이를 다시 UsernamePasswordAuthenticationFilter에 전달한다.

 

6. 인증된 Authentication 객체 저장

UsernamePasswordAuthenticationFilter는 전달 받은 인증된 Authentication 객체를

SecurityContextHolder를 사용해 SecurityContext에 저장한다.

 

SecurityContext는 이후에 세션 정책에 따라서

HttpSession 혹은 JWT 등의 방식으로 사용자의 인증 상태에 활용된다.

 

인증 처리 흐름 요약

  1. 사용자가 서버에 Credential과 함께 로그인 요청
  2. 인증되지 않은 Authentication  객체 생성
  3. AuthenticationManager가 Authentication  객체를 사용해 인증 작업 관리
  4. AuthenticationProvider가 UserDetailsService를 통해 데이터베이스에서 사용자 인증 정보 조회
  5. PasswordEncoder로 사용자 인증 정보와 Credential의 패스워드 일치 검증
  6. 일치한다면 인증된 Authentication  객체 생성
  7. 인증된 Authentication  객체를 UsernamePasswordAuthenticationFilter로 전달
  8. 인증된 Authentication  객체를 SecurityContextHolder를 사용해 SecurityContext에 저장

 

권한 부여(인가) 처리 흐름

1. AuthorizationFilter

AuthorizationFilter는 SecurityContextHolder에서 Authentication을 가져온 후에

Authentication과 HttpServletRequest를 AuthorizationManager에게 전달한다.

2. RequestMatcherDelegatingAuthorizationManager

AuthorizationManager 인터페이스의 구현체 중 RequestMatcherDelegatingAuthorizationManager가

RequestMatcher 평가식에 매치되는 AuthorizationManager에게 인가 처리를 위임

3. AuthorizationManager

RequestMatcher를 통해 매치되는 AuthorizationManager 구현 클래스가 인가 처리를 수행

 

적절한 권한이라면 사용자의 요청을 계속 처리하고 적절하지 않다면 AccessDeniedException 발생

 

권한 부여 컴포넌트

AuthorizationFilter

권한 부여 필터

URL을 통해 사용자의 액세스를 제한

스프링 시큐리티 5.5 버전 이후부터 FilterSecurityInterceptor를 대체함

 

AuthorizationManager를 주입 받아 적절한 권한 부여 여부를 체크하는데

이때 권한 체크 로직은 URL 기반으로 권한 부여 처리를 하기 위해

RequestMatcherDelegatingAuthorizationManager 구현 클래스를 사용한다.

 

AuthorizationManager

권한 부여 처리를 총괄하는 역할을 담당하는 인터페이스

권한 부여 여부를 체크하는 check 메서드만 정의되어 있음

 

RequestMatcherDelegatingAuthorizationManager

AuthorizationManager의 구현 클래스 중 하나

직접 권한 부여 처리를 수행하지 않음

RequestMatcher를 통해 매치되는 AuthorizationManager 구현 클래스에 처리를 위임

.antMatchers("/orders/**").hasRole("ADMIN")

RequestMatcher 같은 경우는 위의 코드처럼

메서드 체인 정보를 말한다.

728x90

DelegatingPasswordEncoder의 장점

DelegatingPasswordEncoder는 비밀번호를 암호화 할 때 사용했던

PasswordEncoder 구현 객체를 생성해주는 컴포넌트다.

 

사용할 암호화 알고리즘을 지정하지 않으면 스프링 시큐리티가 권장하는

최신 암호화 알고리즘을 적용하여 패스워드를 암호화 해준다.

 

암호화 방식을 변경할 일이 생겨도 언제든 암호화 방식을 바꿀 수 있다.

(기존 암호화 된 패스워드에 대한 마이그레이션 필요)

 

커스텀 DelegatingPasswordEncoder 생성

PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

기존에는PasswordEncoderFactories의 createDelegatingPasswordEncoder 메서드를 사용하여

DelegatingPasswordEncoder 객체를 생성하여 다시 적절한 PasswordEncoder 객체를 생성했다.

String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());

PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);

하지만 위와 같이 DelegatingPasswordEncoder의 생성자로 키값과 맵 객체를 넘겨주면

해당 맵 객체에서 키에 해당하는 암호화 알고리즘을 적용할 수 있다.

 

스프링 시큐리티의 암호화 형식

데이터베이스에서 암호화 된 패스워드를 살펴보면

{암호화알고리즘}암호화된패스워드
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

위와 같은 PasswordEncoder의 ID와 encodedPassword

형식으로 저장된 것을 볼 수 있다.

 

암호화 알고리즘

Plain Text

암호화하지 않은 텍스트 그 자체

반드시 암호화 하는 것이 좋다.

 

Hash

단방향 암호화

패스워드 같은 경우는 다시 복호화 할 필요가 없기 때문에

해시 알고리즘을 사용하는 것도 괜찮다.

 

MD5

단방향 암호화

MD2, MD4 해시 알고리즘의 결함을 보완한 알고리즘

 

다이제스트: 원본 메시지를 암호화한 메시지

 

SHA

MD5의 결함을 보완한 해시 알고리즘

해시된 문자열의 비트 값을 회전하면서 해시 처리를 함

 

Salt

알고리즘보다는 대응책에 가깝다.

원본 메시지에 임의의 문자열을 추가하여 해시 처리 함

 

PBKDF2, bcrypt, scrypt

솔트와 키 스트레칭, 메모리 오버헤드 등에

복잡한 알고리즘까지 더하여

복호화를 어렵게 하는 해시 알고리즘

728x90

사용자의 요청이 왔을 때 스프링 시큐리티가 어떤 일들을 수행하는지

로그인 과정을 통해 알아보겠다.

 

인증과 인가

우선은 인증과 인가의 개념과 차이에 대해서 알아야 한다.

 

이름은 비슷하지만 둘의 역할은 조금 다른데

인증은 말그대로 만약 사용자라는 객체가 있다면

해당 객체가 유효한지 인증해주는 것이다.

 

인가는 인증된 객체가 어디까지 접근할 수 있는지

객체의 권한을 확인한다.

 

로그인을 통해 사이트의 인증을 받았다면

로그인한 사용자의 권한에 따라서

이용할 수 있는 기능을 제한하는 것이 인가라고 볼 수 있다.

사용자 시점

사용자가 정확한 인증 정보를 서버에 제공 하였다면

아래와 같은 순서대로 요청이 처리된다.

 

  1. 정확한 로그인 정보(Credential) 제공
  2. 로그인 성공
  3. 접근 권한이 있는 요청 전달
  4. 응답 받은 정보 조회

스프링 시큐리티 시점

  1. 사용자가 서버에 리소스를 요청
  2. 스프링 시큐리티 인증 관리자가 Credential 요청
  3. 사용자가 Credential 제공
  4. 인증 관리자가 데이터베이스에서 사용자 인증 정보 조회
  5. 유효한 Credential인지 확인
  6. 유효하지 않다면 예외 발생, 유효하다면 인증 처리
  7. 접근 결정 관리자가 인증 처리 정보를 토대로 사용자가 요청한 리소스에 접근할 충분한 권한이 있는지 확인
  8. 권한이 부족하다면 예외 발생, 충분하다면 리소스를 응답

복잡해보이지만 간단하게 말하자면

사용자로부터 인증 정보를 받은 후에 데이터베이스의 인증 정보와 일치하는지 확인 후에

인증에 성공하면 해당 사용자가 리소스에 접근할 권한이 있는지 확인하고

최종적으로 리소스 응답 여부를 결정하는 것이다.

 

즉, 리소스 요청 ▶ 인증 ▶ 인가 ▶ 리소스 전달 순서다.

필터의 처리 과정

클라이언트 측에서 요청을 하면 아래와 같이 서블릿 필터 단계에서 검증하는 과정을 거친다.

  1. 클라이언트의 요청
  2. 서블릿 필터 체인에서 DelegatingFilterProxy를 통해 스프링 시큐리티 필터와 연결
  3. FilterChainProxy에서 스프링 시큐리티 필터 체인을 호출
  4. 2 - 3번 과정을 반복하며 순서대로 모든 필터 체인들을 통해 검증
  5. 검증을 통과했다면 사용자의 요청을 처리하는 과정을 수행

익숙하지 않은 용어들이 많이 나와서 어렵게 보이겠지만 알고나면 간단하다.

 

우선 서블릿 필터 체인과 스프링 시큐리티 필터 체인은

서로 다른 영역(서블릿 컨테이너와 스프링 컨테이너)에 존재하기 때문에

DelegatingFilterProxy를 통해 두 영역을 이어준다.

 

두 영역을 이어주는 이유는

서블릿 필터 단계에서 검증을 해야하기 때문이다.

 

두 영역이 연결되었기에 FilterChainProxy를 통해 서블릿 필터 체인 영역에서

스프링 시큐리티 필터 체인을 호출하여 사용한다.

 

이 과정들을 반복하여 모든 필터를 거쳐 검증이 끝나고 난 후에야

요청이 처리되기 시작한다.

필터

필터는 일상생활에서 공기청정기의 필터처럼

무언가를 걸러내주는 역할을 담당한다.

  • Servlet Container
    • Servlet Filter
      • DelegatingFilterProxy
      • FilterChainProxy
        • Spring Security Filter Chain
    • DispatcherServlet
    • Intercepter
    • Controller, Service, Component 등

서블릿 필터는 위와 같이 실질적인 요청에 대한 처리가 시작되기 전에

검증을 하기 위해 서블릿 컨테이너의 시작점에 존재한다.

필터 체인

  • 체인이라는 말처럼 필터들이 사슬로 연결된 필터의 묶음을 말한다.
  • 서블릿의 필터 체인은 요청 URI를 기반으로 어떤 필터와 서블릿을 매핑할지 결정한다.
  • 필터 체인 안에서 필터의 순서를 지정할 수 있다.
    • @Order 어노테이션이나 Ordered 인터페이스를 구현해 순서를 지정할 수 있다.
    • FilterRegistrationBean을 이용해 필터의 순서를 명시적으로 지정할 수 있다.

커스텀 필터 만들기

public class MyCustomFilter implements Filter {
    // 필터 초기화
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
        System.out.println("필터 생성");
    }

	@Override
	public void doFilter(ServletRequest request,
                        ServletResponse response,
                        FilterChain chain)
                        throws IOException, ServletException {
        // 다음 Filter 전의 전처리 작업	
		System.out.println("필터 시작");
        
        chain.doFilter(request, response); // 필터가 처리하는 실제 로직

        // 후처리 작업
        System.out.println("필터 종료");
    }
	
    // 필터가 사용한 자원 반납
    public void destroy() {
    	System.out.println("필터 삭제");
        Filter.super.destroy();
    }
}

우선 Filter 인터페이스를 구현할 MyCustomFilter 클래스를 생성한다.

 

init 메서드를 통해 필터를 초기화 한 후에

doFilter 메서드를 통해 전처리 및 후처리, 실질적인 필터의 처리 로직을 구현한다.

 

이때 전처리는 ServletRequest 객체를 사용하고

후처리는 ServletResponse 객체를 사용한다.

 

destroy 메서드를 사용하여 사용한 자원을 반납하여 필터를 끝낸다.

@Configuration
public class FilterConfiguration {

    @Bean
    public FilterRegistrationBean<MyCustomFilter> customFilterRegister()  {
        FilterRegistrationBean<MyCustomFilter> registrationBean = new FilterRegistrationBean<>(new MyCustomFilter());
        return registrationBean;
    }
}

커스텀 필터를 적용하기 위해 FilterConfiguration 클래스를 만든 후에

FilterRegistrationBean의 생성자로 필터 인터페이스를 구현한 MyCustomFilter를 넘겨주어

빈으로 등록하여 서블릿 필터 체인에서 사용할 수 있도록 한다.

@Bean
public FilterRegistrationBean<MyCustomFilter> customFilterARegister()  {
    FilterRegistrationBean<MyCustomFilterA> registrationBean = new FilterRegistrationBean<>(new MyCustomFilterA());
    registrationBean.setOrder(1);
    return registrationBean;
}

@Bean
public FilterRegistrationBean<MyCustomFilter> customFilterBRegister()  {
    FilterRegistrationBean<MyCustomFilterB> registrationBean = new FilterRegistrationBean<>(new MyCustomFilterB());
    registrationBean.setOrder(2);
    return registrationBean;
}

만약 여러 개의 필터를 사용하는 경우에는 위와 같이 우선순위를 지정할 수도 있는데

지정한 숫자가 낮을 수록 우선순위가 높은 필터다.

+ Recent posts