06 Permission Management How to Analyze the Authorization Principles of Spring Security

06 Permission Management How to Analyze the Authorization Principles of Spring Security #

In the previous lecture, we analyzed the authorization functionality provided by Spring Security. You may have noticed that using this functionality is simple, by just using a set of utility methods provided by the HttpSecurity object, you can implement access control in complex scenarios. However, features that are easy to use often have more complex internal implementations than they appear. Today, I will analyze the implementation mechanism behind the authorization functionality in depth. In the implementation process of the authorization functionality, Spring Security adopts many excellent design concepts and implementation techniques, which are worth studying in depth.

Overall Process of Spring Security Authorization #

Let’s briefly review the content of the previous lecture. We know that in Spring Security, the configuration method to control the access permissions of all requests only requires one line of code as shown below:

http.authorizeRequests();

We can understand the effect of this code execution in conjunction with the response process of an HTTP request. When an HTTP request arrives at the Servlet container, it will be intercepted by the container and some additional processing logic will be added. In the Servlet, this processing logic is implemented using filters. Multiple filters combined in a certain order form a filter chain. We will discuss filters in detail in Lecture 08 “Pipeline Filtering: How to Extend Security Based on Spring Security Filters?”. In this lecture, we only need to know that Spring Security also intercepts requests based on filters to implement access control.

In Spring Security, there is an interceptor called FilterSecurityInterceptor, which is located at the end of the entire filter chain. Its core function is to intercept the authorization process to determine whether a request can access the target HTTP endpoint. FilterSecurityInterceptor is the first link in the entire authorization process, and we call it “intercepting requests”.

After intercepting the request, the next step is to obtain the requested access resource and the permission information required to access these resources. We call this step “getting permission configuration”. In Spring Security, there is a SecurityMetadataSource interface, which serves as the data source for a series of security metadata and represents the abstraction of permission configuration. In the previous lecture, we have set up many permission information through the configuration method, such as:

http.authorizeRequests().anyRequest().hasAuthority("CREATE");  

Please note that the return value of the http.authorizeRequests() method is an ExpressionInterceptUrlRegistry, the return value of the anyRequest() method is an AuthorizedUrl, and the return value of the hasAuthority() method is again an ExpressionInterceptUrlRegistry. These objects will be introduced in today’s content.

The SecurityMetadataSource interface defines a set of methods to manipulate these permission configurations, and the specific form of permission configuration is the ConfigAttribute interface. Through ExpressionInterceptUrlRegistry and AuthorizedUrl, we can transform the configuration information into concrete ConfigAttributes.

After obtaining the permission configuration information, we can use these configurations to determine whether an HTTP request has access permission, that is, to execute the authorization decision. Spring Security specifically provides an AccessDecisionManager interface to complete this operation. In the AccessDecisionManager interface, it delegates the specific decision-making process to the AccessDecisionVoter interface. The AccessDecisionVoter can be considered as a type of voter responsible for voting on authorization decisions.

The above three steps constitute the overall workflow of Spring Security’s authorization process, which can be represented by the following sequence diagram:

Drawing 0.png

Overall workflow of Spring Security authorization

Next, we will explain the three steps of intercepting requests, obtaining permission configuration, and executing authorization decisions based on this class diagram.

Intercepting Requests #

As an interceptor, FilterSecurityInterceptor implements the request interception. Let’s take a look at its definition first:

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter

FilterSecurityInterceptor implements the Servlet Filter interface, so it is essentially a filter, and it implements the invoke method of the Filter interface. In its invoke method, FilterSecurityInterceptor itself does not perform any special operations, it only gets the HTTP request and calls the beforeInvocation() method of the base class AbstractSecurityInterceptor to intercept the request:

public void invoke(FilterInvocation fi) throws IOException, ServletException {

        

   …

   InterceptorStatusToken token = super.beforeInvocation(fi);

   …

   super.afterInvocation(token, null);       

}

The beforeInvocation() method in AbstractSecurityInterceptor is very long. After trimming it, we can get the following main process code:

protected InterceptorStatusToken beforeInvocation(Object object) {

        

	    …

	    // Get the collection of ConfigAttribute

        Collection< ConfigAttribute > attributes = this.obtainSecurityMetadataSource()

                 .getAttributes(object);

 

        …

        // Get authentication information

        Authentication authenticated = authenticateIfRequired();

 

        // Execute authorization

        try {

             this.accessDecisionManager.decide(authenticated, object, attributes);

        }

        catch (AccessDeniedException accessDeniedException) {

             …

        }

        …

}

As can be seen, in the above code, the ConfigAttribute corresponding to the current request is obtained from the configured SecurityMetadataSource, which represents the permission information. So, how is this SecurityMetadataSource obtained?

Obtaining Access Policies #

We noticed that in FilterSecurityInterceptor, there is a FilterInvocationSecurityMetadataSource variable defined, and it is injected through the setSecurityMetadataSource() method. Obviously, this variable is a type of SecurityMetadataSource.

MetadataSource #

By browsing the invocation relationship of FilterSecurityInterceptor, we found that the class is initialized in the AbstractInterceptUrlConfigurer class, as shown below:

private FilterSecurityInterceptor createFilterSecurityInterceptor(H http,

             FilterInvocationSecurityMetadataSource metadataSource,

             AuthenticationManager authenticationManager) throws Exception {

        FilterSecurityInterceptor securityInterceptor = new FilterSecurityInterceptor();

        securityInterceptor.setSecurityMetadataSource(metadataSource);

        securityInterceptor.setAccessDecisionManager(getAccessDecisionManager(http));

        securityInterceptor.setAuthenticationManager(authenticationManager);

        securityInterceptor.afterPropertiesSet();

        return securityInterceptor;

}

The creation of the FilterInvocationSecurityMetadataSource object is based on the abstract method provided by AbstractInterceptUrlConfigurer:

abstract FilterInvocationSecurityMetadataSource createMetadataSource(H http);

The implementation of this method is provided by the subclass ExpressionUrlAuthorizationConfigurer of AbstractInterceptUrlConfigurer, as shown below:

@Override
ExpressionBasedFilterInvocationSecurityMetadataSource createMetadataSource(H http) {

        LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = REGISTRY.createRequestMap();

        ...

        return new ExpressionBasedFilterInvocationSecurityMetadataSource(requestMap,
                 getExpressionHandler(http));

}

Please note: there is a REGISTRY object here, whose type is ExpressionInterceptUrlRegistry. This corresponds to the content introduced earlier. We have mentioned that the return type of the http.authorizeRequests() method is ExpressionInterceptUrlRegistry.

ExpressionInterceptUrlRegistry #

Let’s continue to look at the implementation of createRequestMap() in ExpressionInterceptUrlRegistry, as shown below:

final LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> createRequestMap() {

        ...

        LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<>();

        for (UrlMapping mapping : getUrlMappings()) {

             RequestMatcher matcher = mapping.getRequestMatcher();

             Collection<ConfigAttribute> configAttrs = mapping.getConfigAttrs();

             requestMap.put(matcher, configAttrs);

        }

        return requestMap;

}

This code converts the configured http.authorizeRequests() into UrlMappings, and then further converts them into mappings between RequestMatcher and Collection<ConfigAttribute>. So, where is the entry point to create these UrlMappings? It is also in the interceptUrl method of ExpressionUrlAuthorizationConfigurer, as shown below:

private void interceptUrl(Iterable<? extends RequestMatcher> requestMatchers,
           Collection<ConfigAttribute> configAttributes) {

        for (RequestMatcher requestMatcher : requestMatchers) {

           REGISTRY.addMapping(new AbstractConfigAttributeRequestMatcherRegistry.UrlMapping(
                   requestMatcher, configAttributes));

        }

}

AuthorizedUrl #

Let’s further trace the code flow and find that the entry point to the interceptUrl() method mentioned above is in the access() method shown below:

public ExpressionInterceptUrlRegistry access(String attribute) {

           if (not) {

               attribute = "!" + attribute;

           }

           interceptUrl(requestMatchers, SecurityConfig.createList(attribute));

           return ExpressionUrlAuthorizationConfigurer.this.REGISTRY;

}

Combining the content from the previous lesson, we can easily understand the purpose of this access() method. Please note that this method is located in the AuthorizedUrl class, and the return value of the http.authorizeRequests().anyRequest() method is an instance of this AuthorizedUrl class. In this class, a batch of familiar configuration methods are defined, such as hasRole, hasAuthority, etc., and these methods all internally call the access() method mentioned above:

public ExpressionInterceptUrlRegistry hasRole(String role) {

           return access(ExpressionUrlAuthorizationConfigurer.hasRole(role));

}


public ExpressionInterceptUrlRegistry hasAuthority(String authority) {

           return access(ExpressionUrlAuthorizationConfigurer.hasAuthority(authority));

}

Now that we have reached this point, the process of obtaining the access strategy is basically completed, and we have obtained a set of ConfigAttribute objects representing permissions.

Executing Authorization Decisions #

The prerequisite for executing authorization decisions is obtaining authentication information. Therefore, in the interception process of FilterSecurityInterceptor, we found the following line of code that performs authentication:

Authentication authenticated = authenticateIfRequired();

The authenticateIfRequired() method performs authentication, and its implementation is as follows:

private Authentication authenticateIfRequired() {
    
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    
            …
    
            authentication = authenticationManager.authenticate(authentication);
    
    	    …
    
            SecurityContextHolder.getContext().setAuthentication(authentication);
    
            return authentication;
    
}

As can be seen, the authentication logic is not complicated. First, it checks whether the Authentication object exists in the context object to determine if the current user has been authenticated. If the user has not been authenticated, it calls the AuthenticationManager to perform authentication and stores the Authentication in the context object. For a detailed introduction to the user authentication process, you can refer to the content of “Authentication System: How to Deeply Understand the Authentication Mechanism of Spring Security?”.

AccessDecisionManager #

AccessDecisionManager is the entry point for authorization decisions. Its most core method is the decide() method, which we have seen earlier in the execution process:

this.accessDecisionManager.decide(authenticated, object, attributes);

Similarly, when introducing the AbstractInterceptUrlConfigurer class, we also found the corresponding methods for obtaining and creating the AccessDecisionManager:

private AccessDecisionManager getAccessDecisionManager(H http) {
    
            if (accessDecisionManager == null) {
    
                 accessDecisionManager = createDefaultAccessDecisionManager(http);
    
            }
    
            return accessDecisionManager;
    
}
    
private AccessDecisionManager createDefaultAccessDecisionManager(H http) {
    
            AffirmativeBased result = new AffirmativeBased(getDecisionVoters(http));
    
            return postProcess(result);
    
}

Obviously, if a custom AccessDecisionManager is not set, a AffirmativeBased instance will be created by default. The decide() method of AffirmativeBased is as follows:

public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
    
            int deny = 0;
    
            for (AccessDecisionVoter voter : getDecisionVoters()) {
    
                 int result = voter.vote(authentication, object, configAttributes);
    
                 switch (result) {
    
               case AccessDecisionVoter.ACCESS_GRANTED:
    
                   return;
    
               case AccessDecisionVoter.ACCESS_DENIED:
    
                   deny++;
    
                   break;
    
               default:
    
                   break;
    
                 }
    
            }
    
            if (deny > 0) {
    
                 throw new AccessDeniedException(messages.getMessage(
    
                         "AbstractAccessDecisionManager.accessDenied", "Access is denied"));
    
            }
    
            checkAllowIfAllAbstainDecisions();
    
}

As can be seen, the process of calculating whether there is access permission is delegated to a group of AccessDecisionVoter objects. If any of them denies the access, an AccessDeniedException will be thrown.

AccessDecisionVoter #

AccessDecisionVoter is also an interface that provides the vote() method as follows:

int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);

Once again, we found the abstract method implementation of getDecisionVoters() in the AbstractInterceptUrlConfigurer class, as shown below:

abstract List<AccessDecisionVoter<?>> getDecisionVoters(H http);

In its subclass ExpressionUrlAuthorizationConfigurer, we found the specific implementation of this abstract method:

@Override
    
List<AccessDecisionVoter<?>> getDecisionVoters(H http) {
    
            List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
    
            WebExpressionVoter expressionVoter = new WebExpressionVoter();
    
           expressionVoter.setExpressionHandler(getExpressionHandler(http));
    
            decisionVoters.add(expressionVoter);
    
            return decisionVoters;
    
}

As can be seen, the AccessDecisionVoter created here is actually WebExpressionVoter. Its vote() method is as follows:

public int vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes) {
    
            
    
            WebExpressionConfigAttribute weca = findConfigAttribute(attributes);
    
            
    
            EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, fi);
    
            ctx = weca.postProcess(ctx, fi);
    
            return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED: ACCESS_DENIED;
    
}

Here, we encounter a SecurityExpressionHandler, which is related to the expression language in Spring. It constructs an EvaluationContext object used for evaluation. The ExpressionUtils.evaluateAsBoolean() method evaluates the final result based on the authorization expression obtained from WebExpressionConfigAttribute and the EvaluationContext object:

public static boolean evaluateAsBoolean(Expression expr, EvaluationContext ctx) {
    
            try {
    
                 return expr.getValue(ctx, Boolean.class);
    
            }
    
            catch (EvaluationException e) {
    
                 
    
            }
    
}

Obviously, the final evaluation process simply uses the SpEL expression language provided by Spring.

To summarize, we have organized the core components involved in this process in the form of a class diagram, as shown in the following figure:

Drawing 1.png

Class Diagram of Spring Security Authorization

Summary and Preview #

In this tutorial, we focused on the implementation principle of the Spring Security authorization mechanism. We divided the entire authorization process into three major steps: intercepting requests, obtaining access strategies, and executing authorization decisions. For each step, a set of core classes and their interaction patterns are involved. The explanation of these core classes is based on the basic configuration methods introduced in the previous tutorial to ensure the connection between practical applications and source code analysis.

The summary of this tutorial is as follows:

Drawing 2.png

Finally, we have a question for you to think about: Can you briefly describe the execution process of the entire authorization mechanism in Spring Security?