2024. 7. 7. 14:39ㆍ스프링 (Spring)/스프링 보안 (Spring Security)
본 장은 아래 문서를 참고해서 작성했습니다.
Authentication
이전 Spring Security - 개요 글에서 인증(authentication)에 대해 설명했었습니다.
사용자가 누구이고, 우리 서버에 접근해도 되는 지 확인하는 것이죠.
필자는 언제나 먼저 직접 해보는 것을 좋아하기에, Spring Security에서 제공한 예시를 따라해보겠습니다.
(출처 : Spring Security - SAMPLES)
가장 먼저 보아야 할 것은 SecurityConfig.kt 입니다.
package org.springframework.security.samples.config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.security.web.SecurityFilterChain
/**
* @author Eleftheria Stein
*/
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize("/css/**", permitAll)
authorize("/user/**", hasAuthority("ROLE_USER"))
}
formLogin {
loginPage = "/log-in"
}
}
return http.build()
}
@Bean
fun userDetailsService(): UserDetailsService {
val userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
return InMemoryUserDetailsManager(userDetails)
}
}
당연하게도 처음 보는 사용자가 누구인지 알려면, 우리 쪽에서 미리 어떤 사용자들이 있는 지 알아야 합니다.
userDetailsService() 함수를 보면 User를 만들어 제공해주고 있는 걸 확인할 수 있죠.
- 이름 : user
- 비밀번호 : password
- 역할 : USER
그리고 어떤 자원에 인증이 필요한 지 설정해야 합니다.
filterChain() 함수에 있는 authorizeRequests() 함수를 보면 "/css/**", "/user/**"에 대해서 설정되어 있는 것을 확인할 수 있죠.
다만 인가(authorization)에 관련된 부분은 이 글의 범주를 넘으므로, hasAuthority() 대신 authenticated를 사용하겠습니다.
...
class SecurityConfig {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize("/css/**", permitAll)
authorize("/user/**", authenticated)
}
formLogin {
loginPage = "/log-in"
}
}
return http.build()
}
...
이제 실행해서 로그인을 진행해봅니다.
만일 잘못된 이름이나 비밀번호를 입력한다면, 다음 페이지로 넘어가지 않을 것입니다.
그렇다면 과연 내부적으로 어떻게 인증을 처리할까요?
Authentication
Authentication 인터페이스는 사용자 인증 정보를 제공하는 함수가 있습니다.
위 아키텍처에서 볼 수 있듯, 주요하게는 Principal, Credentials, Authorities 3개를 가지고 있죠.
package org.springframework.security.core;
...
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
실제 인터페이스를 열어보면 getDetails() 함수와 isAuthenticated, setAuthenticated() 함수가 더 있긴 합니다.
- getDetails()는 ip 주소나 인증서 일련 번호(certificate serial number) 등 요청에 대한 추가 정보입니다.
- isAuthenticated는 인증 여부를 판단합니다.
Authorities
단순히 권한 정보입니다.
(자세한 내용은 이 글의 범위를 넘으므로 다른 글에서 다루도록 하겠습니다.)
Principal
특정 사용자 정보입니다. 대부분 UserDetails 인터페이스를 구현한 객체를 제공합니다.
해당 인터페이스를 보면 이름과 비밀번호, 계정 만료 여부 등을 확인할 수 있죠.
package org.springframework.security.core.userdetails;
...
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
재밌는 점은 Authentiction과 UserDetails 모두 getAuthorities를 가지고 있는데요.
principal이 UserDetails라면 크게 다를 건 없습니다.
Collection<? extends GrantedAuthority> getAuthorities();
Returns: the authorities granted to the principal,
or an empty collection if the token has not been authenticated. Never null.
Credentials
말 그대로 사용자를 증명할 정보입니다.
주로 비밀번호(password)가 이에 해당합니다.
AuthenticationManager를 구현한 ProviderManager를 보면
사용자가 제공한 authentication 정보를 가지고 인증을 수행하는 코드가 있습니다.
package org.springframework.security.authentication;
...
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
for (AuthenticationProvider provider : getProviders()) {
...
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
...
username / password 인증의 경우
AbstractUserDetailsAuthenticationProvider 추상 클래스에서 authenticate() 함수의 구현부를 볼 수 있는데
package org.springframework.security.authentication.dao;
...
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
try {
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
preAuthenticationChecks, postAuthenticationChecks 와 더불어 additionalAuthenticationChecks 를 볼 수 있습니다.
마지막으로 additionalAuthenticationChecks는 DaoAuthenticationProvider에서 구현했는데,
package org.springframework.security.authentication.dao;
...
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Override
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
사용자가 제공한 authentication의 비밀번호와 서버가 가지고 있는 userDetails의 비밀번호를 비교하는 걸 알 수 있죠.
사실 여기서 AuthenticationManager, ProviderManager, AuthenticationProvider의 역할을 모두 볼 수 있는데,
credentials을 비교하거나 만료 여부를 확인하는 등 실질적인 비교는 AuthenticationProvider가 수행합니다.
AuthenticationManager는 인증을 진행하고, 결과를 만들고, 검증에 사용된 정보를 지우는 등의 전체적인 역할을 합니다.
대표적으로 이를 구현한 ProviderManager에서 authenticate() 함수의 로직을 보면 다음과 같은 내용이 있습니다.
- AuthenticationProviders들을 순회하며 인증 함수를 호출하기
- 인증이 끝나면 credential 청소하기
- 실패 시 적절한 오류를 던져주기
- 성공 시 결과(Authentication)를 반환해주기
- ...
이제 실제로 Authentication 정보를 보기 위해
MainController에 showAuthentication() 함수를 추가하고, 각 api에서 호출해봅니다.
@Controller
class MainController {
companion object{
private fun showAuthentication() {
val authentication: Authentication? = SecurityContextHolder.getContext().authentication
if (authentication != null) {
println("*".repeat(30))
println(authentication.principal)
println(authentication.credentials)
println(authentication.authorities)
println(authentication.details)
println(authentication.isAuthenticated)
} else {
println("-".repeat(30))
println("there is no authentication")
}
}
}
@GetMapping("/")
fun index(): String {
showAuthentication()
return "index"
}
@GetMapping("/user/index")
fun userIndex(): String {
showAuthentication()
return "user/index"
}
@GetMapping("/log-in")
fun login(): String {
showAuthentication()
return "login"
}
}
홈화면이나 로그인화면에서는 anonymous가 출력됩니다.
anonymousUser
[ROLE_ANONYMOUS]
WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=?]
true
로그인에 성공하면, 다음과 같이 인증된 사용자 정보가 출력됩니다.
org.springframework.security.core.userdetails.User [Username=user, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]]
null
[ROLE_USER]
WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=?]
true
SecurityContext
MainController 예제에서 살짝 엿볼 수 있었는데,
Authentication은 SecurityContext에서 얻을 수 있습니다.
package org.springframework.security.core.context;
import java.io.Serializable;
import org.springframework.security.core.Authentication;
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
그리고 SecurityContext는 SecurityContextHolder에서 얻을 수 있고요.
package org.springframework.security.core.context;
...
public class SecurityContextHolder {
...
private static SecurityContextHolderStrategy strategy;
...
public static SecurityContext getContext() {
return strategy.getContext();
}
...
그러나 바로 직접 접근해서 제공하는 것이 아닌, SecurityContextHolderStrategy를 통해서 제공합니다.
보통 이를 구현한 건 ThreadLocalSecurityContextHolderStrategy 클래스인데,
그 이름에서 알 수 있듯 ThreadLocal로 SecurityContext를 제공하기 때문입니다.
package org.springframework.security.core.context;
import java.util.function.Supplier;
import org.springframework.util.Assert;
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static final ThreadLocal<Supplier<SecurityContext>> contextHolder = new ThreadLocal();
...
public SecurityContext getContext() {
return (SecurityContext)this.getDeferredContext().get();
}
public Supplier<SecurityContext> getDeferredContext() {
Supplier<SecurityContext> result = (Supplier)contextHolder.get();
if (result == null) {
SecurityContext context = this.createEmptyContext();
result = () -> {
return context;
};
contextHolder.set(result);
}
return result;
}
...
그 이유는 SecurityContextHolder에서 context 변수가 정적(static)으로 선언되어 있는 걸로 알 수 있는데,
아시다시피 스프링에서 1개 요청에 대해 1개의 스레드를 만들어서 사용합니다.
정적 변수는 모든 스레드가 공유하므로, 결국 동시적으로 오는 요청들에 대한 인증 정보가 공유되겠죠.
하지만 ThreadLocal를 사용하면 각 스레드가 독립적으로 context를 가지게 됩니다.
결론적으로는 각 스레드는 자신만의 SecurityContext를 가지므로, 인증 정보가 공유되지 않을 것입니다.
AuthenticationToken
이번엔 MainController의 showAuthentication() 함수를 다음과 같이 수정해서 실행해봅니다.
@Controller
class MainController {
companion object{
private fun showAuthentication() {
val authentication: Authentication? = SecurityContextHolder.getContext().authentication
if (authentication != null) {
println("*".repeat(30))
println(authentication.javaClass)
// println(authentication.principal)
// println(authentication.credentials)
// println(authentication.authorities)
// println(authentication.details)
// println(authentication.isAuthenticated)
} else {
println("-".repeat(30))
println("there is no authentication")
}
}
}
...
출력되는 결과물은 다음과 같습니다.
...
******************************
class org.springframework.security.authentication.AnonymousAuthenticationToken
...
******************************
class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
...
눈치채셨을 수도 있겠지만, Authentication를 구현한 것이 AuthenticationToken 입니다.
AbstractAuthenticationToken을 구현한 토큰 클래스들을 열어보면 각 특성에 맞게 구현된 것을 알 수 있는데,
한 번 AnonymousAuthenticationToken을 열어보면,
package org.springframework.security.authentication;
...
public class AnonymousAuthenticationToken extends AbstractAuthenticationToken implements Serializable {
private static final long serialVersionUID = 1L;
private final Object principal;
private final int keyHash;
public AnonymousAuthenticationToken(String key, Object principal, Collection<? extends GrantedAuthority> authorities) {
this(extractKeyHash(key), principal, authorities);
}
private AnonymousAuthenticationToken(Integer keyHash, Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
Assert.isTrue(principal != null && !"".equals(principal), "principal cannot be null or empty");
Assert.notEmpty(authorities, "authorities cannot be null or empty");
this.keyHash = keyHash;
this.principal = principal;
this.setAuthenticated(true);
}
private static Integer extractKeyHash(String key) {
Assert.hasLength(key, "key cannot be empty or null");
return key.hashCode();
}
...
public Object getCredentials() {
return "";
}
public int getKeyHash() {
return this.keyHash;
}
public Object getPrincipal() {
return this.principal;
}
}
그 이름답게 Credentials은 빈 값을 주네요.
그리고 생성자 부분을 보면 몇 가지 신기한 것들이 보입니다.
먼저 principal과 authorities에 내용이 있어야 한다고 합니다.
이는 기본적으로 "anonymousUser", ["ROLE_ANONYMOUS"]를 가지도록 하고 있습니다.
두 번째는 this.setAuthenticated(true) 이 부분인데,
사실 이전 Authentication을 설명했을 당시 예제에서
마지막 인증 여부를 나타내는 isAuthenticated를 출력했을 때 true가 나오는 조금 불편한 사실이 있었죠.
필자도 헷갈린 부분이었는데, 정리해보면 authenticated 된 사용자만 "/user/**"에 접근할 수 있고
class SecurityConfig ...
authorize("/user/**", authenticated)
AnonymousAuthenticationToken은 isAuthenticated가 true이므로 접근할 수 있겠구나! 하는 부분입니다.
public class AnonymousAuthenticationToken ...
this.setAuthenticated(true);
그러나 여전히 anonymous는 "/user/**"에 접근하지 못했죠...
그 이유를 천천히 살펴봅시다.
우선 Kotlin Dsl에서 벗어나 자바스럽게 코드를 바꿔보겠습니다.
이때 authorizeRequest 대신 authorizeHttpRequest로 변경하겠습니다.
(그 이유가 궁금하다면...
첫 번째 이유는 authorizeRequest는 사실 deprecated 되었습니다.
사실 두 번째 이유가 가장 주요한데, 구현 코드가 장난아니게 복잡합니다...
그래도 궁금하다면 대략적인 흐름만 좀 알려드리겠습니다.
[1]
먼저 authenticated() 함수가 호출되면, ExpressionUrlAuthorizationConfigurer의
REGISTRY(ExpressionInterceptUrlRegistry) 멤버 변수에 다음과 같은 맵핑이 추가됩니다.
하나는 "/user/**"와 같은 path를 비교할 RequestMatcher이고,
다른 하나는 "authenticated"를 담고 있는 SecurityConfig의 컬렉션입니다.
[2]
REGISTRY에 저장된 내용을 사용할 때 ExpressionBasedFilterInvocationSecurityMetadataSource로 만듭니다.
어려운 건 아니고, ExpressionInterceptUrlRegistry는 createRequestMap() 함수를 통해
일전의 RequestMatcher - Collection<SecurityConfig> 형식의 LinkedMap을 만든 것 하나와,
"authenticated"와 같은 표현을 다룰 핸들러(SecurityExpressionHandler) 하나로 만듭니다.
이때 핸들러의 멤버로 권한을 검증해줄 AuthenticationTrustResolver가 존재합니다.
[3]
ExpressionBasedFilterInvocationSecurityMetadataSource의 생성자에서는
Expression과 EvaluationContextPostProcessor<FilterInvocation>를 가지는
WebExpressionConfigAttribute들로 만듭니다. (ConfigAttribute 인터페이스를 구현한 것입니다)
전자는 "authenticated"와 같은 문자열을 Expression으로 파싱한 결과이고,
후자는 요청이 들어오면 "/user/**"와 매칭되는 지 확인해주는 RequestVariablesExtractorEvaluationContextPostProcessor라는 점에서 별 다를 건 없습니다.
이 모든 내용은 ExpressionBasedFilterInvocationSecurityMetadataSource의
부모 클래스인 DefaultFilterInvocationSecurityMetadataSource가 가지고 있는 requestMap에 저장됩니다.
[4]
requestMap에서 ConfigAttribute 컬렉션을 가져오는 getAttributes() 함수의 호출부를 보면
AbstractSecurityInterceptor 클래스의 beforeInvocation() 함수가 등장합니다.
여기서 인증을 시도하는 attemptAuthorization() 함수를 호출하는데, 여기서 아래 코드가 존재합니다.
this.accessDecisionManager.decide(authenticated, object, attributes);
AccessDecisionManager의 구현 클래스로는 AffirmativeBased, ConsensusBased, UnanimousBased가 있는데
모두 AccessDecisionVoter의 vote의 결과를 사용하고 있습니다.
int result = voter.vote(...);
[5]
우리는 "authenticated"라는 Expression을 사용했었으므로,
이에 해당하는 AccessDecisionVoter의 구현 클래스는 WebExpressionVoter입니다.
public class WebExpressionVoter implements AccessDecisionVoter<FilterInvocation> {
private final Log logger = LogFactory.getLog(getClass());
private SecurityExpressionHandler<FilterInvocation> expressionHandler = new DefaultWebSecurityExpressionHandler();
@Override
public int vote(Authentication authentication, FilterInvocation filterInvocation,
Collection<ConfigAttribute> attributes) {
Assert.notNull(authentication, "authentication must not be null");
Assert.notNull(filterInvocation, "filterInvocation must not be null");
Assert.notNull(attributes, "attributes must not be null");
WebExpressionConfigAttribute webExpressionConfigAttribute = findConfigAttribute(attributes);
if (webExpressionConfigAttribute == null) {
this.logger
.trace("Abstained since did not find a config attribute of instance WebExpressionConfigAttribute");
return ACCESS_ABSTAIN;
}
EvaluationContext ctx = webExpressionConfigAttribute.postProcess(
this.expressionHandler.createEvaluationContext(authentication, filterInvocation), filterInvocation);
boolean granted = ExpressionUtils.evaluateAsBoolean(webExpressionConfigAttribute.getAuthorizeExpression(), ctx);
if (granted) {
return ACCESS_GRANTED;
}
this.logger.trace("Voted to deny authorization");
return ACCESS_DENIED;
}
...
주요하게 볼 코드는 아래와 같습니다.
EvaluationContext ctx = webExpressionConfigAttribute.postProcess(
this.expressionHandler.createEvaluationContext(authentication, filterInvocation), filterInvocation);
boolean granted = ExpressionUtils.evaluateAsBoolean(webExpressionConfigAttribute.getAuthorizeExpression(), ctx);
filterInvocation이 가지고 있는 Filter가 동작하도록 EvaluationContext를 만들고 evaluate 결과를 받습니다.
이때 AuthenticationTrustResolver 중 isAuthenticated() 함수를 호출해줍니다.
public interface AuthenticationTrustResolver {
...
default boolean isAuthenticated(Authentication authentication) {
return authentication != null && authentication.isAuthenticated() && !isAnonymous(authentication);
}
...
조건식을 잘 보면 !isAnonymous(authentication) 가 AND 조건으로 되어있죠.
이 구현은 대부분 클래스가 AnonymousAuthenticationToken 인지 확인하는 게 전부입니다.
public class AuthenticationTrustResolverImpl implements AuthenticationTrustResolver {
...
@Override
public boolean isAnonymous(Authentication authentication) {
if ((this.anonymousClass == null) || (authentication == null)) {
return false;
}
return this.anonymousClass.isAssignableFrom(authentication.getClass());
}
...
어휴...
)
// from kotlin
class SecurityConfig {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize("/css/**", permitAll)
authorize("/user/**", authenticated)
}
...
// to java
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) {
http
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/css/**").permitAll()
.requestMatchers("/user/**").authenticated()
)
...
저 authenticated() 함수는 AuthorizeHttpRequestsConfigurer.AuthorizedUrl 클래스에 있습니다.
package org.springframework.security.config.annotation.web.configurers;
...
public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<AuthorizeHttpRequestsConfigurer<H>, H> {
...
public class AuthorizedUrl {
...
public AuthorizationManagerRequestMatcherRegistry authenticated() {
return access(AuthenticatedAuthorizationManager.authenticated());
}
...
public AuthorizationManagerRequestMatcherRegistry access(
AuthorizationManager<RequestAuthorizationContext> manager) {
Assert.notNull(manager, "manager cannot be null");
return (this.not)
? AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, AuthorizationManagers.not(manager))
: AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager);
}
...
addMapping은 굳이 구현부를 보지 않더라도 requestMatchers("/css/**") 처럼 path를 매칭하는 것과
AuthenticatedAuthorizationManager.authenticated()의 리턴값을 맵핑하는 걸로 이해할 수 있겠죠.
package org.springframework.security.authorization;
...
public final class AuthenticatedAuthorizationManager<T> implements AuthorizationManager<T> {
public AuthenticatedAuthorizationManager() {
this(new AuthenticatedAuthorizationStrategy());
}
public static <T> AuthenticatedAuthorizationManager<T> authenticated() {
return new AuthenticatedAuthorizationManager<>();
}
...
private static class AuthenticatedAuthorizationStrategy extends AbstractAuthorizationStrategy {
@Override
boolean isGranted(Authentication authentication) {
return this.trustResolver.isAuthenticated(authentication);
}
}
그리고 AuthenticatedAuthorizationManager.authenticated()은 AuthenticatedAuthorizationManager 객체를 반환합니다.
이때 기본 AuthenticatedAuthorizationStrategy() 객체를 또 가져옴을 알 수 있는데,
여기서 trustResolver를 발견할 수 있습니다.
뭐, 그냥 기본값인 AuthenticationTrustResolverImpl 객체를 사용합니다.
private abstract static class AbstractAuthorizationStrategy {
AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
private void setTrustResolver(AuthenticationTrustResolver trustResolver) {
Assert.notNull(trustResolver, "trustResolver cannot be null");
this.trustResolver = trustResolver;
}
abstract boolean isGranted(Authentication authentication);
}
열어보면 isAnonymous() 함수가 있는데, 단순히 인자가 AnonymousAuthenticationToken 인지 확인합니다.
public class AuthenticationTrustResolverImpl implements AuthenticationTrustResolver {
private Class<? extends Authentication> anonymousClass = AnonymousAuthenticationToken.class;
...
Class<? extends Authentication> getAnonymousClass() {
return this.anonymousClass;
}
...
@Override
public boolean isAnonymous(Authentication authentication) {
if ((this.anonymousClass == null) || (authentication == null)) {
return false;
}
return this.anonymousClass.isAssignableFrom(authentication.getClass());
}
...
그리고 마지막으로 가장 중요한 AuthenticationTrustResolver 인터페이스를 열어보면
package org.springframework.security.authentication;
import org.springframework.security.core.Authentication;
public interface AuthenticationTrustResolver {
...
default boolean isAuthenticated(Authentication authentication) {
return authentication != null && authentication.isAuthenticated() && !isAnonymous(authentication);
}
...
isAuthenticated() 함수의 조건문의 맨 마지막 !isAnonymous(authentication)을 볼 수 있습니다.
따라서 AnonymousAuthenticationToken의 isAuthenticated가 true 인 것과 별개로,
인증되지 않은 사용자가 되게끔 만듭니다.
필자는 여전히 isAuthenticated가 true 인 것이 마음에 걸리긴 하는데,
Spring Security 문서는 나름 이렇게 만들었을 때의 장점과 Anonymous Authentication에 대한 전반적인 설명이 있습니다.
궁금하시면 아래 문서를 방문하셔도 좋습니다.
'스프링 (Spring) > 스프링 보안 (Spring Security)' 카테고리의 다른 글
Spring Security - Persistance (0) | 2024.07.13 |
---|---|
Spring Security - Servlet이란? (0) | 2024.07.06 |
Spring Security - 개요 (0) | 2024.07.04 |