When a user wants to access a protected resource in Spring Security, the user has some roles and the access to the resource requires some roles. The voting machine and voting mechanism are used when comparing the roles the user has with the roles the resource requires.

When a user wants to access a resource, the voter votes for or against the resource based on the user’s role, and the voting is based on the results of the voter.

In Spring Security, three voting mechanisms are provided by default, but it is possible to not use the voting mechanisms and voters provided by the system and define them completely by ourselves.

This article focuses on the three voting mechanisms and the default voter.

1. Voters

Let’s look at the voter first.

In Spring Security, the voter is regulated by the AccessDecisionVoter interface, let’s look at the implementation of the AccessDecisionVoter interface.

AccessDecisionVoter

As you can see, there are several implementations of the voter, and we can choose one or more of them, or we can customize the voter, and the default voter is WebExpressionVoter.

Let’s look at the definition of AccessDecisionVoter.

1
2
3
4
5
6
7
8
9
public interface AccessDecisionVoter<S> {
    int ACCESS_GRANTED = 1;
    int ACCESS_ABSTAIN = 0;
    int ACCESS_DENIED = -1;
    boolean supports(ConfigAttribute attribute);
    boolean supports(Class<?> clazz);
    int vote(Authentication authentication, S object,
            Collection<ConfigAttribute> attributes);
}

Let me explain a little.

  1. first of all, three constants are defined, and the meaning of each constant can be seen from its name. 1 means yes; 0 means abstain; -1 means reject.
  2. two supports methods are used to determine whether the voter supports the current request.
  3. vote is a specific voting method. It is implemented in different implementation classes. Three parameters, authentication indicates the current login subject; object is an ilterInvocation that encapsulates the current request; attributes indicates the set of roles required by the currently accessed interface.

Let’s look at the implementation of each of the voters.

1.1 RoleVoter

RoleVoter is mainly used to determine whether the current request has the role required by the interface, let’s look at its vote method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public int vote(Authentication authentication, Object object,
        Collection<ConfigAttribute> attributes) {
    if (authentication == null) {
        return ACCESS_DENIED;
    }
    int result = ACCESS_ABSTAIN;
    Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);
    for (ConfigAttribute attribute : attributes) {
        if (this.supports(attribute)) {
            result = ACCESS_DENIED;
            for (GrantedAuthority authority : authorities) {
                if (attribute.getAttribute().equals(authority.getAuthority())) {
                    return ACCESS_GRANTED;
                }
            }
        }
    }
    return result;
}

The logic of this method is very simple. If the current login subject is null, it returns ACCESS_DENIED to deny access; otherwise, it extracts the role information from the authentication of the current login subject and compares it with the attributes, and if it has any of the required roles in the attributes, it returns ACCESS_GRANTED to deny access. If any of the required roles in attributes is present, ACCESS_GRANTED is returned, indicating that access is allowed. For example, if the role in attributes is [a,b,c] and the current user has a, then access is allowed, and it is not necessary to have all three roles.

Another thing to note is the supports method of RoleVoter, let’s take a look.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class RoleVoter implements AccessDecisionVoter<Object> {
    private String rolePrefix = "ROLE_";
    public String getRolePrefix() {
        return rolePrefix;
    }
    public void setRolePrefix(String rolePrefix) {
        this.rolePrefix = rolePrefix;
    }
    public boolean supports(ConfigAttribute attribute) {
        if ((attribute.getAttribute() != null)
                && attribute.getAttribute().startsWith(getRolePrefix())) {
            return true;
        }
        else {
            return false;
        }
    }
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

As you can see, there is a rolePrefix prefix involved here, which is ROLE_. In the supports method, only if the subject role prefix is ROLE_, this supoorts method will return true and this voter will take effect.

1.2 RoleHierarchyVoter

RoleHierarchyVoter is a subclass of RoleVoter, which introduces role hierarchy management, or role inheritance, based on RoleVoter role judgment.

The vote method of the RoleHierarchyVoter class is the same as RoleVoter, the only difference is that the RoleHierarchyVoter class overrides the extractAuthorities method.

1
2
3
4
5
6
@Override
Collection<? extends GrantedAuthority> extractAuthorities(
        Authentication authentication) {
    return roleHierarchy.getReachableGrantedAuthorities(authentication
            .getAuthorities());
}

After the role hierarchy, you need to get the actual roles available through the getReachableGrantedAuthorities method

1.3 WebExpressionVoter

This is an expression-based authority control voter, which we won’t go into too much detail here (I’ll explain it in detail when I get a chance). Simply look at its vote method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public int vote(Authentication authentication, FilterInvocation fi,
        Collection<ConfigAttribute> attributes) {
    assert authentication != null;
    assert fi != null;
    assert attributes != null;
    WebExpressionConfigAttribute weca = findConfigAttribute(attributes);
    if (weca == null) {
        return ACCESS_ABSTAIN;
    }
    EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
            fi);
    ctx = weca.postProcess(ctx, fi);
    return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
            : ACCESS_DENIED;
}

If you are skilled in using SpEL, this code should be easy to understand

The code here actually builds a weca object based on the attributes passed in, then builds a ctx object based on the authentication parameters passed in, and finally calls the evaluateAsBoolean method to determine if the permissions match.

The three voters described above are the three we use more often in practical development.

1.4 Others

More cold voting machines

Jsr250Voter

Voters that handle Jsr-250 permissions annotations, such as @PermitAll, @DenyAll, etc.

AuthenticatedVoter

AuthenticatedVoter is used to determine whether the ConfigAttribute has IS_AUTHENTICATED_FULLY, IS_AUTHENTICATED_REMEMBERED, and IS_AUTHENTICATED_ANONYMOUSLY roles on it.

  • IS_AUTHENTICATED_FULLY means the current authenticated user must be authenticated by username/password, authentication by RememberMe is not valid.
  • IS_AUTHENTICATED_REMEMBERED means that the currently logged-in user must be authenticated by RememberMe.
  • IS_AUTHENTICATED_ANONYMOUSLY means that the currently logged-in user must be anonymous.

Consider this voter when a project introduces RememberMe and wants to distinguish between different authentication methods.

AbstractAclVoter

Provides helper methods for writing domain object ACL options that are not bound to any specific ACL system.

PreInvocationAuthorizationAdviceVoter

Permissions handled using the @PreFilter and @PreAuthorize annotations are authorized through PreInvocationAuthorizationAdvice.

Of course, if these voters do not meet the requirements, they can be customized.

2. Voting mechanism

A request doesn’t necessarily have only one voter, there may be multiple voters, so on top of the voters we need voting mechanism.

Voting mechanism

There are three main voting-related classes.

  • AffirmativeBased
  • ConsensusBased
  • UnanimousBased

Their inheritance relationship is shown in the figure above.

All three decision makers call through all the voters in the project, and the default decision maker used is AffirmativeBased.

The differences between the three decision makers are as follows.

  • AffirmativeBased: Passes if one voter agrees.
  • ConsensusBased: The request passes if a majority of the voters agree, in case of a tie, it depends on the value of the allowIfEqualGrantedDeniedDecisions parameter.
  • UnanimousBased: The request is passed if all voters agree.

The specific logic here is relatively simple, I will not analyze the source code, you can see for yourself if you are interested.

3. Where to configure?

When we use expression-based permission control, like the following.

1
2
3
4
http.authorizeRequests()
        .antMatchers("/admin/**").hasRole("admin")
        .antMatchers("/user/**").hasRole("user")
        .anyRequest().fullyAuthenticated()

Then the default voter and decision maker are configured in the AbstractInterceptUrlConfigurer#createDefaultAccessDecisionManager method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
private AccessDecisionManager createDefaultAccessDecisionManager(H http) {
    AffirmativeBased result = new AffirmativeBased(getDecisionVoters(http));
    return postProcess(result);
}
List<AccessDecisionVoter<?>> getDecisionVoters(H http) {
    List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
    WebExpressionVoter expressionVoter = new WebExpressionVoter();
    expressionVoter.setExpressionHandler(getExpressionHandler(http));
    decisionVoters.add(expressionVoter);
    return decisionVoters;
}

Here you can see the default decision maker and voter, and after the decision maker AffirmativeBased object is created, it also calls the postProcess method to register to the Spring container. If we want to modify the object, it is very easy to do so.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
http.authorizeRequests()
        .antMatchers("/admin/**").hasRole("admin")
        .antMatchers("/user/**").hasRole("user")
        .anyRequest().fullyAuthenticated()
        .withObjectPostProcessor(new ObjectPostProcessor<AffirmativeBased>() {
            @Override
            public <O extends AffirmativeBased> O postProcess(O object) {
                List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
                decisionVoters.add(new RoleHierarchyVoter(roleHierarchy()));
                AffirmativeBased affirmativeBased = new AffirmativeBased(decisionVoters);
                return (O) affirmativeBased;
            }
        })
        .and()
        .csrf()
        .disable();

Here is just a demo for you, normally we don’t need to modify it like this . When we use different permission configuration methods, there will be automatically configured corresponding voter and decision maker. Or we configure the voter and decision maker manually, if they are configured by the system, in most cases we don’t need to modify them.

Reference http://www.enmalvi.com/2021/06/24/springsecurity-accessdecisionvoter/