Spring Security 详解【万字解析】

前言

  • 在阅读本文前,希望你具备如下知识
    • 了解Spring Boot和Spring Security的基本使用
    • 具备Java Web的基本知识
    • 具备认证和授权的基本知识
  • 本文基于Spring Boot2.6.6讲解,内置Spring Security版本为5.6.2,所采用的配置方式依然为继承WebSecurityConfigurerAdapter的方式,虽然新版Spring Security已经不再采用这种配置方式,但本文对于你了解Spring Security的工作原理仍然具有重要意义。
  • 看源码的时候最好盯紧流程图,抓住主线

1.Spring Boot如何集成Spring Security

  • IDEA中新建一个Spring Boot工程
  • 引入依赖
    <dependencies>
        <!-- Spring Web相关依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <!-- Spring Security相关依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>
    
  • 编写一个简单的Controller
    @RestController
    @RequestMapping("/hello")
    public class HelloController {
    
        @GetMapping("/helloWorld")
        public String helloWorld() {
            return "Hello World!";
        }
    }
    
  • 启动项目,在浏览器的地址栏输入http://localhost:8080/hello/helloWorld,此时会出现如何如下登录页
    登录页
  • 输入默认的用户名user和密码(idea控制台会打印出默认密码,如下图所示),登录成功,浏览器页面成功展示出"Hello World!"
    默认密码 以上便是Spring Boot集成Spring Security的基本流程,那么我们访问HelloController时,浏览器为何会跳转到登录页?默认用户名为什么是user?控制台的初始密码是如何打印出来的?

2.Spring Security的默认认证和授权流程

  首先说一下的浏览器为何会跳转到登录页,打开浏览器的开发者工具可以看到我们的请求被重定向到了http://localhost:8080/login
重定向
  为什么我们的请求会被重定向呢?肯定是我们没有通过认证呀,一定是在没有通过Spring Security的认证的情况下,Spring Security拦截了我们请求之后,将请求重定向到了默认登录页,当然这只是猜测,具体为什么会跳转到默认登录页,还需要我们了解Spring Security的认证和授权流程。既然前面都已经提到拦截请求了,那什么能拦截我们的请求呢,那肯定是Filter呀!对,你没猜错,Spring Security的认证和授权就是通过一系列Filter实现的,即FilterChain,当我们集成Spring Security时,Spring security就为我们配置了默认的FilterChain,其中包含15个Filter,可以可以通过下面的代码查看15个Filter,具体这15个Filter从何而来,我们会在后面进行讨论。

@SpringBootApplication
public class SpringSecurityApplication {
    public static void main(String[] args) throws ClassNotFoundException {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(SpringSecurityApplication.class, args);
        DefaultSecurityFilterChain defaultSecurityFilterChain = applicationContext.getBean(DefaultSecurityFilterChain.class);
        List<Filter> filters = defaultSecurityFilterChain.getFilters();
        for (int i = 0; i < filters.size(); i++) {
            System.out.println((i + 1) + ": " + filters.get(i));
        }
    }
}

FilterChain
  默认情况下,当我们访问HelloController,我们请求会经过上图所示的15个Filter,啊!怎么会有这么多FilterSpring Security怎么如此复杂,既然Spring Secuity是认证和授权框架,那么我们只要着重研究与认证和授权有关的Filter就行,那么,是哪个Filter呢?对,你没看错,就是那个UsernamePasswordAuthenticationFilter,见名知意,一看这个Filter就是用来进行认证的。既然是UsernamePasswordAuthenticationFilter,那肯定是将用户输入的用户名和密码与服务器存储的用户名和密码进行比对,一致就认证通过,不一致就认证失败,至于这个密码怎么存储,存储在什么地方,我们后面再进行讨论。认证通过之后就进行授权,那么授权又是哪一个Filter完成的呢,可能看名字看不出来,其实就是最后一个FilterSecurityInterceptor,咦?怎么就这个不叫Filter,叫Interceptor,那是因为请求到达接口之前要进行拦截,判断本次请求是否有权限访问接口。

2.1 认证

UsernamePasswordAuthenticationFilter的大致认证流程如下图所示。
认证流程图

  • AbstractAuthenticationProcessingFilter:基于浏览器的http认证请求的抽象处理器。该过滤器有一个authenticationManager属性。具体的认证流程交给AuthenticationManager进行处理。该过滤器还有一个requiresAuthenticationRequestMatcher,如果请求与matcher匹配,该过滤器将拦截请求并尝试从该请求执行身份验证,身份验证由attemptAuthentication方法执行,该方法必须由子类实现。
  • UsernamePasswordAuthenticationFilter:对表单提交的用户名和密码进行认证。
  • Authentication:封装了当前待认证的用户信息,和认证通过后的用户信息,最常见的实现类是UsernamePasswordAuthenticationToken
  • AuthenticationManager:只有一个抽象方法Authentication authenticate(Authentication authentication),其实现类需要重写该方法,所有的认证流程都在该方法中,ProviderManager是其最常见的实现类。
  • AbstractUserDetailsAuthenticationProviderAuthenticationProvider的实现类,而AbstractUserDetailsAuthenticationProvider是使用UserDetail对象进行认证的,认证时需要一个Authentication对象,认证成功后返回一个Authentication对象。该类主要用于响应UsernamePasswordAuthenticationToken身份验证请求。不同的AuthenticationProvider在认证时所需要的Authentication对象是不同的。
  • AuthenticationProvider:具有Authentication authenticate(Authentication authentication)boolean supports(Class<?> authentication)两个抽象方法,具体的认证逻辑由不同的实现类提供,不同的实现类认证不同Authentication对象,方法supports用于判定该实现类是否支持对应的Authentication
  • UserDetailsService:用于获取服务器存储的用户信息的核心接口,只有一个抽象方法UserDetails loadUserByUsername(String username)
  • UserDetails:封装服务器存储的用户信息。

2.1.1 UsernamePasswordAuthenticationFilter

  可能直接看认证流程图比较抽象,下面就结合具体代码来理解上面的认证流程,代码里面还有很多细节,首先是UsernamePasswordAuthenticationFilter的父类AbstractAuthenticationProcessingFilter如下所示。

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
        implements ApplicationEventPublisherAware, MessageSourceAware {

    protected ApplicationEventPublisher eventPublisher;
    
    /**
     * 用于请求认证
     */
    private AuthenticationManager authenticationManager;

    private RememberMeServices rememberMeServices = new NullRememberMeServices();

    /**
     * 用于判断当前请求是否需要认证
     */
    private RequestMatcher requiresAuthenticationRequestMatcher;

    /**
     * 认证成功后是否要继续执行过滤器链
     */
    private boolean continueChainBeforeSuccessfulAuthentication = false;

    /**
     * 认证成功后的session处理策略
     */
    private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
    
    /**
     * 用于处理认证成功后相关事宜
     */
    private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();

    /**
     * 用于处理认证失败后相关事宜
     */
    private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

    /**
     * @param defaultFilterProcessesUrl 当前Filter要进行认证的url,实际就是为属性requiresAuthenticationRequestMatcher赋值
     */
    protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
        setFilterProcessesUrl(defaultFilterProcessesUrl);
    }

    /**
     * @param requiresAuthenticationRequestMatcher 用于判断当前请求是否需要认证
     */
    protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
        Assert.notNull(requiresAuthenticationRequestMatcher, "requiresAuthenticationRequestMatcher cannot be null");
        this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
    }

    /**
     * @param defaultFilterProcessesUrl 当前Filter要进行认证的url
     * @param authenticationManager 用于请求认证
     */
    protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl,
                                                     AuthenticationManager authenticationManager) {
        setFilterProcessesUrl(defaultFilterProcessesUrl);
        setAuthenticationManager(authenticationManager);
    }

    /**
     * @param requiresAuthenticationRequestMatcher 用于判断当前请求是否需要认证
     * @param authenticationManager 用于请求认证
     */
    protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher,
                                                     AuthenticationManager authenticationManager) {
        setRequiresAuthenticationRequestMatcher(requiresAuthenticationRequestMatcher);
        setAuthenticationManager(authenticationManager);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
    }

    /**
     * 调用requiresAuthentication方法来确定请求是否用于身份验证,是否应该由此过滤器处理。
     * 如果是身份验证请求,则调用attemptAuthentication来执行身份验证。有三种可能的结果:
     *  1.返回一个Authentication对象。配置的SessionAuthenticationStrategy将被调用,
     *    然后调用successfulAuthentication方法
     *  2.认证过程中出现异常。将调用unsuccessfulAuthentication方法
     *  3.返回Null,表示认证过程未完成
     */
    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        // 判断本次请求是否需要认证,每次会话认证只需要进行一次,认证成功后,以后的请求都不需要认证
        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
            return;
        }
        try {
            // 进行认证,具体的认证流程由子类实现
            Authentication authenticationResult = attemptAuthentication(request, response);
            if (authenticationResult == null) {
                // return immediately as subclass has indicated that it hasn't completed
                return;
            }
            // 认证成功后的session处理策略
            this.sessionStrategy.onAuthentication(authenticationResult, request, response);
            // Authentication success
            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }
            // 认证成功后的处理策略
            successfulAuthentication(request, response, chain, authenticationResult);
        }
        catch (InternalAuthenticationServiceException failed) {
            this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
            // 认证失败后的处理策略
            unsuccessfulAuthentication(request, response, failed);
        }
        catch (AuthenticationException ex) {
            // 认证失败后的处理策略
            unsuccessfulAuthentication(request, response, ex);
        }
    }

    /**
     * 判断当前的请求是否需要进行认证
     * @return 需要认证就返回ture,不需要认证就返回false
     */
    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
        if (this.requiresAuthenticationRequestMatcher.matches(request)) {
            return true;
        }
        if (this.logger.isTraceEnabled()) {
            this.logger
                    .trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));
        }
        return false;
    }

    /**
     * 用于执行具体的认证流程,由子类进行实现
     * @return 经过身份验证的用户令牌,如果身份验证不完整,则为空
     * @throws AuthenticationException 认证失败将抛出异常
     */
    public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException;

    /**
     * 认证成功后的处理策略,子类可以重写此方法,以便在身份验证成功后继续FilterChain。
     *  1.将认证成功后的用户信息存入SecurityContextHolder
     *  2.登录成功后通知已配置的RememberMeServices
     *  3.通过配置的ApplicationEventPublisher触发一个InteractiveAuthenticationSuccessEvent
     *  4.将其他行为委托给AuthenticationSuccessHandler
     * @param authResult 方法attemptAuthentication返回的经过身份验证的用户令牌
     */
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authResult);

        // 1.将认证成功后的用户信息存入SecurityContextHolder,
        SecurityContextHolder.setContext(context);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
        }

        // 2.登录成功后通知已配置的RememberMeServices
        this.rememberMeServices.loginSuccess(request, response, authResult);
        if (this.eventPublisher != null) {
            // 3.通过配置的ApplicationEventPublisher触发一个InteractiveAuthenticationSuccessEvent
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }

        // 4.将其他行为委托给AuthenticationSuccessHandler
        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    }

    /**
     * 认证失败后的处理策略
     *  1.清除SecurityContextHolder
     *  2.将登录失败通知已配置的RememberMeServices
     *  3.将其他行为委托给AuthenticationFailureHandler
     */
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        this.logger.trace("Failed to process authentication request", failed);
        this.logger.trace("Cleared SecurityContextHolder");
        this.logger.trace("Handling authentication failure");
        this.rememberMeServices.loginFail(request, response);
        this.failureHandler.onAuthenticationFailure(request, response, failed);
    }
}

  从上面的源码可以看出AbstractAuthenticationProcessingFilter的主要作用就以下三点:

  • 判断当前请求是否需要认证
  • 如果当前请求需要认证,就调用attemptAuthentication来执行身份验证
  • 处理认证成功或失败相关事宜

​  从上面的分析可以看出,具体的认证逻辑都在其子类当中,接下来就来看看其子类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";

    /**
     * 默认情况下,只有当前请求的url为 “/login”,且http请求为post请求时进行认证
     */
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
            "POST");

    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

    private boolean postOnly = true;

    public UsernamePasswordAuthenticationFilter() {
        // 初始化父类的属性requiresAuthenticationRequestMatcher
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
        // 初始化父类的属性requiresAuthenticationRequestMatcher和authenticationManager
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            // 1.判断请求是不是POST请求,只有POST请求才进行认证
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }

        // 2.从请求中获取用户名
        String username = obtainUsername(request);
        username = (username != null) ? username : "";
        username = username.trim();

        // 3.从请求中获取密码
        String password = obtainPassword(request);
        password = (password != null) ? password : "";

        // 4.将用户名和密码封装为UsernamePasswordAuthenticationToken
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);

        // 5.为待认证的UsernamePasswordAuthenticationToken添加配置信息,扩展UsernamePasswordAuthenticationToken
        setDetails(request, authRequest);

        // 6.调用AuthenticationManager的authenticate进行认证
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    /**
     * 从请求中获取密码
     */
    @Nullable
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(this.passwordParameter);
    }

    /**
     * 从请求中获取用户名
     */
    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(this.usernameParameter);
    }

    /**
     * 为待认证的UsernamePasswordAuthenticationToken添加配置信息
     */
    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }
}

  从上面的源码可以看出,UsernamePasswordAuthenticationFilter做的事情其实非常简单,就是获取到请求参数后,将请求参数封装为对应的Authentication,并将Authentication交给AuthenticationManager进行认证。

2.1.2 ProviderManager

  从前面的分析可以看出,UsernamePasswordAuthenticationFilter只是简单的做了一些认证前的准备工作和认证后的善后工作,真正的认证流程其实是在AuthenticationManager当中,下面就来看一下其实现类ProviderManager是如何实现认证的。

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

    private static final Log logger = LogFactory.getLog(org.springframework.security.authentication.ProviderManager.class);

    private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();

    /**
     * 一系列AuthenticationProvider,不同的AuthenticationProvider认证不同的Authentication
     */
    private List<AuthenticationProvider> providers = Collections.emptyList();

    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

    /**
     * 父AuthenticationManager,通常情况下为ProviderManager
     */
    private AuthenticationManager parent;

    /**
     * 认证完成后是否要清楚凭据信息
     */
    private boolean eraseCredentialsAfterAuthentication = true;

    /**
     * 初始化providers
     */
    public ProviderManager(AuthenticationProvider... providers) {
        this(Arrays.asList(providers), null);
    }

    /**
     * 初始化providers
     */
    public ProviderManager(List<AuthenticationProvider> providers) {
        this(providers, null);
    }

    /**
     * 初始化providers和父AuthenticationManager
     */
    public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
        Assert.notNull(providers, "providers list cannot be null");
        this.providers = providers;
        this.parent = parent;
        checkState();
    }

    @Override
    public void afterPropertiesSet() {
        checkState();
    }

    private void checkState() {
        Assert.isTrue(this.parent != null || !this.providers.isEmpty(),
                "A parent AuthenticationManager or a list of AuthenticationProviders is required");
        Assert.isTrue(!CollectionUtils.contains(this.providers.iterator(), null),
                "providers list cannot contain null values");
    }

    /**
     * 对传入的Authentication对象进行身份验证。
     * @param authentication 待认证的Authentication对象
     * @return 认证完成后Authentication对象,包含用户权限信息
     * @throws AuthenticationException 认证失败将抛出异常
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        int currentPosition = 0;
        int size = this.providers.size();
        for (AuthenticationProvider provider : getProviders()) {
            // 1.遍历providers,判断当前的AuthenticationProvider是否支持传入的Authentication对象
            if (!provider.supports(toTest)) {
                continue;
            }
            if (logger.isTraceEnabled()) {
                logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
                        provider.getClass().getSimpleName(), ++currentPosition, size));
            }
            try {

                // 2.调用AuthenticationProvider的authenticate方法进行认证
                result = provider.authenticate(authentication);
                if (result != null) {
                    // 3.将身份验证请求的其他详细信息(例如IP地址,证书序列号等)拷贝到认证完成后Authentication对象中
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException | InternalAuthenticationServiceException ex) {
                prepareException(ex, authentication);
                // SEC-546: Avoid polling additional providers if auth failure is due to
                // invalid account status
                throw ex;
            }
            catch (AuthenticationException ex) {
                lastException = ex;
            }
        }

        // 4.认证结果结果为空,则交给父AuthenticationManager进行认证
        if (result == null && this.parent != null) {
            // Allow the parent to try.
            try {
                // 5.父AuthenticationManager开始认证,再次进入ProviderManager的authenticate方法
                parentResult = this.parent.authenticate(authentication);
                result = parentResult;
            }
            catch (ProviderNotFoundException ex) {
                // ignore as we will throw below if no other exception occurred prior to
                // calling parent and the parent
                // may throw ProviderNotFound even though a provider in the child already
                // handled the request
            }
            catch (AuthenticationException ex) {
                parentException = ex;
                lastException = ex;
            }
        }
        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
                // Authentication is complete. Remove credentials and other secret data
                // from authentication
                ((CredentialsContainer) result).eraseCredentials();
            }
            // If the parent AuthenticationManager was attempted and successful then it
            // will publish an AuthenticationSuccessEvent
            // This check prevents a duplicate AuthenticationSuccessEvent if the parent
            // AuthenticationManager already published it
            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }

            // 6.返回结果
            return result;
        }

        // Parent was null, or didn't authenticate (or throw an exception).
        if (lastException == null) {
            lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
        }
        // If the parent AuthenticationManager was attempted and failed then it will
        // publish an AbstractAuthenticationFailureEvent
        // This check prevents a duplicate AbstractAuthenticationFailureEvent if the
        // parent AuthenticationManager already published it
        if (parentException == null) {
            prepareException(lastException, authentication);
        }

        // 7.认证失败,抛出异常
        throw lastException;
    }

    @SuppressWarnings("deprecation")
    private void prepareException(AuthenticationException ex, Authentication auth) {
        this.eventPublisher.publishAuthenticationFailure(ex, auth);
    }

    /**
     * 将身份验证请求的其他详细信息(例如IP地址,证书序列号等)拷贝到认证完成后Authentication对象中
     */
    private void copyDetails(Authentication source, Authentication dest) {
        if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
            AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
            token.setDetails(source.getDetails());
        }
    }

    public List<AuthenticationProvider> getProviders() {
        return this.providers;
    }

    @Override
    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }

    public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
        Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");
        this.eventPublisher = eventPublisher;
    }
    
    public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) {
        this.eraseCredentialsAfterAuthentication = eraseSecretData;
    }

    public boolean isEraseCredentialsAfterAuthentication() {
        return this.eraseCredentialsAfterAuthentication;
    }

    private static final class NullEventPublisher implements AuthenticationEventPublisher {

        @Override
        public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
        }

        @Override
        public void publishAuthenticationSuccess(Authentication authentication) {
        }
    }
}

  从上面的源码可以看出AuthenticationManager做的事情也非常简单,就是找到合适的AuthenticationProvider进行认证,如果自己搞不定,就交给父AuthenticationManager来搞定,那么为什么要给每一个AuthenticationManager设置一个父AuthenticationManager,具体可以参阅官方文档

2.1.3 DaoAuthenticationProvider

  从前面的分析可以看出,AuthenticationManager实际上就相当于一个管理者,真正干活的其实还是AuthenticationProvider,事实上,UsernamePasswordAuthenticationFilterAuthenticationManager管理的AuthenticationProvider,其实并不是DaoAuthenticationProvider,而是AnonymousAuthenticationProvider,真正的DaoAuthenticationProvider其实是在其父AuthenticationManager中的。前面讲到过不同的AuthenticationProvider对不同的Authentication进行认证,而前面UsernamePasswordAuthenticationFilter封装的Authentication实现类是UsernamePasswordAuthenticationToken,只有DaoAuthenticationProvider支持这种类型的AuthenticationAnonymousAuthenticationProvider是不支持的,所以最终完成认证的是UsernamePasswordAuthenticationFilterAuthenticationManager的父AuthenticationManager,下面就来看看DaoAuthenticationProvider是如何进行认证的,首先看到它的父类AbstractUserDetailsAuthenticationProvider,因为其父类重写了authenticate方法。

public abstract class AbstractUserDetailsAuthenticationProvider
        implements AuthenticationProvider, InitializingBean, MessageSourceAware {

    protected final Log logger = LogFactory.getLog(getClass());

    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

    private UserCache userCache = new NullUserCache();

    private boolean forcePrincipalAsString = false;

    protected boolean hideUserNotFoundExceptions = true;

    private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();

    private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();

    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    /**
     * 此方法由子类进行实现,用于对用户信息进行一些额外的检查,例如密码检查
     * @param userDetails 服务器事先存储的用户信息
     * @param authentication 当前需要被认证的Authentication
     * @throws AuthenticationException 认证失败抛出异常
     */
    protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
                                                           UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;

    /**
     * 此方法由子类实现,用于加载服务器事先存储的用户信息
     * @param username 用户名
     * @param authentication 当前需要被认证的Authentication
     * @return 服务器事先存储的用户信息
     */
    protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException;

    @Override
    public final void afterPropertiesSet() throws Exception {
        Assert.notNull(this.userCache, "A user cache must be set");
        Assert.notNull(this.messages, "A message source must be set");
        doAfterPropertiesSet();
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 1.断言,当前AuthenticationProvider仅支持UsernamePasswordAuthenticationToken类型的Authentication
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));

        // 2.从Authentication中获取获取用户名
        String username = determineUsername(authentication);
        boolean cacheWasUsed = true;
        // 3.根据用户名从缓存中获取用户信息
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;
            try {
                // 4.从缓存中未获取到用户信息,从其它地方获取用户信息,此方法由子类实现
                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 {
            // 5.对用户信息进行一些预检查
            this.preAuthenticationChecks.check(user);
            // 6.对用户信息进行一些额外的检查(例如检查密码是否正确),此方法由子类进行实现
            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);
        }
        // 7.对用户信息进行一些后置检查
        this.postAuthenticationChecks.check(user);
        if (!cacheWasUsed) {
            // 8.缓存中不存在用户信息,将用户信息存入缓存
            this.userCache.putUserInCache(user);
        }
        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        // 8.创建验证通过后的Authentication,将其返回给调用者
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

    private String determineUsername(Authentication authentication) {
        return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
    }

    /**
     * 创建验证通过后的Authentication(此Authentication包含用户的权限信息)
     */
    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;
    }

    protected void doAfterPropertiesSet() throws Exception {
    }

    @Override
    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }

    /**
     * 判断当前的AuthenticationProvider是否支持传入的Authentication
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
    }

    private class DefaultPreAuthenticationChecks implements UserDetailsChecker {

        @Override
        public void check(UserDetails user) {
            if (!user.isAccountNonLocked()) {
                AbstractUserDetailsAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account is locked");
                throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
            }
            if (!user.isEnabled()) {
                AbstractUserDetailsAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account is disabled");
                throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
            }
            if (!user.isAccountNonExpired()) {
                AbstractUserDetailsAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account has expired");
                throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
            }
        }

    }

    private class DefaultPostAuthenticationChecks implements UserDetailsChecker {

        @Override
        public void check(UserDetails user) {
            if (!user.isCredentialsNonExpired()) {
                AbstractUserDetailsAuthenticationProvider.this.logger
                        .debug("Failed to authenticate since user account credentials have expired");
                throw new CredentialsExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired",
                                "User credentials have expired"));
            }
        }
    }
}

  从上面的源码可以看出,父类做的工作其实非常简单,主要就就两点:一是判断当前的AuthenticationProvider是否支持传入的Authentication,而是获取服务器存储的用户信息,对用户信息进行一些简单的检查工作。

  ​既然父类把具体的认证工作交给了子类的additionalAuthenticationChecks方法,那么接下来就来看看子类DaoAuthenticationProvider是如何完成认证的。

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

    @Override
    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
            // 调用UserDetailsService的loadUserByUsername方法获取服务器存储的用户信息,默认情况下实现类为InMemoryUserDetailsManager
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }
}

  从上面的源码当中,我们可以看出子类DaoAuthenticationProvider仅仅是利用PasswordEncoder做了密码的校验,并未做其它复杂的校验。

2.1.4 总结

  啊!怎么认证流程还是怎么复杂?其实总结起来,就是UsernamePasswordAuthenticationFilter将我们提交的用户信息封装为Authentication对象,此时的Authentication是没有权限信息的。然后,将Authentication对象交给内部的AuthenticationManager处理,AuthenticationManager一看,这我也不会处理呀,于是就去找AuthenticationProviderAuthenticationProvider那么多,找哪一个呢,肯定是根据不同的Authentication找不同的AuthenticationProvider,即调用每个AuthenticationProvidersupports方法,找到对应的AuthenticationProvider后就交给对应AuthenticationProviderAuthenticationProvider拿到对应的Authentication对象后,就进行具体的认证流程:找UserDetailsService拿到事先存储的用户信息和提交的Authentication中封装的用户信息进行比对,认证成功后,就重新生成一个认证通过后的Authentication对象,此时的Authentication对象中具有用户的权限信息,所以,整个认证流程的核心就是AuthenticationManagerAuthenticationProvider,两者之间的关系可参阅官方文档
ProviderManager

2.2 授权

上面讲完了认证流程,接下来讲一下授权流程,FilterSecurityInterceptor的大致授权流程如下图所示。
授权流程图

  • AbstractSecurityInterceptor:对用户的请求进行授权的抽象类,负责从SecurityContextHolder中获取Authentication,以及获取ConfigAttribute
  • FilterSecurityInterceptorAbstractSecurityInterceptor的实现类,负责拦截请求,将ServletRequestServletResponseFilterChain封装为FilterInvocation
  • FilterInvocation:保证请求和响应是HttpServletRequestHttpServletResponse的实例,供后面的授权使用。
  • ConfigAttribute:封装了用户要访问接口所需的权限信息。
  • AccessDecisionManager:根据AccessDecisionVoter的投票结果判断是否允许用户访问指定的接口。
  • AffirmativeBasedAccessDecisionManager的简单实现,通过轮询其管理的所有AccessDecisionVoter,只要有一个AccessDecisionVoter投赞成票或反对票,本次就允许或禁止用户访问接口。
  • AccessDecisionVoter:对用户的本次访问进行投票,最终的投票结果由AccessDecisionVoter收集,并做出最终的访问决策。
  • WebExpressionVoterAccessDecisionVoter的实现类,基于SpEL(Spring 表达式语言)的AccessDecisionVoter

2.2.1 FilterSecurityInterceptor

  FilterSecurityInterceptor作为过滤器链中最后一个Filter,起着至关重要的作用,决定着本次请求能否访问目标资源,接下来就来看看FilterSecurityInterceptor是如何授权的。

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

    // 用于标识每个请求进行过授权
	private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";

    // 用于确定请求的安全属性,包括可访问的URL、所需的权限、角色等,用于后续的访问决策和权限验证
	private FilterInvocationSecurityMetadataSource securityMetadataSource;

    // 标识每个请求是否只进行一次授权
	private boolean observeOncePerRequest = true;
    
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
        // 将request、response、chain封装为FilterInvocation,保证当前请求是HTTP请求
		invoke(new FilterInvocation(request, response, chain));
	}
    
    public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
        // 判断当前请求是否已经授权过,防止重复检查
		if (isApplied(filterInvocation) && this.observeOncePerRequest) {
			// filter already applied to this request and user wants us to observe
			// once-per-request handling, so don't re-do security checking
			filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
			return;
		}
		// first time this request being called, so perform security checking
		if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
			filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
		}
        // 开始授权
		InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
		try {
			filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
		}
		finally {
			super.finallyInvocation(token);
		}
		super.afterInvocation(token, null);
	}
    
    private boolean isApplied(FilterInvocation filterInvocation) {
		return (filterInvocation.getRequest() != null)
				&& (filterInvocation.getRequest().getAttribute(FILTER_APPLIED) != null);
	}

	public boolean isObserveOncePerRequest() {
		return this.observeOncePerRequest;
	}

	public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
		return this.securityMetadataSource;
	}

	@Override
	public SecurityMetadataSource obtainSecurityMetadataSource() {
		return this.securityMetadataSource;
	}

	public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
		this.securityMetadataSource = newSource;
	}
}

  从上面可以看出,FilterSecurityInterceptor只是做了一些准备工作和善后工作,真正的授权流程在其父类的beforeInvocation方法中,下面就来看看父类是如何进行授权的。

public abstract class AbstractSecurityInterceptor
        implements InitializingBean, ApplicationEventPublisherAware, MessageSourceAware {

    protected final Log logger = LogFactory.getLog(getClass());

    /**
     * 决定是否授权用户访问目标资源
     */
    private AccessDecisionManager accessDecisionManager;

    /**
     * 是否拒绝公共请求
     */
    private boolean rejectPublicInvocations = false;

    protected InterceptorStatusToken beforeInvocation(Object object) {
        Assert.notNull(object, "Object was null");
        // 1.判断当前传入对象的类型是否为FilterInvocation
        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());
        }
        // 2.根据传入的对象获取要访问资源所需的权限或角色信息
        Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
        if (CollectionUtils.isEmpty(attributes)) {
            // 3.若未获取要访问资源所需的权限或角色信息,认为当前的请求为公共请求,若拒绝策略为true,则抛出异常,即当前请求需要配置权限信息
            Assert.isTrue(!this.rejectPublicInvocations,
                    () -> "Secure object invocation " + object
                            + " was denied as public invocations are not allowed via this interceptor. "
                            + "This indicates a configuration error because the "
                            + "rejectPublicInvocations property is set to 'true'");
            if (this.logger.isDebugEnabled()) {
                this.logger.debug(LogMessage.format("Authorized public object %s", object));
            }
            publishEvent(new PublicInvocationEvent(object));
            return null; // no further work post-invocation
        }
        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound",
                    "An Authentication object was not found in the SecurityContext"), object, attributes);
        }
        // 4.判断当前用户是否已经经过身份认证
        Authentication authenticated = authenticateIfRequired();
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(LogMessage.format("Authorizing %s with attributes %s", object, attributes));
        }
        // 5、开始授权
        attemptAuthorization(object, attributes, authenticated);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Authorized %s with attributes %s", object, attributes));
        }
        if (this.publishAuthorizationSuccess) {
            publishEvent(new AuthorizedEvent(object, attributes, authenticated));
        }

        // Attempt to run as a different user
        Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
        if (runAs != null) {
            SecurityContext origCtx = SecurityContextHolder.getContext();
            SecurityContext newCtx = SecurityContextHolder.createEmptyContext();
            newCtx.setAuthentication(runAs);
            SecurityContextHolder.setContext(newCtx);

            if (this.logger.isDebugEnabled()) {
                this.logger.debug(LogMessage.format("Switched to RunAs authentication %s", runAs));
            }
            // need to revert to token.Authenticated post-invocation
            return new InterceptorStatusToken(origCtx, true, attributes, object);
        }
        this.logger.trace("Did not switch RunAs authentication since RunAsManager returned null");
        // no further work post-invocation
        return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);

    }

    /**
     * 授权方法,委托AccessDecisionManager进行授权
     * @param object FilterInvocation
     * @param attributes 要访问资源所需的权限或角色信息
     * @param authenticated 经过身份认证的Authentication对象,里面包含用户具有的权限信息
     */
    private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,
                                      Authentication authenticated) {
        try {
            this.accessDecisionManager.decide(authenticated, object, attributes);
        }
        catch (AccessDeniedException ex) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object,
                        attributes, this.accessDecisionManager));
            }
            else if (this.logger.isDebugEnabled()) {
                this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes));
            }
            publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));
            throw ex;
        }
    }
    
    /**
     * 判断当前用户是否已经经过身份认证,若未经过身份认证,再用AuthenticationManager认证一遍
     * @return 经过身份认证的Authentication对象
     */
    private Authentication authenticateIfRequired() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication.isAuthenticated() && !this.alwaysReauthenticate) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace(LogMessage.format("Did not re-authenticate %s before authorizing", authentication));
            }
            return authentication;
        }
        authentication = this.authenticationManager.authenticate(authentication);
        // Don't authenticated.setAuthentication(true) because each provider does that
        if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Re-authenticated %s before authorizing", authentication));
        }
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);
        return authentication;
    }
}

  从上面的源码可以看出,FilterSecurityInterceptor和前面的UsernamePasswordAuthenticationFilter一样,都是先做一些准备工作,最后再把具体的认证逻辑或授权逻辑交给具体的AuthenticationManagerAccessDecisionManager,最后再做一些善后处理工作。

2.2.2 AffirmativeBased

  在默认情况下中,AccessDecisionManager的默认实现类是AffirmativeBased,接下来就来看一下AffirmativeBased是如何进行授权的。

public class AffirmativeBased extends AbstractAccessDecisionManager {

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

    /**
     * 简单地轮询所有配置的AccessDecisionVoter,并在任何AccessDecisionVoter投了肯定票时授予访问权。拒绝访问只有当有拒绝投票和没有肯定的投票。
     * 如果每个AccessDecisionVoter都放弃投票,则决定将基于isAllowIfAllAbstainDecisions()属性(默认为false)
     * @param authentication 经过身份认证的Authentication对象,里面包含用户具有的权限信息
     * @param object FilterInvocation
     * @param configAttributes 要访问资源所需的权限或角色信息
     * @throws AccessDeniedException 拒绝访问抛出异常
     */
    @Override
    @SuppressWarnings({ "rawtypes", "unchecked" })
    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(
                    this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
        }
        // To get this far, every AccessDecisionVoter abstained
        checkAllowIfAllAbstainDecisions();
    }
}

  从上面的源码可以看出,AffirmativeBased只是简单的轮询每个AccessDecisionVoter,根据它们的投票结果做出访问决策,只要其中有一个AccessDecisionVoter投了赞成票时就允许访问,反之,只要有一个投反对票就不允许访问。

2.2.3 WebExpressionVoter

  在Spring Security中,AccessDecisionVoter最常见的实现类是WebExpressionVoter,这是一种根据SpEL表达式来做出投票结果的AccessDecisionVoter,关于SpEL表达式,大家感兴趣的话可以参阅官方文档,当然,还有其它AccessDecisionVoter,大家可以自行研究。

2.2.4 总结

  从前面的流程图和分析不难看出,整个授权流程的核心其实是AccessDecisionManagerAccessDecisionVoterAccessDecisionManager管理着一系列AccessDecisionVoter,并根据每一个AccessDecisionVoter的投票结果做出最终决策。每一个AccessDecisionVoter通过用户所具有的权限信息(前面的认证流程已经将用户的权限信息存入Authentication)和待访问接口所需的权限信息(ConfigAttribute)进行比较,做出自己的投票结果。不难看出,其实AccessDecisionManagerAccessDecisionVoter的关系有点类似与前面认证流程中AuthenticationManagerAuthenticationProvider的关系。

2.3 异常

  当然,前面所讲述的只是正常情况下认证和授权流程,那么当认证和授权过程中出现异常,Spring Security又是如何处理的呢?下面就来看一下Spring Security中的异常处理机制。前面已经讲到过,UsernamePasswordAuthenticationFilter在认证过程中如果出现异常会调用unsuccessfulAuthentication方法,将异常交给AuthenticationFailureHandler进行处理,那么,如果当前请求不需要认证,在整个过滤器链中执行的过程中如果出现异常,Spring Security是如何进行处理的呢?接下来请看另一个FilterExceptionTranslationFilter,这个过滤器被用来处理过滤器链中抛出的任何AccessDeniedExceptionAuthenticationException。下面就来看看这个过滤器是如何处理异常的。

public class ExceptionTranslationFilter extends GenericFilterBean implements MessageSourceAware {

    /**
     * 用于处理AccessDeniedException
     */
    private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();

    /**
     * 用于处理AuthenticationException
     */
    private AuthenticationEntryPoint authenticationEntryPoint;

    /**
     * 用户判断当前用户是不是匿名用户或已经记住的用户
     */
    private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();

    /***
     * 分析当前的异常中是否包含AccessDeniedException或AuthenticationException
     */
    private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();

    private RequestCache requestCache = new HttpSessionRequestCache();

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
    }

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        try {
            chain.doFilter(request, response);
        }
        catch (IOException ex) {
            throw ex;
        }
        catch (Exception ex) {
            // Try to extract a SpringSecurityException from the stacktrace
            Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
            RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
                    .getFirstThrowableOfType(AuthenticationException.class, causeChain);
            if (securityException == null) {
                securityException = (AccessDeniedException) this.throwableAnalyzer
                        .getFirstThrowableOfType(AccessDeniedException.class, causeChain);
            }
            if (securityException == null) {
                // 当前异常信息中既不包括AuthenticationException,也不包括AccessDeniedException
                rethrow(ex);
            }
            if (response.isCommitted()) {
                throw new ServletException("Unable to handle the Spring Security Exception "
                        + "because the response is already committed.", ex);
            }
            // 开始处理异常
            handleSpringSecurityException(request, response, chain, securityException);
        }
    }

    private void rethrow(Exception ex) throws ServletException {
        // Rethrow ServletExceptions and RuntimeExceptions as-is
        if (ex instanceof ServletException) {
            throw (ServletException) ex;
        }
        if (ex instanceof RuntimeException) {
            throw (RuntimeException) ex;
        }
        // Wrap other Exceptions. This shouldn't actually happen
        // as we've already covered all the possibilities for doFilter
        throw new RuntimeException(ex);
    }

    private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
                                               FilterChain chain, RuntimeException exception) throws IOException, ServletException {
        if (exception instanceof AuthenticationException) {
            handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
        }
        else if (exception instanceof AccessDeniedException) {
            handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
        }
    }

    private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response,
                                               FilterChain chain, AuthenticationException exception) throws ServletException, IOException {
        this.logger.trace("Sending to authentication entry point since authentication failed", exception);
        sendStartAuthentication(request, response, chain, exception);
    }

    private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
                                             FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
        if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
            // 当前的用户是匿名用户或已经记住的用户
            if (logger.isTraceEnabled()) {
                logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied",
                        authentication), exception);
            }
            sendStartAuthentication(request, response, chain,
                    new InsufficientAuthenticationException(
                            this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
                                    "Full authentication is required to access this resource")));
        }
        else {
            if (logger.isTraceEnabled()) {
                logger.trace(
                        LogMessage.format("Sending %s to access denied handler since access is denied", authentication),
                        exception);
            }
            // 交给对应的AccessDeniedHandler处理AccessDeniedException
            this.accessDeniedHandler.handle(request, response, exception);
        }
    }

    protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                           AuthenticationException reason) throws ServletException, IOException {
        // SEC-112: Clear the SecurityContextHolder's Authentication, as the
        // existing Authentication is no longer considered valid
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        SecurityContextHolder.setContext(context);
        this.requestCache.saveRequest(request, response);
        // 交给对应的AuthenticationEntryPoint处理AuthenticationException
        this.authenticationEntryPoint.commence(request, response, reason);
    }

    private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer {
        @Override
        protected void initExtractorMap() {
            super.initExtractorMap();
            registerExtractor(ServletException.class, (throwable) -> {
                ThrowableAnalyzer.verifyThrowableHierarchy(throwable, ServletException.class);
                return ((ServletException) throwable).getRootCause();
            });
        }
    }
}

  通过上面的源码分析可以看出,ExceptionTranslationFilter把过滤器链中出现的AuthenticationException交给了AuthenticationEntryPoint处理,AccessDeniedException交给了AccessDeniedHandler处理。无论是UsernamePasswordAuthenticationFilter中的AuthenticationFailureHandler,还是这里的AuthenticationEntryPointAccessDeniedHandler,都是一个接口,我们都可以自定义其实现类,自定义异常处理逻辑。

2.4 SecurityContextHolder

  从前面的分析可以看出,无论是前面的认证和授权,还是异常处理,都或多或少用到了SecurityContextHolder,前面说过SecurityContextHolder存储着SecurityContext,而SecurityContext中存储着AuthenticationAuthentication中又存储着用户的相关信息,那么接下来就来看一下SecurityContextHolder是如何存储SecurityContext的。这里就不贴源码了,直接给出结论,SecurityContextHolder中默认存储策略是ThreadLocalSecurityContextHolderStrategy,就是将SecurityContext存储在ThreadLocal,方便当前线程在任何地方取出Authentication使用。

​  前面在讲解UsernamePasswordAuthenticationFilter时说过,认证成功后的Authentication会被存入SecurityContextHolder,那么本次请求后面的所有Filter都可以获取到认证成功后的用户信息。认证成功后,后面的请求都不需要认证了,那么UsernamePasswordAuthenticationFilter就不会将Authentication存入SecurityContextHolder,此时,后面的Filter是如何从SecurityContextHolder获取Authentication的呢?Authentication又是何时被存入SecurityContextHolder的呢?接下来就得说一下另一个非常重要的FilterSecurityContextPersistenceFilter

public class SecurityContextPersistenceFilter extends GenericFilterBean {

    // 标识当前Filter已经处理过该请求
	static final String FILTER_APPLIED = "__spring_security_scpf_applied";

    // 请求之间持久化SecurityContext的策略
	private SecurityContextRepository repo;

    // 是否强制创建session
	private boolean forceEagerSessionCreation = false;

	public SecurityContextPersistenceFilter() {
        // 初始化repo,默认情况下为基于HttpSession的存储策略
		this(new HttpSessionSecurityContextRepository());
	}

	public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
		this.repo = repo;
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
	}

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		// ensure that filter is only applied once per request
		if (request.getAttribute(FILTER_APPLIED) != null) {
			chain.doFilter(request, response);
			return;
		}
		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
		if (this.forceEagerSessionCreation) {
			HttpSession session = request.getSession();
			if (this.logger.isDebugEnabled() && session.isNew()) {
				this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
			}
		}
		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
        
        // 从session中获取SecurityContext
		SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
		try {
            // 将SecurityContext存入SecurityContextHolder
			SecurityContextHolder.setContext(contextBeforeChainExecution);
			if (contextBeforeChainExecution.getAuthentication() == null) {
				logger.debug("Set SecurityContextHolder to empty SecurityContext");
			}
			else {
				if (this.logger.isDebugEnabled()) {
					this.logger
							.debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));
				}
			}
			chain.doFilter(holder.getRequest(), holder.getResponse());
		}
		finally {
			SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
			// Crucial removal of SecurityContextHolder contents before anything else.
			SecurityContextHolder.clearContext();
            // 在整个过滤器链执行完成后,重新将SecurityContext存入session,此时的SecurityContext可能存储的是认证成功后的用户信息
			this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
			request.removeAttribute(FILTER_APPLIED);
			this.logger.debug("Cleared SecurityContextHolder to complete request");
		}
	}

	public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
		this.forceEagerSessionCreation = forceEagerSessionCreation;
	}
}

  从上面的源码可以看出,每次请求SecurityContextPersistenceFilter都会从session中取出SecurityContext存入SecurityContextHolder,在整个过滤器执行完成后又从SecurityContextHolder中取出SecurityContext,再将SecurityContext存入session,那样,认证完成后的Authentication都会被存入session中,后面每次访问再重新取出SecurityContext存入SecurityContextHolder,后面授权的时候就可以取出用户信息。至于SecurityContextRepository是如何将SecurityContext存入session的,又是如何从session中取出SecurityContext的,大家可以自行去了解。

3. 过滤器链SecurityFilterChain

  前面讲完了Spring Security的默认认证和授权流程,现在就来填一下前面的坑,讨论一下过滤器链是如何产生的,以及过滤器链中一些重要过滤器的作用。我们都知道Spring Boot的厉害之处,就在于它的自动装配,那么这过滤器链不可能凭空产生,肯定是由Spring Boot自动装配产生的。首先,我们找到自动装配的配置文件spring.factories,如下图所示。
spring.factories
spring.factories中找到名为SecurityAutoConfiguration的配置类,这就是Spring Security的自动配置类。
SecurityAutoConfiguration
  我们可以看到这个自动配置类生效的条件是类加载路径下存在名为DefaultAuthenticationEventPublisher的类,这个毋庸置疑,肯定是成立的,当我们引入依赖的那一刻,这个类就存在了。下面我们重点关注@Import导入的两个配置类,SpringBootWebSecurityConfigurationWebSecurityEnablerConfiguration,这两个类至关重要。

3.1 SpringBootWebSecurityConfiguration

首先,我们看到这个名为SpringBootWebSecurityConfiguration的配置类,如下图所示。
SpringBootWebSecurityConfiguration
  我们看到这个配置类的注释,其大概意思是说这个配置类是web安全的默认配置,如果用户配置了自己的WebSecurityConfigurerAdapterSecurityFilterChain bean,这个配置类就不会生效,那么这解释了为什么我们集成Spring Security时要自己写一个类继承WebSecurityConfigurerAdapter作为Spring Security的配置类。我们可以看到在这个配置类中只有一个名为defaultSecurityFilterChain的bean,这个bean的类型就是SecurityFilterChain,是不是很熟悉?没错,这就是Spring Security为我们配置的默认FilterChain,这也是我们前面为什么会用那段代码获取所有的Filter。这时可能有人就会问了,前面的代码不是DefaultSecurityFilterChain吗?事实上,只要我们进入http.build()一探究竟,就会发现DefaultSecurityFilterChain就是SecurityFilterChain的实现类,而这个方法所需要的参数是一个HttpSecurity对象,而这个参数会由其它配置类产生自动注入到这里。

​  我们可以看到这个配置类生效的条件是使用默认的WebSecurity配置,且当前的应用程序是一个web应用程序,我们进入@ConditionalOnDefaultWebSecurity,如下图所示。
ConditionalOnDefaultWebSecurity
  然后我们进入DefaultWebSecurityCondition,如下图所示。
DefaultWebSecurityCondition
  我们可以看到SpringBootWebSecurityConfiguration生效的条件就在这个类里面,即类加载路径下有SecurityFilterChainHttpSecurity这两个类,且容器中不存在类型为WebSecurityConfigurerAdapterSecurityFilterChain的bean,我们在集成Spring Security时,什么都没配置,容器中肯定是不会存在这两个bean的。

3.2 WebSecurityEnablerConfiguration

接下来,看到名为WebSecurityEnablerConfiguration的配置类,如下图所示。
WebSecurityEnablerConfiguration
  首先看到这个配置类的注释,其大概意思是说,这个配置类的目的是确保引入注解@EnableWebSecurity,如果用户自己添加了该注解或者容器中已经存在名为springSecurityFilterChain的bean,那么这个配置类就不会生效。一句话,这个配置类的目的就是引入注解@EnableWebSecurity,所以,这个注解才是关键。

​  接下来,我们进入@EnableWebSecurity,如下图所示。
EnableWebSecurity
  我们重点关注@Import导入的两个配置类,WebSecurityConfigurationHttpSecurityConfiguration,这两个类至关重要。

3.2.1 WebSecurityConfiguration

  WebSecurityConfiguration,顾名思义,就是WebSecurity的配置类,至于这个WebSecurity是什么?有什么作用?我们后面再做详细讨论。首先我们看到WebSecurityConfiguration的注释,如下图所示。
WebSecurityConfiguration注释
  这个注释的大概意思是说这个配置类的作用是使用WebSecurity创建一个FilterChainProxy,而FilterChainProxy与Web安全至关重要,至于这个FilterChainProxy是什么?有什么作用?这个我们后面再做详细讨论,现在只需知道前面的SecurityFilterChain是由它管理的。注释后面还说我们可以通过继承WebSecurityConfigurerAdapter或是实现WebSecurityConfigurer的方式对WebSecurity进行个性化配置,欸!这里又提到了WebSecurityConfigurerAdapter,说明WebSecurityConfigurerAdapterSpring Security中如此重要,至于它和WebSecurity是什么关系,我们后面再做详细讨论。
  我们看到创建FilterChainProxy的方法,如下图所示。
创建FilterChainProxy
  首先看到方法的注释,方法的注释说这个方法返回的是一个代表SecurityFilterChainFilter,哦!原来FilterChainProxy也是一个Filter呀!而且它还管理着SecurityFilterChain。再看这个方法产生的Bean的名字是springSecurityFilterChain,也就是到时可以通过这个名字获取FilterChainProxy。再看这个方法的前面三句,前面有个断言,大概意思是说,WebSecurityConfigurerAdapterSecurityFilterChain只能存在其一,而这个SecurityFilterChain可由前面的SpringBootWebSecurityConfiguration产生,在这个方法下面就有一个set方法将容器中的所有SecurityFilterChain注入到这个配置类中,为什么这两个只能存在一个呢?这两者之间又是什么关系呢?这个我们后文再做讨论。继续往后看,如果这两个都没有的话,Spring Security还贴心的为我们添加了一个WebSecurityConfigurerAdapter,将WebSecurityConfigurerAdapter添加到WebSecurity,如果存在SecurityFilterChain的话,还将为WebSecurity添加一个SecurityFilterChainBuilder,然后遍历所有的WebSecurityCustomizerWebSecurity进行个性化配置,最后调用WebSecuritybuild方法来创建FilterChainProxy。看来WebSecurity创建FilterChainProxy需要一个WebSecurityConfigurerAdapterSecurityFilterChain,那么这个WebSecurity从何而来,接下来看到这个配置类的另一个方法setFilterChainProxySecurityConfigurer,如下图所示。
setFilterChainProxySecurityConfigurer
  我们可以看到这个方法需要两个参数:ObjectPostProcessorList<SecurityConfigurer<Filter, WebSecurity>>。首先说一下这两个参数从何而来,还记得前面的注解@EnableWebSecurity吗?ObjectPostProcessor就是由@EnableWebSecurity上面另一个注解@EnableGlobalAuthentication产生的,至于这个ObjectPostProcessor的具体作用,现在我们不去过多深究,只需要知道它会对创建的对象做一些增强的逻辑;再看到另一个参数,这个参数需要容器中所有SecurityConfigurer<Filter, WebSecurity>的实现类(实际上前面的注解限制了为SecurityConfigurer的子接口WebSecurityConfigurer的实现类),而WebSecurityConfigurerAdapter就是其中的一个实现类。这个方法的主要作用就是创建WebSecurity,然后获取容器中的所有WebSecurityConfigurer,为其configurers赋值(即调用WebSecurityapply方法)。

3.2.2 HttpSecurityConfiguration

接下来看到HttpSecurityConfiguration,首先看到这个配置类的注释,如下图所示。
HttpSecurityConfiguration注释
  从注释可以看出,这个配置类的主要作用就是创建HttpSecurity,这个HttpSecurity是什么,有什么作用,相信配置过WebSecurityConfigurerAdapter的并不陌生,这个HttpSecurity有什么作用?与WebSecurity又有什么关系?我们后面再做详细讨论,下面看到创建HttpSecurity的方法。
创建HttpSecurity
  注意看,这个Bean是多例的,并不是单例的,也就是说这个Bean并不会提前产生,只会在需要用到的时候产生。还记得前面的SpringBootWebSecurityConfiguration吗?那里就需要一个HttpSecurity,这里就是Spring Security做的一系列默认配置,可以看到已经添加了两个Filter,其它的都使用了默认的配置。

3.3 FilterChainProxy

  说到FilterChainProxy,就不得不提到Spring Security的整体架构,FilterChainProxySpring Security中扮演者极其重要的角色,具体可以参阅Spring Security官方文档
首先来看下面这张图。
DelegatingFilterProxy
官网对这张图的解释是:
  Spring provides a Filter implementation named DelegatingFilterProxy that allows bridging between the Servlet container’s lifecycle and Spring’s ApplicationContext. The Servlet container allows registering Filters using its own standards, but it is not aware of Spring defined Beans. DelegatingFilterProxy can be registered via standard Servlet container mechanisms, but delegate all the work to a Spring Bean that implements Filter.

  我们都知道服务器在启动后会在Servlet容器中注册一系列Filter在请求到达服务器时,会经过这些Filter,而Servlet容器并不知道Spring Security中的Filter,因为这些Filter都在SpringApplicationContext中,为了能让Spring Security中的Filter处理请求,就设计了一个DelegatingFilterProxy,将其注册到Servlet容器中,负责拦截请求,并将所有的工作委派给Spring Security中的Filter,但是Spring Security中的Filter数量众多,这时就需要一个FilterChain,即前面SpringBootWebSecurityConfiguration创建的SecurityFilterChain,而SecurityFilterChain就是由HttpSecurity创建的。这是就需要引出我们的主角了:FilterChainProxy,咦!怎么是FilterChainProxy?不是SecurityFilterChain吗?别急,看下面这两张图.。
SecurityFilterChain
Multiple SecurityFilterChain
  从图中可以看出DelegatingFilterProxy中的delegate就是FilterChainProxy,而FilterChainProxy中管理着一系列SecurityFilterChainSecurityFilterChain中又管理着一系列Filter,这些Filter正是SpringApplicationContext中的Filter,为什么FilterChainProxy要管理着众多SecurityFilterChain呢?看右边的图就知道了,不同SecurityFilterChain针对不同的路径,也就是说我们可以在项目中根据不同的路径配置不同的SecurityFilterChain,不同的路径具有不同的校验规则,而FilterChainProxy会根据不同的路径选取不同的SecurityFilterChain

3.4 总结

  相信前面讲了那么多,大家都已经绕晕了吧,别急,我们先来捋捋这个过程。

​  首先,既然我们要实现认证和授权,那就需要Filter,而这些一个Filter又无法搞定,需要很多的Filter,不同的Filter各司其职,共同完成认证和授权,保护我们的Web应用程序。那么,如何让这些Filter协调起来,有效工作呢?没错,此时就需要FilterChain(即SecurityFilterChain)。但是,我们如果想要更加精细化的配置,让不同的路径具有的校验规则,怎么办呢?这时就需要多个SecurityFilterChain,不同的SecurityFilterChain针对不同的路径。

​  其次,当请求到达服务器时,怎么多的SecurityFilterChain,该用哪一个呢,那肯定的做出决策呀!这时,就需要FilterChainProxy,让它来管理众多的SecurityFilterChain,根据不同的请求路径使用不同的SecurityFilterChain

​  最后,我们有这么多Filter,有用吗?没用啊!为什么没用?因为Servlet容器不知道啊,那么怎样才能让Servlet容器知道呢?那就弄个DelegatingFilterProxy,将它注册到Servlet容器,让它将请求传递给我们的FilterChainProxy,这样我们创建的那些Filter就可以正常工作了。

​  那么,,Spring Security整这么麻烦干什么呢?直接将那些Filter注册到Servlet容器不更香吗?这个问题就留给各位自己思考了。

4. HttpSecurity和WebSecurity

​  前面已经介绍完了Spring Security的整体架构,想必大家都已经知道了Spring Security的两大核心:FilterChainProxySecurityFilterChain。下面就来介绍一下这两大核心的缔造者:WebSecurityHttpSecurity

​​  首先来回顾一下WebSecurityHttpSecurity是如何创建FilterChainProxySecurityFilterChain。前面已经讲到到过WebSecurityHttpSecurity都是经过一系列配置后,最后调用build方法创建FilterChainProxySecurityFilterChain,那么调用build方法都发生了什么?FilterChainProxySecurityFilterChain是如何被创建出来的?下面我们就来一探究竟。

​  先来看一下这两个类的类继承关系图,如下图所示。
类继承关系图
  我们可以看到这两个类都有一个相同的类继承关系:SecurityBuilder–>AbstractSecurityBuilder–>AbstractConfiguredSecurityBuilder。无论是WebSecurity,还是HttpSecurity,其实都是一个SecurityBuilder,这个接口中只有一个build方法,通过调用build方法构建我们想要的对象,这其实就是设计模式中建造者模式在Spring Security中的具体体现。当我们调用build方法,其调用流程如下图所示。
build流程图
  首先看到AbstractConfiguredSecurityBuilderdoBuild方法,如下图所示。
doBuild
  我们可以看到beforeInitbeforeConfigureperformBuild方法,AbstractConfiguredSecurityBuilder都未对其进行实现,而是由子类自行实现,那么子类就可以在initconfigure之前,进行自己的个性化配置。在一切准备工作完成后,最后,再调用子类的performBuild方法,构建具体的对象。我们再看到另外两个方法:initconfigure
init
  可以看到这两个方法都是调用SecurityConfigurerinitconfigure,对当前的SecurityBuilder进行一系列配置操作,那么SecurityBuilder在构建对象的过程中,重点研究对象便是这些SecurityConfigurer(init方法和configure方法都干了什么)和留给子类实现的三个方法。我们看到接口SecurityConfigurer,如下图所示。
SecurityConfigurer
  我们可以看见这个这个SecurityConfigurer是一个泛型接口,这个接口中定义了两个方法来对SecurityBuilder做一些必要的配置,至于这些重要的配置是什么,我们后文再做详细讨论。这个接口的泛型参数O代表SecurityBuilder要构建的目标对象,而泛型参数B代表构建目标对象的SecurityBuilder,我们可以通过这两个参数清晰的知道当前SecurityConfigurer是配置的哪一个SecurityBuilder,以及这个SecurityBuilder构建的目标对象是什么。

4.1 HttpSecurity

​  首先我们看到HttpSecurity,如下图所示。
HttpSecurity
​  我们看到HttpSecurity的定义,继承了AbstractConfiguredSecurityBuilder(其中的泛型参数和SecurityConfigurer一样),而其中的泛型参数ODefaultSecurityFilterChain,泛型参数BHttpSecurity,也就是说HttpSecurity是一个构建DefaultSecurityFilterChainSecurityBuilder

4.1.1 默认配置

​  在HttpSecurity的源码当中,HttpSecurity并未实现beforeInit,只是实现了beforeConfigureperformBuild,下面我们就来看看这两个方法里面到底发生了什么,以及在默认情况下HttpSecurity中有哪些SecurityConfigurer
​  要研究HttpSecurity中的SecurityConfigurer,就得知道这些SecurityConfigurer从何而来,还记得前面讲解HttpSecurityConfiguration中那段默认配置的代码和SpringBootWebSecurityConfiguration中创建DefaultSecurityFilterChain那段代码吗?Spring Security在这两处为HttpSecurity设置了默认的SecurityConfigurer,下面就来具体看这两处代码。

// HttpSecurityConfiguration
http
			.csrf(withDefaults())								// 1.添加CsrfConfigurer,并采用默认配置
			.addFilter(new WebAsyncManagerIntegrationFilter())	// 直接添加filter
			.exceptionHandling(withDefaults())					// 2.添加ExceptionHandlingConfigurer,并采用默认配置
			.headers(withDefaults())							// 3.添加HeadersConfigurer,并采用默认配置
			.sessionManagement(withDefaults())					// 4.添加SessionManagementConfigurer,并采用默认配置
			.securityContext(withDefaults())					// 5.添加SecurityContextConfigurer,并采用默认配置
			.requestCache(withDefaults())						// 6.添加RequestCacheConfigurer,并采用默认配置
			.anonymous(withDefaults())							// 7.添加AnonymousConfigurer,并采用默认配置
			.servletApi(withDefaults())							// 8.添加ServletApiConfigurer,并采用默认配置
			.apply(new DefaultLoginPageConfigurer<>());			// 9.添加DefaultLoginPageConfigurer,并采用默认配置
		http.logout(withDefaults());							// 10.添加LogoutConfigurer,并采用默认配置


// SpringBootWebSecurityConfiguration
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()		// 11.添加ExpressionUrlAuthorizationConfigurer
        .anyRequest()				// ExpressionUrlAuthorizationConfigurer的配置信息
        .authenticated()			// ExpressionUrlAuthorizationConfigurer的配置信息
        .and()						// 返回当前的SecurityBuilder,即HttpSecurity
        .formLogin()				// 12.添加FormLoginConfigurer,并采用默认配置
        .and()						// 返回当前的SecurityBuilder,即HttpSecurity
        .httpBasic();				// 13.添加HttpBasicConfigurer,并采用默认配置
    
    // 构建DefaultSecurityFilterChain
    return http.build();
}

​  从上面的代码可以看出,在默认情况下,Spring Security为我们添加了13个SecurityConfigurer和一个Filter,那么这些SecurityConfigurer是如何添加到HttpSecurity当中的呢?我们可以发现无论是HttpSecurityConfiguration中的配置方式,还是SpringBootWebSecurityConfiguration中的配置方式,最终都是经过了getOrApplyapply这两个方法。

public HttpSecurity csrf(Customizer<CsrfConfigurer<HttpSecurity>> csrfCustomizer) throws Exception {
    ApplicationContext context = getContext();
    // 使用传入的Customizer个性化配置当前的SecurityConfigurer
    csrfCustomizer.customize(getOrApply(new CsrfConfigurer<>(context)));
    return HttpSecurity.this;
}

public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
    ApplicationContext context = getContext();
    return getOrApply(new CsrfConfigurer<>(context));
}

private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply(C configurer)
    throws Exception {
    C existingConfig = (C) getConfigurer(configurer.getClass());
    if (existingConfig != null) {
        return existingConfig;
    }
    return apply(configurer);
}

public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer) throws Exception {
    configurer.addObjectPostProcessor(this.objectPostProcessor);
    // 告知当前SecurityConfigurer要配置的SecurityBuilder是谁
    configurer.setBuilder((B) this);
    add(configurer);
    return configurer;
}

  从上面可以看出,我们有两种配置方式可以个性化配置当前的SecurityConfigurer:一是像HttpSecurityConfiguration中那样传入一个Customizer,二是像SpringBootWebSecurityConfiguration中那样先添加对应的SecurityConfigurer,然后再调用SecurityConfigurer对应的方法进行个性化配置,若要想配置另外的SecurityConfigurer,可以调用当前配置SecurityConfigurerand方法获得要配置的SecurityBuilder,再配置另外的SecurityConfigurer
  前面说完了这些SecurityConfigurer,那么这些SecurityConfigurer是拿来干什么的呢,其实大家可能已经猜出来了,这些SecurityConfigurer的数量和前面Spring Security为我们默认配置的Filter数量非常接近,没错,这些SecurityConfigurer就是用来配置HttpSecurity中的每一个Filter的,我们挑选一个SecurityConfigurer,例如DefaultLoginPageConfigurer,查看其源码,如下所示。

public final class DefaultLoginPageConfigurer<H extends HttpSecurityBuilder<H>>
		extends AbstractHttpConfigurer<DefaultLoginPageConfigurer<H>, H> {

	private DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = new DefaultLoginPageGeneratingFilter();

	private DefaultLogoutPageGeneratingFilter logoutPageGeneratingFilter = new DefaultLogoutPageGeneratingFilter();

	@Override
	public void init(H http) {
        
        // 对DefaultLoginPageGeneratingFilter进行配置
		this.loginPageGeneratingFilter.setResolveHiddenInputs(DefaultLoginPageConfigurer.this::hiddenInputs);
        
        // 对DefaultLogoutPageGeneratingFilter进行配置
		this.logoutPageGeneratingFilter.setResolveHiddenInputs(DefaultLoginPageConfigurer.this::hiddenInputs);
		http.setSharedObject(DefaultLoginPageGeneratingFilter.class, this.loginPageGeneratingFilter);
	}

	private Map<String, String> hiddenInputs(HttpServletRequest request) {
		CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
		return (token != null) ? Collections.singletonMap(token.getParameterName(), token.getToken())
				: Collections.emptyMap();
	}

	@Override
	@SuppressWarnings("unchecked")
	public void configure(H http) {
		AuthenticationEntryPoint authenticationEntryPoint = null;
		ExceptionHandlingConfigurer<?> exceptionConf = http.getConfigurer(ExceptionHandlingConfigurer.class);
		if (exceptionConf != null) {
			authenticationEntryPoint = exceptionConf.getAuthenticationEntryPoint();
		}
		if (this.loginPageGeneratingFilter.isEnabled() && authenticationEntryPoint == null) {
			this.loginPageGeneratingFilter = postProcess(this.loginPageGeneratingFilter);
            
            // 添加DefaultLoginPageGeneratingFilter
			http.addFilter(this.loginPageGeneratingFilter);
			LogoutConfigurer<H> logoutConfigurer = http.getConfigurer(LogoutConfigurer.class);
			if (logoutConfigurer != null) {
                
                // 添加DefaultLogoutPageGeneratingFilter
				http.addFilter(this.logoutPageGeneratingFilter);
			}
		}
	}
}

  从上面的源码可以看出,DefaultLoginPageConfigurerinit方法中对其中的两个Filter进行了配置,最后在configure方法中将这两个Filter添加到了HttpSecurity(在添加时会将Filter封装为HttpSecurity内部的OrderedFilter,方便排序)中,为后面创建DefaultSecurityFilterChain方法做准备。至于这些SecurityConfigurer都配置了哪些Filter,是如何配置的,大家可以自行去研究。
  介绍完了HttpSecurity中的SecurityConfigurer,下面就来看看HttpSecurityperformBuild到底是如何创建DefaultSecurityFilterChain的。

@Override
protected DefaultSecurityFilterChain performBuild() {
    this.filters.sort(OrderComparator.INSTANCE);
    List<Filter> sortedFilters = new ArrayList<>(this.filters.size());
    for (Filter filter : this.filters) {
        sortedFilters.add(((OrderedFilter) filter).filter);
    }
    return new DefaultSecurityFilterChain(this.requestMatcher, sortedFilters);
}

  其实这个方法很简单,就是对之前SecurityConfigurer添加的所有Filter进行排序后,最后再创建DefaultSecurityFilterChain

4.1.2 继承WebSecurityConfigurerAdapter后

  当我们继承WebSecurityConfigurerAdapter后,前面的配置类SpringBootWebSecurityConfiguration将会失效,因此Spring Security将不会为我们产生默认的SecurityFilterChain,而是采用我们在WebSecurityConfigurerAdapterconfigure方法的配置信息创建SecurityFilterChain,因此前面的代码将无法获取Spring Security配置的所有Filter,但是可以通过如下代码获取。

@SpringBootApplication
public class SpringSecurityApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(SpringSecurityApplication.class, args);
        FilterChainProxy filterChainProxy = applicationContext.getBean(FilterChainProxy.class);
        List<SecurityFilterChain> filterChains = filterChainProxy.getFilterChains();
        for (SecurityFilterChain filterChain : filterChains) {
            List<Filter> filters = filterChain.getFilters();
            for (int i = 0; i < filters.size(); i++) {
                System.out.println((i + 1) + ": " + filters.get(i));
            }
        }
    }
}

  既然Spring Security不会为我们产生默认的SecurityFilterChain,那么Spring Security是如何创建SecurityFilterChain的?具体详见4.2.2。

4.2 WebSecurity

  首先我们看到WebSecurity,如下图所示。
WebSecurity
  首先看到注释,其大概意思是说WebSecurityConfiguration将创建WebSecurity,然后利用WebSecurity创建FilterChainProxy。后面还说了如果要对WebSecurity进行个性化配置,我们可以创建WebSecurityConfigurerWebSecurityCustomizer,或者继承WebSecurityConfigurerAdapter,当然更多的情况是继承WebSecurityConfigurerAdapter(毕竟继承抽象类肯定比自己实现接口要方便得多)。当然,这些内容在前面讲解WebSecurityConfiguration时都已经提及,在Spring Boot启动过程中,WebSecurityConfiguration会获取容器中所有的WebSecurityConfigurer(WebSecurityConfigurerAdapter是其实现类)、WebSecurityCustomizer对创建的WebSecurity进行个性化配置,然后利用WebSecurity创建FilterChainProxy

​  我们再看到WebSecurity的定义,继承了AbstractConfiguredSecurityBuilder(其中的泛型参数和SecurityConfigurer一样),而其中的泛型参数OFilter,泛型参数BWebSecurity,也就是说当前的SecurityBuilderWebSecurity,其构建的目标对象是Filter,而FilterChainProxy就是一个Filter(前面讲解WebSecurityConfiguration时已经提及)。

4.2.1 默认配置

​  在WebSecurity的源码当中,WebSecurity并未实现beforeInitbeforeConfigure,只是实现了performBuild。在前面已经说过,SecurityBuilder在构建对象的过程中,重点研究对象是这些SecurityConfigurer和最后构建目标对象的performBuild方法,默认情况下,容器中并没有WebSecurityConfigurer(SecurityConfigurer的子接口)的实现类,因此WebSecurity中并没有SecurityConfigurer,所以最后就剩下了performBuild方法,如下所示。

protected Filter performBuild() throws Exception {
    /*
    	断言:securityFilterChainBuilders不能为空
    */
    Assert.state(!this.securityFilterChainBuilders.isEmpty(),
                 () -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
                 + "Typically this is done by exposing a SecurityFilterChain bean "
                 + "or by adding a @Configuration that extends WebSecurityConfigurerAdapter. "
                 + "More advanced users can invoke " + WebSecurity.class.getSimpleName()
                 + ".addSecurityFilterChainBuilder directly");
    int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size();
    List<SecurityFilterChain> securityFilterChains = new ArrayList<>(chainSize);
    List<RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>>> requestMatcherPrivilegeEvaluatorsEntries = new ArrayList<>();
    for (RequestMatcher ignoredRequest : this.ignoredRequests) {
        WebSecurity.this.logger.warn("You are asking Spring Security to ignore " + ignoredRequest
                                     + ". This is not recommended -- please use permitAll via HttpSecurity#authorizeHttpRequests instead.");
        SecurityFilterChain securityFilterChain = new DefaultSecurityFilterChain(ignoredRequest);
        securityFilterChains.add(securityFilterChain);
        requestMatcherPrivilegeEvaluatorsEntries
            .add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
    }
    
    // 遍历所有的securityFilterChainBuilders,构建SecurityFilterChain
    for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : this.securityFilterChainBuilders) {
        SecurityFilterChain securityFilterChain = securityFilterChainBuilder.build();
        securityFilterChains.add(securityFilterChain);
        requestMatcherPrivilegeEvaluatorsEntries
            .add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
    }
    if (this.privilegeEvaluator == null) {
        this.privilegeEvaluator = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
            requestMatcherPrivilegeEvaluatorsEntries);
    }
    FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
    if (this.httpFirewall != null) {
        filterChainProxy.setFirewall(this.httpFirewall);
    }
    if (this.requestRejectedHandler != null) {
        filterChainProxy.setRequestRejectedHandler(this.requestRejectedHandler);
    }
    filterChainProxy.afterPropertiesSet();

    Filter result = filterChainProxy;
    if (this.debugEnabled) {
        this.logger.warn("\n\n" + "********************************************************************\n"
                         + "**********        Security debugging is enabled.       *************\n"
                         + "**********    This may include sensitive information.  *************\n"
                         + "**********      Do not use in a production system!     *************\n"
                         + "********************************************************************\n\n");
        result = new DebugFilter(filterChainProxy);
    }
    this.postBuildAction.run();
    return result;
}

  从上面的代码可以看出,WebSecurity就是遍历所有的securityFilterChainBuilder(即构建SecurityFilterChainSecurityBuilder,这些securityFilterChainBuilderWebSecurityConfiguration已经添加),调用每一个securityFilterChainBuilderbuild方法创建SecurityFilterChain,将这些SecurityFilterChain放入集合中,最后创建FilterChainProxy

4.2.2 继承WebSecurityConfigurerAdapter后

  当我们继承WebSecurityConfigurerAdapter之后,容器中就存在了WebSecurityConfigurer(SecurityConfigurer的子接口)的实现类,WebSecurityConfiguration将不会向WebSecurity中添加securityFilterChainBuilder,那么securityFilterChainBuilder又是从何处添加的呢?我们继续往下看。
  既然WebSecurity中有了WebSecurityConfigurer,那么重点就在WebSecurityConfigurerAdapterinit方法和configure方法中,如下所示。

@Override
public void init(WebSecurity web) throws Exception {
    HttpSecurity http = getHttp();
    /**
    	向WebSecurity中添加securityFilterChainBuilder(能构建SecurityFilterChain的SecurityBuilder),即HttpSecurity
    	这段代码和WebSecurityConfiguration中添加securityFilterChainBuilder的那段代码类似
    */
    web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
        FilterSecurityInterceptor securityInterceptor = http.getSharedObject(FilterSecurityInterceptor.class);
        web.securityInterceptor(securityInterceptor);
    });
}

// 我们可以再子类中重写此方法,配置WebSecurity,更多的情况下是重写另一个configure方法,配置HttpSecurity
@Override
public void configure(WebSecurity web) throws Exception {
}

protected final HttpSecurity getHttp() throws Exception {
    if (this.http != null) {
        return this.http;
    }
    AuthenticationEventPublisher eventPublisher = getAuthenticationEventPublisher();
    this.localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
    AuthenticationManager authenticationManager = authenticationManager();
    this.authenticationBuilder.parentAuthenticationManager(authenticationManager);
    Map<Class<?>, Object> sharedObjects = createSharedObjects();
    // 创建HttpSecurity
    this.http = new HttpSecurity(this.objectPostProcessor, this.authenticationBuilder, sharedObjects);
    if (!this.disableDefaults) {
        // 如果没有禁用默认配置,将采用默认配置
        applyDefaultConfiguration(this.http);
        ClassLoader classLoader = this.context.getClassLoader();
        List<AbstractHttpConfigurer> defaultHttpConfigurers = SpringFactoriesLoader
            .loadFactories(AbstractHttpConfigurer.class, classLoader);
        for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
            this.http.apply(configurer);
        }
    }
    // 将创建好的HttpSecurity传递给我们重写的configure方法,让我们对HttpSecurity进行个性化配置
    configure(this.http);
    return this.http;
}

/**
	子类一般需要重写此方法,对HttpSecurity进行个性化配置
	这段代码里面的配置与配置类SpringBootWebSecurityConfiguration中的配置一模一样
*/
protected void configure(HttpSecurity http) throws Exception {
    this.logger.debug("Using default configure(HttpSecurity). "
                      + "If subclassed this will potentially override subclass configure(HttpSecurity).");
    http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
    http.formLogin();
    http.httpBasic();
}

/**
	默认配置:与配置类HttpSecurityConfiguration中的配置一模一样,都是向HttpSecurity中添加SecurityConfigurer
*/
private void applyDefaultConfiguration(HttpSecurity http) throws Exception {
    http.csrf();
    http.addFilter(new WebAsyncManagerIntegrationFilter());
    http.exceptionHandling();
    http.headers();
    http.sessionManagement();
    http.securityContext();
    http.requestCache();
    http.anonymous();
    http.servletApi();
    http.apply(new DefaultLoginPageConfigurer<>());
    http.logout();
}

  从上面的源码可以看出,当WebSecurityConfiguration不向WebSecurity中添加securityFilterChainBuilder后,WebSecurity利用我们继承的WebSecurityConfigurerAdapter添加了securityFilterChainBuilder,最后再调用performBuild方法完成了SecurityFilterChain(调用每个securityFilterChainBuilderbuild方法)和FilterChainProxy的创建。

4.3 总结

  不知不觉已经讲了那么多,想必大家已经被绕的云里雾里了吧,别急,先来回顾一下。

  首先,WebSecurityHttpSecurity都是通过调用自己的build方法创建FilterChainProxySecurityFilterChain的,而Spring Security又将这个过程拆分为了五步:beforeInitinitbeforeConfigureconfigureperformBuild

  其次,WebSecurity创建FilterChainProxy需要SecurityFilterChain,在默认情况下就采用SpringBootWebSecurityConfiguration事先创建好的SecurityFilterChain,当我们继承了WebSecurityConfigurerAdapter后,就利用WebSecurityConfigurerAdapter创建securityFilterChainBuilder(即HttpSecurity),最后再调用其build方法创建SecurityFilterChainHttpSecurity创建SecurityFilterChain需要一系列Filter,而Spring Security并不是采用直接创建Filter的方式,而是用一系列SecurityConfigurer去对Filter进行配置后,再创建最终的Filter,将其添加到HttpSecurity中。

  最后,想必大家都已经明白了我们为什么要继承WebSecurityConfigurerAdapter,并重写里面的configure(HttpSecurity http)方法,实际上就是将HttpSecurity 对象传给我们,然后将HttpSecurity中的SecurityConfigurer暴露给我们,让我们对其进行配置,进而影响到每一个Filter的配置,最后影响整个SecurityFilterChain的最终配置。WebSecurityConfigurerAdapter本身就是一个SecurityConfigurer,影响着最终FilterChainProxy的产生。

  事实上,在我们继承WebSecurityConfigurerAdapter的那一刻,WebSecurityHttpSecurity的创建先后顺序就已经发生了改变,默认情况下是HttpSecurity在前,WebSecurity在后,继承WebSecurityConfigurerAdapter之后,则是WebSecurity在前,HttpSecurity(由WebSecurity帮我们创建HttpSecurity)在后。

  通过前面的讲述我们可以发现,无论是SecurityFilterChain,还是FilterChainProxy,都是利用SecurityBuilder一步一步构建出来的,在构建的过程中,通过一系列的SecurityConfigurer进行个性化配置,最终构建出目标对象。前面所讲述的AuthenticationManager也是通过这种方式构建出来的,大家如果感兴趣的话可以自行去了解,毕竟授之以鱼不如授人以渔嘛。

5. 控制台的初始密码是如何产生的

  初始密码是如何产生的,和前面的过滤器链一样,肯定不能凭空产生,也是由Spring Boot自动装配产生的。我们在spring.factories中找到名为UserDetailsServiceAutoConfiguration,如下所示。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean(
		value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
				AuthenticationManagerResolver.class },
		type = { "org.springframework.security.oauth2.jwt.JwtDecoder",
				"org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector",
				"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository" })
public class UserDetailsServiceAutoConfiguration {

    // 明文密码前缀,数据库中直接存储明文
	private static final String NOOP_PASSWORD_PREFIX = "{noop}";

    // 密码加密算法正则表达式
	private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");

	private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);

	@Bean
	@Lazy
	public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
			ObjectProvider<PasswordEncoder> passwordEncoder) {
        // 从SecurityProperties获取用户信息,这意味着我们可以直接在application.yml中配置用户信息
		SecurityProperties.User user = properties.getUser();
		List<String> roles = user.getRoles();
		return new InMemoryUserDetailsManager(
				User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
						.roles(StringUtils.toStringArray(roles)).build());
	}

	private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
		String password = user.getPassword();
        // 将生成的初始密码打印在控制台
		if (user.isPasswordGenerated()) {
			logger.warn(String.format(
					"%n%nUsing generated security password: %s%n%nThis generated password is for development use only. "
							+ "Your security configuration must be updated before running your application in "
							+ "production.%n",
					user.getPassword()));
		}
        // 如果我们指定了PasswordEncoder,存储的密码就不会指定前缀,注意:指定了PasswordEncoder后,密码一定也要以这种方式存储
		if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
			return password;
		}
        // 存储明文密码,加上指定前缀
		return NOOP_PASSWORD_PREFIX + password;
	}
}

6. PasswordEncoder

  前面在讲解UserDetailsServiceAutoConfigurationDaoAuthenticationProvider是都提到了PasswordEncoder,而且在讲解UserDetailsServiceAutoConfiguration的时候讲到了Spring Security在存储密码时,会加上特定的前缀,为什么要给密码加前缀呢?为什么指定了PasswordEncoder后,就不会加前缀呢?下面就来探究以下。

​  在默认情况下,DaoAuthenticationProvider中的PasswordEncoder会在无参构造方法中被赋值为DelegatingPasswordEncoder,而DelegatingPasswordEncoder,则是由PasswordEncoderFactories产生的。

public final class PasswordEncoderFactories {

	private PasswordEncoderFactories() {
	}

	@SuppressWarnings("deprecation")
	public static PasswordEncoder createDelegatingPasswordEncoder() {
		String encodingId = "bcrypt";
		Map<String, PasswordEncoder> encoders = new HashMap<>();
		encoders.put(encodingId, new BCryptPasswordEncoder());
		encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
		encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
		encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
		encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
		encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
		encoders.put("scrypt", new SCryptPasswordEncoder());
		encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
		encoders.put("SHA-256",
				new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
		encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
		encoders.put("argon2", new Argon2PasswordEncoder());
		return new DelegatingPasswordEncoder(encodingId, encoders);
	}
}

​  我们可以看到,在DelegatingPasswordEncoder中存储着一系列PasswordEncoder,每一个PasswordEncoder都指定了特定的前缀,那么这些前缀有什么作用呢?接下来我们看到DelegatingPasswordEncodermatches方法。

public class DelegatingPasswordEncoder implements PasswordEncoder {

	private static final String PREFIX = "{";

	private static final String SUFFIX = "}";

	@Override
	public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
		if (rawPassword == null && prefixEncodedPassword == null) {
			return true;
		}
        // 从带有前缀的密码中提取前缀,获得当前密码的加密方式
		String id = extractId(prefixEncodedPassword);
        // 根据前缀找特定的PasswordEncoder
		PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
		if (delegate == null) {
			return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
		}
        // 从带有前缀的密码中提取密码
		String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
        // 进行密码比对
		return delegate.matches(rawPassword, encodedPassword);
	}

	private String extractId(String prefixEncodedPassword) {
		if (prefixEncodedPassword == null) {
			return null;
		}
		int start = prefixEncodedPassword.indexOf(PREFIX);
		if (start != 0) {
			return null;
		}
		int end = prefixEncodedPassword.indexOf(SUFFIX, start);
		if (end < 0) {
			return null;
		}
		return prefixEncodedPassword.substring(start + 1, end);
	}

	private String extractEncodedPassword(String prefixEncodedPassword) {
		int start = prefixEncodedPassword.indexOf(SUFFIX);
		return prefixEncodedPassword.substring(start + 1);
	}
}

​  我们可以看到,前缀就是用来指定密码的加密方式,最后校验密码的时候方便找到特定的PasswordEncoder进行密码校验,而前缀为{noop}就直接存储的是明文密码。那么,Spring Security为什么要这么设计呢?指定了PasswordEncoder后为什么就不会采用DelegatingPasswordEncoder呢?这些问题大家可以自行去思考。

7. 代码示例

https://github.com/POILKJHMNBV/SpringSecurityDemo

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值