Spring Security (2)

강서진·2024년 3월 13일

미니 프로젝트를 시작하기 전 스프링 시큐리티에서 JWT를 사용해 인증 및 인가를 구현해보고자 한다. 하여 스프링 시큐티리 필터에서 사용하는 클래스들을 찾아 문서를 정리해보았다.

기본 Authentication 플로우

  1. 클라이언트가 로그인을 요청한다.

  2. FilterChainDispatcherServlet로 넘어가기 전에 요청을 인터셉트한다.

  3. 요청이 AuthenticationFilter를 거친다. 기본으로 설정된 폼 로그인을 사용하면 UsernamePasswordAuthenticationFilter를 거치게 된다.

  4. 해당 필터는 요청에서 username과 password를 추출하여 UsernamePasswordAuthenticationToken을 만든다.

  5. 이 토큰은 AuthenticationManager에게 전달되고, AuthenticationManager는 가지고 있는 일렬의 AuthenticationProviders에게 토큰을 넘긴다.

  6. 적절한 AuthenticationProvider가 토큰을 authenticate()한다.

    authenticate 과정에서 UserDetailsService에서 loadByUsername()으로 User 객체를 가져와 토큰에 있는 username이 일치하는지 확인한다.
    username이 일치하면 UserDetails를 반환하고, AuthenticationProvider에서 토큰에서 받은 password와 UserDetails에 있는 password가 일치하는지 확인한다. 이 때는 PasswordEncoder의 matches() 메서드를 사용한다.

  7. authenticate()가 성공하면 UserDetailsauthorities를 담은 Authentication 객체를 반환한다.

  8. AuthenticationSecurityContext에 저장된다.

Authentication

Represents the token for an authentication request or for an authenticated principal once the request has been processed by the AuthenticationManager.authenticate(Authentication) method.
Once the request has been authenticated, the Authentication will usually be stored in a thread-local SecurityContext managed by the SecurityContextHolder by the authentication mechanism which is being used. An explicit authentication can be achieved, without using one of Spring Security's authentication mechanisms, by creating an Authentication instance and using the code:

SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(anAuthentication);
SecurityContextHolder.setContext(context);

Note that unless the Authentication has the authenticated property set to true, it will still be authenticated by any security interceptor (for method or web invocations) which encounters it.
In most cases, the framework transparently takes care of managing the security context and authentication objects for you.
Author: Ben Alex

AuthenticationManager를 거쳐 인증된 principal이나 인증 요청에 대한 토큰을 나타내는 인터페이스이다. 요청이 인증되면, Authentication 객체는 SecurityContextHolder가 관리하는 스레드-로컬 SecurityContext에 저장된다.
스프링 시큐리티의 인증 매커니즘을 사용하지 않을 때는

SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(anAuthentication);
SecurityContextHolder.setContext(context);

를 사용하여 Authentication 인스턴스를 생성할 수 있다.
Authentication 객체는 authenticated 속성이 true로 설정되지 않으면 마주하는 보안 인터셉터들에게 인증과정을 계속 거치게 된다.

➡️ Spring Security에서는 디폴트로 세션을 사용하다보니 용어가 살짝 헷갈려, JWT를 사용해 Session Policy를 STATELESS로 설정하면 어떻게 되는지 찾아보았다. STATELESS라고 하더라도 SpringSecurityContext는 생성이 되고, Authentication 정보는 이 Context에 저장된다. 다만 HttpSession은 사용할 수 없고, 매 요청마다 헤더에 JWT가 첨부되어 서버로 날아온다. 서버는 이 JWT를 Context에 있는 JWT와 비교하여 응답을 내리게 된다.

UsernamePasswordAuthenticationToken

An org.springframework.security.core.Authentication implementation that is designed for simple presentation of a username and password.
The principal and credentials should be set with an Object that provides the respective property via its Object.toString() method. The simplest such Object to use is String.
Author: Ben Alex, Norbert Nowak

Authentication의 구현체로 usernamepassword를 가진다. principalcredentials 를 가지는데, 이들은 자료형이 Object로 설정되어 있어 이론적으로는 뭐든지 넣을 수 있다. 하지만 각 속성에 대한 toString() 메서드가 필요한데, 그런 가장 편리한 Object는 문자열이라 주로 문자열로 설정된다.

UsernamePasswordAuthenticationToken을 생성할 때마다 setAuthenticated라는 불리언을 함께 설정해주는데, 이 속성을 true로 설정하는 생성자는 AuthenticationManagerAuthenticationProvider만 호출할 수 있다.
unauthenticated, authenticated 메서드로 이 생성자 메서드들을 각각 호출할 수 있다.

AuthenticationManager

Processes an Authentication request.
Author: Ben Alex

Authentication 요청을 일렬의 AuthenticationProvider에 위임한다.

주요 메서드: Authenticate
Params: authentication – the authentication request object
Returns: a fully authenticated object including credentials
Throws: AuthenticationException – if authentication fails
Attempts to authenticate the passed Authentication object, returning a fully populated Authentication object (including granted authorities) if successful.

전달받은 Authentication 객체를 인증하려 시도하며, 성공적으로 인증되면 granted authorities를 포함한 Authentication 객체를 반환한다.

계정이 비활성화된 경우(disabled)에는 DisabledException, 잠긴(locked) 경우에는 LockedException, 인증 정보가 올바르지 않은 경우에는 BadCredentialsException이 발생한다.

ProviderManager

Iterates an Authentication request through a list of AuthenticationProviders.
AuthenticationProviders are usually tried in order until one provides a non-null response. A non-null response indicates the provider had authority to decide on the authentication request and no further providers are tried. If a subsequent provider successfully authenticates the request, the earlier authentication exception is disregarded and the successful authentication will be used. If no subsequent provider provides a non-null response, or a new AuthenticationException, the last AuthenticationException received will be used. If no provider returns a non-null response, or indicates it can even process an Authentication, the ProviderManager will throw a ProviderNotFoundException. A parent AuthenticationManager can also be set, and this will also be tried if none of the configured providers can perform the authentication. This is intended to support namespace configuration options though and is not a feature that should normally be required.
The exception to this process is when a provider throws an AccountStatusException, in which case no further providers in the list will be queried. Post-authentication, the credentials will be cleared from the returned Authentication object, if it implements the CredentialsContainer interface. This behaviour can be controlled by modifying the eraseCredentialsAfterAuthentication property.
Author: Ben Alex, Luke Taylor

ProviderManagerAuthenticationManager의 구현체로, 일렬의 AuthenticationProviders를 통해 Authentication 요청을 수행한다.
AuthenticationProvidersnon-null의 결과를 반환받을 때까지 순서대로 작동한다. non-null 결과가 반환되면 그 providerAuthentication 요청에 대한 결정권을 가지며 더이상 다른 provider이 인증을 시도하지 않게 할 수 있다. 만약 그 뒤에 오는 provider가 성공적으로 Authentication 객체를 인증한다면, 그 앞에서 발생했던 예외는 처리되지 않고 성공한 Authentication이 사용된다.
만약 뒤에 오는 provider들 중 그 어느것도 non-null 결과나 AuthenticationException을 발생시키지 않는다면, 가장 최근에 발생했던 AuthenticationException이 사용된다.
provider 중 그 어느것도 non-null 결과를 내놓지 못하고, Authentication 객체를 처리할 수 없을 경우에는 ProviderNotFoundException이 발생한다.

부모 AuthenticationManager가 세팅될 수도 있으며, 구성된 provider들이 인증을 처리하지 못할 경우에는 해당 AuthenticationManager도 인증을 시도해볼 수 있다. 이것은 네임스페이스 구성 옵션을 지원하기 위해서이고 일반적으로 요구되는 사항은 아니다.

providerAccountStatusException을 발생시킬 경우에는 더 이상 그 뒤의 provider들에게로 Authentication이 전달되지 않는다. 인증이 성공하고 난 후, 만약 Authentication 객체가 CredentialContainer 인터페이스를 구현한 경우 반환된 Authentication 객체는 credential들을 삭제한다. 이 작업을 변경하고 싶으면 eraseCredentialsAfterAuthentication 속성을 변경하면 된다.

AuthenticationProvider

Indicates a class can process a specific Authentication implementation.
Author: Ben Alex

해당 인터페이스를 구현한 클래스는 특정 Authentication을 처리할 수 있다.

주요 메서드 Authenticate
Performs authentication with the same contract as AuthenticationManager.authenticate(Authentication). May return null if the AuthenticationProvider is unable to support authentication of the passed Authentication object. In such a case, the next AuthenticationProvider that supports the presented Authentication class will be tried.

AuthenticationManagerauthenticate와 같은 작업을 수행한다. Authentication 요청을 받아 온전히 인증된 Authentication 객체를 반환한다. 전달받은 Authentication 객체를 인증할 수 없는 경우에는 null을 반환한다. 이 경우에는 바로 다음에 오는 AuthenticationProvider가 인증을 시도한다.


Deprecated

DaoAuthenticationProvider

An AuthenticationProvider implementation that retrieves user details from a UserDetailsService.
Author: Ben Alex, Rob Winch

AbstractUserDetailsAuthenticationProvider를 상속한 AuthenticationProvider의 구현체로, UserDetailsService에서 UserDetails를 받아온다.

주요 메서드: retrieveUser
username, UsernamePasswordAuthenticationToken authentication을 전달받고, AuthenticationException을 발생시킨다.

전달받은 username과 일치하는 UserDetailsUserDetailsService에서 loadUserByUsername으로 불러온다.

: createSuccessAuthentication
Object, Authentication, UserDetails를 전달받고 Authentication을 반환한다.

providerUserDetailsPasswordService가 등록되어있고, passwordpasswordEncoderencode 되어 있는지 확인한다. encode가 되어있지 않을 경우 true를 반환한다. 둘 다 true일 경우, authentication으로 전달받은 passwordencode하고, 이를 user의 비밀번호로 업데이트한다(update).
password가 이미 암호화 되어 있는 경우, update는 일어나지 않는다. update를 결정한 후에는 AbstractUserDetailsAuthenticationProvidercreateSuccessAuthentication가 호출되어 userprincipal, credentials, authorities를 가진 Authentication 객체를 반환한다.

➡️ 그러면 대체 비밀번호를 어디서 비교하나 봤더니 상속받은
AbstractUserDetailsAuthenticationProvider에 추상 메서드 additionalAuthenticationChecks가 있었다.
(그런데 이 메서드는 deprecated라고 나와있다.)

➡️➡️ GPT에게 물어봤더니, 5.X 버전부터는 additionalAuthenticationChecks 메서드나 DaoAuthenticationProvider 클래스 사용도 더 이상 권장하지 않는다고 한다. 이제는 직접 PasswordEncoder로 비밀번호를 검증하고, AuthenticationProvider를 사용해 사용자 정의 인증 로직을 구현하는 것을 권장한다.

0개의 댓글