Spring Security - Authentication

2024. 7. 7. 14:39스프링 (Spring)/스프링 보안 (Spring Security)

본 장은 아래 문서를 참고해서 작성했습니다.

 

Authentication :: Spring Security

Spring Security provides comprehensive support for Authentication. We start by discussing the overall Servlet Authentication Architecture. As you might expect, this section is more abstract describing the architecture without much discussion on how it appl

docs.spring.io

 

 


Authentication

 

이전 Spring Security - 개요 글에서 인증(authentication)에 대해 설명했었습니다.

사용자가 누구이고, 우리 서버에 접근해도 되는 지 확인하는 것이죠.

 

필자는 언제나 먼저 직접 해보는 것을 좋아하기에, Spring Security에서 제공한 예시를 따라해보겠습니다.

(출처 : Spring Security - SAMPLES)

 

spring-security-samples/servlet/spring-boot/kotlin/hello-security at main · spring-projects/spring-security-samples

Contribute to spring-projects/spring-security-samples development by creating an account on GitHub.

github.com

 

가장 먼저 보아야 할 것은 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

 

Spring Security / Servlet Applications / Authentication / Authentication Architecture - SecurityContextHolder Architecture

 

 

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에 대한 전반적인 설명이 있습니다.

궁금하시면 아래 문서를 방문하셔도 좋습니다.

 

Anonymous Authentication :: Spring Security

It is generally considered good security practice to adopt a “deny-by-default” stance, where you explicitly specify what is allowed and disallow everything else. Defining what is accessible to unauthenticated users is a similar situation, particularly

docs.spring.io