Spring Security08 认证授权流程

前言

上一章节带大家认识了Spring Security一些重要的核心Api,只有了解了这些Api的功能用法,才能为本章节的认证授权流程打下基础,这样才真正的达到了 "知其所以然" !

流程图概述

上一章节,我就贴出过Security的认证授权流程图,展示了认证授权时经历的核心api和流程,接下来我们结合源码,一点点分析认证和授权的实现过程。


image.png

简要剖析认证授权实现流程代码逻辑

Spring Security的认证授权流程很复杂,在深入了解源码前,我先为大家简要概括一下认证和授权流程,如下:

1、用户登陆前,默认生成的Authentication对象处于未认证状态,登陆时会交给AuthenticationManager负责认证
2、AuthenticationManager会将Authentication中的用户名和密码与UserDetails中的用户名和密码对比,完成认证后,认证成功会生成一个已认证状态的Authentication对象
3、最后把认证通过的Authentication对象写入到SecurityContext中,在用户有后续请求时,可以从Authentication中检查权限。

借鉴一下Spring Security官方文档中提供的一个简化流程代码,来认识认证授权的实现过程,该代码省略了UserDetails操作,只做了简单认证,可以对认证授权有个大概了解。

public class AuthenticationExample {
    
    private static AuthenticationManager am = new SampleAuthenticationManager();
 
    public static void main(String[] args) throws Exception {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
 
        while (true) {
            //模拟输入用户名密码
            System.out.println("Please enter your username:");
            String name = in.readLine();
            
            System.out.println("Please enter your password:");
            String password = in.readLine();
            
            try {
                //根据用户名/密码,生成未认证Authentication
                Authentication request = new UsernamePasswordAuthenticationToken(name, password);
                //交给AuthenticationManager 认证
                Authentication result = am.authenticate(request);
                //将已认证的Authentication放入SecurityContext
                SecurityContextHolder.getContext().setAuthentication(result);
                break;
            } catch (AuthenticationException e) {
                System.out.println("Authentication failed: " + e.getMessage());
            }
        }
        
        System.out.println("Successfully authenticated. Security context contains: "
                + SecurityContextHolder.getContext().getAuthentication());
    }
}

//认证类
class SampleAuthenticationManager implements AuthenticationManager {
    //配置一个简单的用户权限集合
    static final List AUTHORITIES = new ArrayList();
    
    static {
        AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
    }
 
    public Authentication authenticate(Authentication auth) throws AuthenticationException {
        //如果用户名和密码一致,则登录成功,这里只做了简单认证
        if (auth.getName().equals(auth.getCredentials())) {
            //认证成功,生成已认证Authentication,比未认证多了权限
            return new UsernamePasswordAuthenticationToken(auth.getName(), auth.getCredentials(), AUTHORITIES);
        }
        
        throw new BadCredentialsException("Bad Credentials");
    }
}

以上代码只是简单的模拟了认证的过程,那么真正的认证授权操作,是不是也这样呢?

Security认证流程源码详解

1、Spring Security过滤器链执行顺序

认证、授权相关的检验都是利用一系列的过滤器来完成的,这些过滤器共同组成了一个过滤器链,如下图所示:


image.png

那我们怎么知道这些过滤器有没有执行或者他们的执行顺序是什么呢?
其实只要开启Spring Security的debug调试模式,开发时就可以在控制台看到这些过滤器的执行顺序

image.png

image.png

可以看到,Spring Security中默认执行的过滤器顺序如下:

WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
UsernamePasswordAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter:如果之前的认证机制都没有更新 SecurityContextHolder 拥有的 Authentication,那么一个 AnonymousAuthenticationToken 将会设给 SecurityContextHolder。
SessionManagementFilter
ExceptionTranslationFilter:用于处理在 FilterChain 范围内抛出的 AccessDeniedException 和 AuthenticationException,并把它们转换为对应的 Http 错误码返回或者跳转到对应的页面。
FilterSecurityInterceptor:负责保护 Web URI,并且在访问被拒绝时抛出异常。

SecurityContextPersistenceFilter

上面的过滤器链中,有个SecurityContextPersistenceFilter过滤器,他是Saecurity中的一个拦截器,执行时机很早,请求来临时它会从SecurityContextRepository中把SecurityContext对象取出来,然后放入SecurityContextHolder的ThreadLocal中。在所有拦截器都处理完后,再把SecurityContext存入SecurityContextRepository,并且清除SecurityContextHolder内的SecurityContext引用。

AbstractAuthenticationProcessingFilter

和认证授权直接相关的过滤器是AbstractAuthenticationProcessingFilter 和 UsernamePasswordAuthenticationFilter

AbstractAuthenticationProcessingFilter 是一个抽象的父类,在其内部定义了认证处理的过程,UsernamePasswordAuthenticationFilter 就继承自 AbstractAuthenticationProcessingFilter。

image.png

可以看到,AbstractAuthenticationProcessingFilter的父类是GenericFilterBean,而 GenericFilterBean 是 Spring 框架中的过滤器类,最终的父接口是Filter, 也就是我们熟悉的过滤器。那么既然是Filter的子类,肯定会执行其中最核心的doFilter()方法,我们来看看AbstractAuthenticationProcessingFilter的doFilter()方法源码。


image.png

image.png

从上面的源码中我们可以看出,doFilter()方法的内部实现并不复杂,里面就只引用了5个方法,如下:

requiresAuthentication(request, response);
authResult = attemptAuthentication(request, response);
sessionStrategy.onAuthentication(authResult, request, response);
unsuccessfulAuthentication(request, response, failed);
successfulAuthentication(request, response, chain, authResult);
这几个方法具体的作用如下:
首先执行requiresAuthentication(HttpServletRequest, HttpServletResponse) 方法, 来决定是否需要进行验证操作;
如果需要验证,接着会调用attemptAuthentication(HttpServletRequest, HttpServletResponse) 方法来封装用户信息再进行验证,可能会有三种结果产生:
1、返回的Authentication对象为null,表示身份验证不完整,方法立即结束返回。
2、返回的Authentication对象不为空,,则调用配置的 SessionAuthenticationStrategy 对象,执行onAuthentication()方法,然后调用 successfulAuthentication(HttpServletRequest,HttpServletResponse,FilterChain,Authentication) 方法。
3、验证时如果发生 AuthenticationException,则执行unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException) 方法。
我们在上面的源码中得知有个attemptAuthentication()方法,该方法是一个抽象方法,由子类来具体实现。

image.png

这个抽象方法由子类UsernamePasswordAuthenticationFilter来实现


image.png

AbstractAuthenticationProcessingFilter是一个比较复杂的类,内部的处理流程比较多,我们做个简单梳理,如下图所示:


image.png

综上所述,我们可知AbstractAuthenticationProcessingFilter类,可以负责处理所有的HTTP Request和Response对象,并将其封装成AuthenticationMananger可以处理的Authentication对象。在身份验证成功或失败之后,将对应的行为转换为HTTP的Response对象。同时还能处理一些Web特有的资源,比如Session和Cookie等操作。
到此为止,我们已经把AbstractAuthenticationProcessingFilter这个类的核心功能给了解清楚了,接下来我们学习AbstractAuthenticationProcessingFilter的子类UsernamePasswordAuthenticationFilter。

UsernamePasswordAuthenticationFilter

我在上一小节中说过,在 Spring Security 中,认证与授权的相关校验是在AbstractAuthenticationProcessingFilter这个过滤器中完成的,但是该类是一个抽象类,它有个子类UsernamePasswordAuthenticationFilter,该类是和认证直接相关的过滤器实现子类。我们来看一下该类的核心源码:

public class UsernamePasswordAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {
    
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

    ...
    ...

    // ~ Constructors
    // ===================================================================================================

    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String username = obtainUsername(request);
        String password = obtainPassword(request);

        if (username == null) {
            username = "";
        }

        if (password == null) {
            password = "";
        }

        username = username.trim();

        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }

    @Nullable
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(passwordParameter);
    }

    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(usernameParameter);
    }

    protected void setDetails(HttpServletRequest request,
            UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    ......其他略......
}

来解释一下上面的源码:
1、首先我们从构造方法中可以得知,该过滤器 只对post请求方式的"/login"接口有效;
2、然后在该过滤器中,再利用 obtainUsername 和 obtainPassword 方法,提取出请求里边的用户名/密码,提取方式就是 request.getParameter,这也是为什么 Spring Security 中默认的表单登录要通过 key/value 的形式传递参数,而不能传递 JSON 参数。如果像传递 JSON 参数,我们可以通过修改这里的代码来进行实现。
3、获取到请求里传递来的用户名/密码之后,接下来会 构造一个 UsernamePasswordAuthenticationToken 对象,传入 username 和 password。 其中 username 对应了 UsernamePasswordAuthenticationToken 中的 principal 属性,而 password 则对应了它的 credentials 属性。
4、接下来 再利用 setDetails 方法给 details 属性赋值,UsernamePasswordAuthenticationToken 本身是没有 details 属性的,这个属性是在它的父类 AbstractAuthenticationToken 中定义的。details 是一个对象,这个对象里边存放的是 WebAuthenticationDetails 实例,该实例主要描述了 请求的 remoteAddress 以及请求的 sessionId 这两个信息。
5、最后一步,就是利用AuthenticationManager对象来调用 authenticate() 方法去做认证校验。

AuthenticationManager与ProviderManager

在上面 UsernamePasswordAuthenticationToken类的 attemptAuthentication() 方法中得知,该方法的最后一步会进行关于认证的校验,而要进行认证操作首先要获取到一个 AuthenticationManager 对象,这里默认拿到的是AuthenticationManager的子类ProviderManager ,如下图所示:


image.png

image.png

所以接下来进入到 ProviderManager 的 authenticate()方法中,来看看认证到底是怎么实现的。


image.png

image.png

1、首先利用反射,获取到要认证的authentication 对象的 Class字节码。
image.png

2、判断当前provider是否支持该authentication 对象,通过authentication 对象的 Class字节码来判断,在上一节有提到过。如果当前provider不支持该 authentication 对象,则退出当前判断,进行下一次判断。


image.png

3、如果支持,调用provider 的 authenticate 方法开始做校验,校验完成后,会返回一个新的 Authentication。


image.png

4、这里的 provider 会有多个,我们在上一章节给大家介绍过,如下图:


image.png

这里如果 provider 的 authenticate 方法没能返回一个 Authentication 认证对象,则会调用 provider 的 parent 对象中的 authenticate 方法继续校验。


image.png

5、而如果通过了校验,返回了一个Authentication 认证对象,则调用 copyDetails()方法把旧 Token 的 details 属性拷贝到新的 Token 中,如下图。
image.png

image.png

6、接下来会调用 eraseCredentials()方法来擦除凭证信息,也就是我们的密码,这个擦除方法比较简单,就是将 Token 中的 credentials 属性置空。


image.png

image.png

7、最后通过 publishAuthenticationSuccess() 方法将认证成功的事件广播出去。


image.png

在以上代码的for循环中,第一次拿到的provider是一个AnonymousAuthenticationProvider。这个provider是不支持UsernamePasswordAuthenticationToken的,所以会直接在provider.supports()方法中返回false。结束当前for循环,并且进入到下一个if判断中,最后直接调用 parent 的 authenticate 方法进行校验。

这里面的parent就是ProviderManager对象,所以会再次回到这个authenticate()方法中。当再次回到authenticate()方法时,这个provider是支持UsernamePasswordAuthenticationToken 的,所以会顺利进入到该类的authenticate()方法去执行。

DaoAuthenticationProvider

DaoAuthenticationProvider继承自AbstractUserDetailsAuthenticationProvider, DaoAuthenticationProvider类结构如下所示:


image.png

DaoAuthenticationProvider类中并没有重写 authenticate() 方法,authenticate() 方法是在父类AbstractUserDetailsAuthenticationProvider中实现的 。 所以我们看看AbstractUserDetailsAuthenticationProvider#authenticate()方法的源码,这里我对该源码做了一些简化:


public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));
                //获取authentication中存储的用户名
        String username = determineUsername(authentication);
        //判断是否使用了缓存
                boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;
            try {
                                //retrieveUser()是一个抽象方法,由子类DaoAuthenticationProvider来实现,用于根据用户名查询用户。
                user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException ex) {
                this.logger.debug("Failed to find user '" + username + "'");
                if (!this.hideUserNotFoundExceptions) {
                    throw ex;
                }
                throw new BadCredentialsException(this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }
        try {
            
 //进行必要的认证前和额外认证的检查        this.preAuthenticationChecks.check(user);
            //这是抽象方法,由子类DaoAuthenticationProvider来实现,用于进行密码对比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);
    }
private String determineUsername(Authentication authentication) {
        return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
    }
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
            UserDetails user) {
        // Ensure we return the original credentials the user supplied,
        // so subsequent attempts are successful even with encoded passwords.
        // Also ensure we return the original getDetails(), so that future
        // authentication events after cache expiry contain the details
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
                authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        this.logger.debug("Authenticated user");
        return result;
    }

结合上面的源码,我们做进一步的分析梳理:
上面说过,DaoAuthenticationProvider没有重写父类AbstractUserDetailsAuthenticationProvider的authenticate()方法,而是在父类实现的。
AbstractUserDetailsAuthenticationProvider类中的authenticate()方法执行时, 首先会从 Authentication 提取出登录用户名:

image.png

image.png

利用得到的username,先去缓存中查看是否有这个用户


image.png

如果缓存中没有该用户,则去执行 retrieveUser() 方法获取当前用户对象。 而这个retrieveUser()方法是个抽象方法,在AbstractUserDetailsAuthenticationProvider类中并没有实现,是由子类DaoAuthenticationProvider来实现的。


image.png

在DaoAuthenticationProvider类的retrieveUser() 方法中, 会调用getUserDetailsService()方法,得到 UserDetailsService对象,执行 我们自己在登录时候编写的 loadUserByUsername()方法 ,然后返回一个UserDetails对象,也就是我们的登录对象。


image.png

接下来会继续往下执行preAuthenticationChecks.check()方法,检验 user 中各账户属性是否正常,例如账户是否被禁用、是否被锁定、是否过期等,如下所示。


image.png

image.png

接着继续往下执行additionalAuthenticationChecks方法,进行密码的比对,该方法也是抽象方法,也是在子类DaoAuthenticationProvider进行实现。我们在注册用户时对密码加密之后,Spring Security就是在这里进行密码比对的 。

image.png

image.png

image.png

然后在 postAuthenticationChecks.check()方法中检查密码是否过期


image.png

image.png

接下去判断是否进行了缓存,如果未进行缓存,则执行缓存操作,这个缓存是由 SpringCacheBasedUserCache类来实现的。


image.png

image.png

如果我们没有对缓存进行配置,就会执行默认的缓存配置操作。如果我们对缓存进行了自定义的配置,比如配置了RedisCache,就可以把对象缓存到redis中。


image.png

接下来有一个 forcePrincipalAsString 属性,该属性表示 是否强制将 Authentication 中的 principal 属性设置为字符串,这个属性其实我们一开始就在 UsernamePasswordAuthenticationFilter 类中定义为了字符串(即username)。但是默认情况下,当用户登录成功之后,这个属性的值就变成当前用户这个对象了。
之所以会这样,就是因为 forcePrincipalAsString 默认为 false,不过这块其实不用改,就用 false,这样在后期获取当前用户信息的时候反而方便很多。


image.png

最后通过createSuccessAuthentication()方法构建出一个新的 UsernamePasswordAuthenticationToken对象。


image.png

这样我们最终得到了认证通过的Authentication对象,并把该对象利用publishAuthenticationSuccess()方法,将该事件发布出去。
image.png

Spring Security会监听这个事件,接收到这个Authentication对象,进而调用 SecurityContextHolder.getContext().setAuthentication(...)方法,将 AuthenticationManager返回的 Authentication对象,存储在当前的 SecurityContext 对象中。
保存Authentication认证信息

我们在上面说,Authentication认证信息最终是保存在SecurityContext 对象中的,但是具体的代码是在哪里实现的呢?
来到 UsernamePasswordAuthenticationFilter 的父类 AbstractAuthenticationProcessingFilter 中,这个类我们经常会见到,因为很多时候当我们想要在 Spring Security 自定义一个登录验证码或者将登录参数改为 JSON 的时候,我们都需自定义过滤器继承自 AbstractAuthenticationProcessingFilter。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);

            return;
        }

        ......

        Authentication authResult;

        try {
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                // return immediately as subclass has indicated that it hasn't completed
                // authentication
                return;
            }
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        catch (InternalAuthenticationServiceException failed) {
            logger.error(
                    "An internal error occurred while trying to authenticate the user.",
                    failed);
            unsuccessfulAuthentication(request, response, failed);

            return;
        }
        catch (AuthenticationException failed) {
            // Authentication failed
            unsuccessfulAuthentication(request, response, failed);

            return;
        }

        // Authentication success
        if (continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }

        //处理认证后的操作
        successfulAuthentication(request, response, chain, authResult);
    }

有个successfulAuthentication()方法,如下图。


image.png

当UsernamePasswordAuthenticationFilter#attemptAuthentication()方法被触发执行时,如果登录时抛出异常,unsuccessfulAuthentication()方法会被调用;而当登录成功时 , successfulAuthentication()方法则会被调用,正是这个方法来保存的Authentication认证信息,我们来看看这个源码。

image.png

在这里有一段很重要的代码,就是 SecurityContextHolder.getContext().setAuthentication(authResult),登录成功的用户信息就被保存在这里。 在认证成功后,我们就可以在任何地方,通过 SecurityContextHolder.getContext()获取到Authentication认证信息。
最后大家还看到还有一个 successHandler.onAuthenticationSuccess()方法,这是我们在 SecurityConfig中配置的登录成功时的回调处理方法,就是在这里被触发。

ExceptionTranslationFilter

Spring Security在进行认证授权的过程中,可能会产生各种认证授权异常。对于这些异常,都是由ExceptionTranslationFilter来捕获过滤器链中产生的所有异常并进行处理的。但是它只会处理两类异常:**AuthenticationException **和 AccessDeniedException,其它的异常它会继续抛出。
如果捕获到的是 AuthenticationException异常,那么将会使用其对应的 AuthenticationEntryPoint 里的commence()方法来处理。 在处理之前,ExceptionTranslationFilter先使用 RequestCache 将当前的HttpServerletRequest的信息保存起来,以至于用户认证登录成功后可以跳转到之前指定跳转到的界面。
如果捕获到的是 AccessDeniedException异常,那么将根据当前用户是否已经登录认证做出不同的处理。如果未登录,则会使用关联的 AuthenticationEntryPoint 的 commence()方法来进行处理;否则将会使用关联的 AccessDeniedHandler 的handle()方法来进行处理。

FilterSecurityInterceptor

当我们经历了前面一系列的认证授权处理后,最后还有一个FilterSecurityInterceptor 用于保护Http资源,它内部引用了一个AccessDecisionManager和一个AuthenticationManager对象。它会从 SecurityContextHolder 获取 Authentication对象,然后通过 SecurityMetadataSource 可以得知当前是否在请求受保护的资源。如果请求的是那些受保护的资源,如果Authentication.isAuthenticated() 返回false或者FilterSecurityInterceptor的alwaysReauthenticate 属性为 true,那么将会使用其引用的 AuthenticationManager 再认证一次。 认证之后再使用认证后的 Authentication 替换 SecurityContextHolder 中拥有的旧的那个Authentication对象,然后就是利用 AccessDecisionManager 进行权限的检查。

  • AuthenticationEntryPoint 是在用户未登录时,用于引导用户进行登录认证的;
  • AccessDeniedHandler 是在用户已经登录后,但是访问了自身没有权限的资源时做出的对应处理。
认证流程总结

接下来我对Spring Security进行认证授权的源码执行流程做个简单总结:
1、首先,用户填写用户名和密码,提交表单登录
2、AbstractAuthenticationProcessingFilter结合UsernamePasswordAuthenticationToken过滤器,将获取到的用户名和密码封装成一个实现了 Authentication 接口的实现子类 UsernamePasswordAuthenticationToken对象。
(也可以手动创建UsernamePasswordAuthenticationToken子类)
3、将上述产生的 token 对象传递给 AuthenticationManager的具体子类ProviderManager 进行登录认证。
4、ProviderManager 认证成功后将会返回一个封装了用户权限等信息的 Authentication 对象。
5、通过调用 SecurityContextHolder.getContext().setAuthentication(...) 将 AuthenticationManager 返回的 Authentication 对象赋予给当前的 SecurityContext。

我们可以结合下面两图,和上面的源码,深入理解掌握Spring Security的认证授权流程。

image.png

image.png

到此为止,大家大致了解掌握了Spring Security核心的认证授权源码,也就是底层执行原理。既然已经知其所以然了,那便少不了应用,上面提到的都是用户密码形式的登录,下节将带大家自定义认证器,实现微信一键登录功能~

你可能感兴趣的:(Spring Security08 认证授权流程)