Analysis of spring security dynamic authentication process

Time:2021-1-12

If we can’t talk about love, we can feel sorry for ourselves.

Wedge

We talked about it in the last articleSpringSecurityI believe that after you have read it carefully, you will have a good understanding of itSpringSecurityThe authentication process has been understood for seven or eight points. This issue is a dynamic authentication chapter that we have come to as promised. You don’t have to understand the knowledge of the first chapter to read this one, because the key points are different. You can take these two chapters as two separate chapters and extract the parts you need.

I wish you a good harvest.

This article is from my nuggets, so some of the article links point to nuggets, but I can also find the corresponding article in my mind.

Article code:Code cloud address   GitHub address

1. Authentication principle of spring security

In the last article, when we talked about authentication, we put a picture, which is as follows:

Analysis of spring security dynamic authentication process

In fact, the whole authentication process has always been around the green part of the filter chain in the figure, while the dynamic authentication we are going to talk about today mainly focuses on the orange part, which is the part marked on the figure:FilterSecurityInterceptor

1. FilterSecurityInterceptor

To know how to dynamically authenticate, we need to understand the authentication logic of spring securityFilterSecurityInterceptorIt is the last link of the filtering chain, and authentication is followed by authenticationFilterSecurityInterceptorIt is mainly responsible for authentication.

A request arrives after it has been authenticated and no exception has been thrownFilterSecurityInterceptorResponsible for the authentication part, that is to say, the entrance of authentication is right hereFilterSecurityInterceptor

Let’s take a look firstFilterSecurityInterceptorThe definition and main methods of this paper are as follows

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
        Filter {

            public void doFilter(ServletRequest request, ServletResponse response,
                    FilterChain chain) throws IOException, ServletException {
                FilterInvocation fi = new FilterInvocation(request, response, chain);
                invoke(fi);
            }
}

As can be seen from the code aboveFilterSecurityInterceptorIt implements the abstract classAbstractSecurityInterceptorThis is an implementation class ofAbstractSecurityInterceptorA very important piece of code has been written in advance in.

FilterSecurityInterceptorThe main method to solve this problem isdoFilterMethods, filter features, we should all know, after the request will be executeddoFiltermethod,FilterSecurityInterceptorOfdoFilterThe method is surprisingly simple, with only two lines in total:

first lineYes, we created oneFilterInvocationObject, thisFilterInvocationObject can be regarded as it encapsulates the request. Its main job is to take the information in the request, such as the URI of the request.

The second lineIt calls its owninvokeMethods, andFilterInvocationObject.

So our main logic must be in thisinvokeThere is a method in it. Let’s open it

public void invoke(FilterInvocation fi) throws IOException, ServletException {
        if ((fi.getRequest() != null)
                && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
                && observeOncePerRequest) {
            // filter already applied to this request and user wants us to observe
            // once-per-request handling, so don't re-do security checking
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        }
        else {
            // first time this request being called, so perform security checking
            if (fi.getRequest() != null && observeOncePerRequest) {
                fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
            }

            //Access authentication
            InterceptorStatusToken token = super.beforeInvocation(fi);

            try {
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            }
            finally {
                super.finallyInvocation(token);
            }

            super.afterInvocation(token, null);
        }
    }

invokeThere is only one methodif-else, generally do not meet the three conditions in if, and then the execution logic will arriveelse

elseThe code can also be summarized into two parts

  1. Calledsuper.beforeInvocation(fi)
  2. After the call, the filter continues to go down.

The second step is not necessary. Each filter has such a step, so we mainly look at itsuper.beforeInvocation(fi)As I said before,
FilterSecurityInterceptorThe abstract class is implementedAbstractSecurityInterceptor
So in this onesuperActually, it meansAbstractSecurityInterceptor
So this code actually callsAbstractSecurityInterceptor.beforeInvocation(fi)
I said earlierAbstractSecurityInterceptorThere is a very important piece of code in,
Let’s continue to look at thisbeforeInvocation(fi)Method source code:

protected InterceptorStatusToken beforeInvocation(Object object) {
        Assert.notNull(object, "Object was null");
        final boolean debug = logger.isDebugEnabled();

        if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
            throw new IllegalArgumentException(
                    "Security invocation attempted for object "
                            + object.getClass().getName()
                            + " but AbstractSecurityInterceptor only configured to support secure objects of type: "
                            + getSecureObjectClass());
        }

        Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
                .getAttributes(object);

        Authentication authenticated = authenticateIfRequired();

        try {
            //Interface to be called for authentication
            this.accessDecisionManager.decide(authenticated, object, attributes);
        }
        catch (AccessDeniedException accessDeniedException) {
            publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
                    accessDeniedException));

            throw accessDeniedException;
        }

    }

The source code is long. Here I simplify the middle part. The code can be roughly divided into three steps

  1. Got oneCollection<ConfigAttribute>Object, which is aListIn fact, it is the filtering rules that we configure in the configuration file.
  2. Got itAuthenticationHere is the callauthenticateIfRequiredI got the method. In fact, it’s still throughSecurityContextHolderI got it. I talked about how to get it in the last article.
  3. CalledaccessDecisionManager.decide(authenticated, object, attributes)The first two steps are rightdecideMethod to prepare parameters. The third step is to formally go to the authentication logic. Since this is the real authentication logic, that is to say, authentication is in factaccessDecisionManagerI’m doing it.

2. AccessDecisionManager

We can see the real processor of authentication through the source codeAccessDecisionManagerDon’t you think that layer by layer, just like dolls, don’t worry, there’s more below. Let’s take a look at the definition of the source interface

public interface AccessDecisionManager {

    //Main authentication methods
    void decide(Authentication authentication, Object object,
                Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
            InsufficientAuthenticationException;

    boolean supports(ConfigAttribute attribute);

    boolean supports(Class<?> clazz);
}

AccessDecisionManagerIs an interface, which declares three methods. In addition to the first authentication method, there are two auxiliary methods, all of which are used for screeningdecideThe validity of the parameters in the method.

Now that it’s an interface, what we called above must be its implementation class. Let’s take a look at the structure tree of this interface

Analysis of spring security dynamic authentication process

From the figure, we can see that it mainly has three implementation classes, which represent three different authentication logics

  • Confirmed based: one vote is passed, as long as one vote is passed, it is passed by default.
  • Unanimousbased: one vote against, as long as there is one vote against, it cannot be passed.
  • Consensus based: the minority is subordinate to the majority.

Why do we use tickets for the expression here? Because in the implementation class, a delegation is used to delegate the request to the voter, and each voter takes the request and calculates whether it can pass according to its own logic, and then votes, so there will be the above statement.

That is to say, these three implementation classes are not really the ones to judge whether the request can pass or not. It is the voter who really judges whether the request can pass or not. Then the implementation class combines the results of the voter to decide whether the request can pass or not.

As I have just said, the implementation class synthesizes the results of the voter to make decisions. That is to say, the voter can be put into multiple classes. The number of voters in each implementation class depends on how many voters are put in during construction. Let’s take a look at the defaultAffirmativeBasedSource code of.

public class AffirmativeBased extends AbstractAccessDecisionManager {

    public AffirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {
        super(decisionVoters);
    }

    //Get all the voting machines, loop through and vote
    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);

            if (logger.isDebugEnabled()) {
                logger.debug("Voter: " + voter + ", returned: " + result);
            }

            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"));
        }

        // To get this far, every AccessDecisionVoter abstained
        checkAllowIfAllAbstainDecisions();
    }
}

AffirmativeBasedThe main authentication logic is given to the voter to judge. The voter returns different numbers to represent different results, and thenAffirmativeBasedAccording to their own one vote strategy to decide whether to release or throw an exception.

AffirmativeBasedBy default, only one constructor is passed in – >WebExpressionVoterThis constructor will logically process the voting result according to your configuration in the configuration file.

thereforeSpringSecurityThe default authentication logic is to authenticate according to the configuration in the configuration file, which is in line with our existing understanding.

Two A kind of Implementation of dynamic authentication

Through the above step by step, I think you should understandSpringSecurityWhat is the implementation of authentication? What should we do if we want to dynamically give different access rights to a certain role?

Since it is dynamic authentication, our permission URI must be put in the database. What we need to do is read the permissions corresponding to different roles in the database in real time, and then compare with the current login user.

Then we can think of some solutions to achieve this step, such as:

  • Rewrite one directlyAccessDecisionManager, using it as the defaultAccessDecisionManagerAnd write the authentication logic directly in it.
  • Another example is to rewrite a voter and put it in the defaultAccessDecisionManagerInside, as before, use the voter authentication.
  • I think there are some blogs on the Internet that can be done directlyFilterSecurityInterceptorChanges to the.

I always like the small and beautiful way, and make few changes, so the code demonstrated here will be based on the second scheme and slightly modified.

Then we need to write a new voter, in which we can get the role of the current user and compare it with the role required by the current request.

This alone is not enough, because we may have some permission to release in the configuration file. For example, login URI is released, so we need to continue to use the permission mentioned aboveWebExpressionVoterThat is to say, I want to customize the double line mode of permission + configuration file, so ourAccessDecisionManagerThere will be two voting machines in it:WebExpressionVoterAnd custom voter.

Then we need to consider what kind of voting strategy to use. Here I am usingUnanimousBasedOne vote against policy, instead of using the default one vote through policy, because in our configuration, all requests except login requests need to be authenticated, and this logic will be ignoredWebExpressionVoterIf the one vote pass policy is used, when we access the protected API,WebExpressionVoterIf you find that the current request has been authenticated, you vote for it directly. Because it’s a one vote policy, this request can’t go to our custom voter.

Note: you can also put your custom permission configuration in the database without the configuration in the configuration file, and then give it to a voter for processing.

1. Reconstruct the accessdecisionmanager

Then we can go ahead and rebuild firstAccessDecisionManager
Because the voter is automatically added when the system starts, we want to add one more constructor and have to rebuild it ourselvesAccessDecisionManagerAnd then put it in the configuration.

And our voting strategy has changed. It’s up to usAffirmativeBasedchange intoUnanimousBasedSo this step is essential.

And we need to customize a voter to register it as a bean,AccessDecisionProcessorWe need a custom voter.

@Bean
    public AccessDecisionVoter<FilterInvocation> accessDecisionProcessor() {
        return new AccessDecisionProcessor();
    }

@Bean
    public AccessDecisionManager accessDecisionManager() {
        //Construct a new accessdecisionmanager and put it into two voters
        List<AccessDecisionVoter<?>> decisionVoters = Arrays.asList(new WebExpressionVoter(), accessDecisionProcessor());
        return new UnanimousBased(decisionVoters);
    }

End of definitionAccessDecisionManagerAfter that, we put it into the boot configuration:

@Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()
                //Release all options requests
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                //Release login method
                .antMatchers("/api/auth/login").permitAll()
                //Other requests require authentication before they can be accessed
                .anyRequest().authenticated()
                //Using custom accessdecisionmanager
                .accessDecisionManager(accessDecisionManager())
                .and()
                //Add an exception handler with no login and insufficient permissions
                .exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler())
                .authenticationEntryPoint(restAuthenticationEntryPoint())
                .and()
                //Put the custom JWT filter into the filter chain
                .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class)
                //Open spring security cross domain
                .cors()
                .and()
                //Turn off CSRF
                .csrf().disable()
                //Turn off session mechanism
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

After that,SpringSecurityInsideAccessDecisionManagerIt will be replaced by our custom oneAccessDecisionManagerIt’s too late.

2. User defined authentication implementation

In the above configuration, there are two voting machines. The second voting machine is the one we need to create. I call itAccessDecisionProcessor

It also has an interface specification. We only need to implement thisAccessDecisionVoterInterface, and then implement its method.

@Slf4j
public class AccessDecisionProcessor implements AccessDecisionVoter<FilterInvocation> {
    @Autowired
    private Cache caffeineCache;

    @Override
    public int vote(Authentication authentication, FilterInvocation object, Collection<ConfigAttribute> attributes) {
        assert authentication != null;
        assert object != null;

        //Get the current request URI
        String requestUrl = object.getRequestUrl();
        String method = object.getRequest().getMethod();
        log.debug ("enter custom authentication voter, URI: {} {}", method, requesturl) ";

        String key = requestUrl + ":" + method;
        //If there is no such permission in the cache, that is, the API is not protected, abstain
        PermissionInfoBO permission = caffeineCache.get(CacheName.PERMISSION, key, PermissionInfoBO.class);
        if (permission == null) {
            return ACCESS_ABSTAIN;
        }

        //Get the permission of the current user
        List<String> roles = ((UserDetail) authentication.getPrincipal()).getRoles();
        if (roles.contains(permission.getRoleCode())) {
            return ACCESS_GRANTED;
        }else{
            return ACCESS_DENIED;
        }
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

The general logic is as follows: we use URI + method as the key to find permission related information in the cache. If this URI is not found, it proves that the URI is not protected, and the voter can abstain directly.

If the relevant permission information of this URI is found, it will be compared with the user’s own role information, and the comparison result will be returnedACCESS_GRANTEDorACCESS_DENIED

Of course, there is a premise to do this, that is, I put the URI permission data into the cache when the system starts up. Generally, the system will put the hotspot data into the cache when it starts up, so as to improve the access efficiency of the system.

@Component
public class InitProcessor {
    @Autowired
    private PermissionService permissionService;
    @Autowired
    private Cache caffeineCache;

    @PostConstruct
    public void init() {
        List<PermissionInfoBO> permissionInfoList = permissionService.listPermissionInfoBO();
        permissionInfoList.forEach(permissionInfo -> {
            caffeineCache.put(CacheName.PERMISSION, permissionInfo.getPermissionUri() + ":" + permissionInfo.getPermissionMethod(), permissionInfo);
        });
    }
}

Here, I consider that there may be many privilege URIs, so I put the privilege URI as a key in the cache. Generally, the speed of reading data through the key in the cache is O (1), so it will be very fast.

How to deal with the authentication logic is actually defined by the developer himself. It should be considered comprehensively according to the system requirements and database table design. Here is just an idea.

If you don’t understand the above idea of making a key, I can give another simple example:

such asYou can also get the current user’s role, find all the accessible URIs under the role, and then compare the current request URI. If there is a consistency, it proves that the current user’s role contains the permission of the URI, so it can be released. If there is no consistency, it proves that the permission is not enough.

In this way, when I compare URIs, I may encounter such a problem: what is my current role permission/api/user/**, and the URI I requested is/user/get/1This ant style permission definition method can be compared with a tool class

@Test
    public void match() {
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        // true
        System.out.println(antPathMatcher.match("/user/**", "/user/get/1"));
    }

This is for me to test directlynewI got oneAntPathMatcherIn practice, you can register it as a bean and inject it into theAccessDecisionProcessorIt can be used in.

It can also compare restful style URIs, such as:

@Test
    public void match() {
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        // true
        System.out.println(antPathMatcher.match("/user/{id}", "/user/1"));
    }

In the face of the real system, it is often based on the combination of system design to use these tool classes and design ideas.

notesACCESS_GRANTEDACCESS_DENIEDandACCESS_ABSTAINyesAccessDecisionVoterConstant with in the interface.

Postscript

Well, that’s all for this issue. I’ve been here since Sunday.

I usually write articles three times

  • The first time is the first draft, which will be translated into words after sorting out the existing ideas.
  • The second time is to find out what’s missing in the original idea.
  • The third time is to reorganize the language structure.

After three times, I dare to send it, so the authentication and authorization are divided into two parts. One is that they can be written separately, and the other is that it takes a lot of time to write together. This is my first time to write, and I dare not set too big a goal.

It’s like the first time you recite a word, you tell yourself that you have to recite 1000 words a day, but you can’t recite them in the end. Then you blame yourself and end up in a cycle.

In the early stage, setting too big a goal is often counterproductive. In the early stage, we must choose what we can do, first taste the joy of completion, and then gradually increase the difficulty. This is the truth of many things.

After the end of this article, the authentication and authorization of spring security are completed. I hope you can get something.

You can review the authentication process of spring security in the last article.

I haven’t thought about the next one. I think I’ll write some common tools or configuration problems that I often encounter in development. Relax. I have plans for oauth2. I don’t know if anyone will read it.

If you think it’s well written, you can give me a hand to praise it. After all, I need to upgrade

Each of your likes and comments is a great affirmation of my knowledge output. If there is any mistake or doubt in the article or my advice, you can leave a message at the bottom of the comment area and discuss it together.

I am ear, a person who always wants to export knowledge. See you next time.

Article code:Code cloud address   GitHub address

Recommended Today

Rust programming video tutorial (Advanced) – 024_ 3 syntax of all modes 3

Video address Headline address:https://www.ixigua.com/i677586170644791348…Station B address:https://www.bilibili.com/video/av81202308/ Source address GitHub address:https://github.com/anonymousGiga/learn_rus… Explanation content 1. Ignore values in mode(1) Use_ Ignore entire valueexample: fn foo(_: i32, y: i32) { println!(“This code only uses the y parameter: {}”, y); } fn main() { foo(3, 4); } Note: placeholders are used for parameters in the function, mainly when implementing […]