一文带你读懂Spring Security 6.0的实现原理

2023年 7月 26日 46.8k 0

导言

Spring Security是一个功能强大且高度且可定制的身份验证和访问控制框架,除了标准的身份认证和授权之外,它还支持点击劫持,CSRF,XSS,MITM(中间人)等常见攻击手段的保护,并提供密码编码,LDAP认证,Session管理,Remember Me认证,JWT,OAuth 2.0等功能特性。

由于安全领域本身的复杂性和丰富的安全特性支持,以及Spring Security高度的可定制性,使得它成为一个庞大且复杂的框架。每次升级可能带来的破坏性更新,加上网络上的陈旧教程,更是加重了Spring Security非常难用的印象。很多新手可能跟作者一样,首次引入Spring Security框架之后,突然发现很多页面无法访问,感到无所适从。

为此,本文将基于Spring Boot 3.1.x依赖的Spring Security 6.1.x版本,深入探讨Spring Security的架构和实现原理。本文将着重解释Spring Security的设计思想,而不会过多涉及具体的实现细节。文章的目标是让读者在阅读完本文之后,能够对整个Spring Security框架有个清晰的理解,并在面对问题时知道如何着手排查。另外,本文重点关注Spring Security的总体架构,以及身份认证(Authentication)和鉴权控制(Authorization)的实现。

【版本兼容性】Spring Security 6引入了很多破坏性的更新,包括废弃代码的删除,方法重命名,全新的配置DSL等,但是架构和基本原理还是保持不变的。本文在讲解过程中会尽量指出当前版本跟老版本的差异,尤其是涉及到兼容性问题的时候。
【阅读提示】本文的篇幅较长,并且包含了部分源码分析,时间有限的情况下,可以重点阅读架构图部分。

Java Web应用的Security实现基本思路

大家可以尝试思考下,安全相关的校验和处理,应该处于应用的哪个部分呢?答案是,应该放在所有请求的入口,因为它是跟具体的业务逻辑无关的,在Spring MVC世界里就是@Controller之前。

在JakartaEE(JavaEE的新版)规范中,Filter和Servlet都符合这个前置要求。然而,Spring的Web应用基本上只包含一个DispatcherServelt,主要用于请求分发,缺乏安全相关的支持和合适的扩展机制。而Filter运行在Servlet之前,而规范本身就支持配置多个Filter。因此,在请求到达Servlet之前,先通过Filter进行安全验证就是一个非常合理的实现方式。这样可以在请求进入业务逻辑之前,对请求进行拦击,然后进行必要的安全性检查和处理。

这也是Spring Security的实现方式。本质上,Spring Security的实现原理很简单,就是提供了一个用于安全验证的Filter。假如我们自己实现一个简化版的Filter,它的大概逻辑应该是这样的:

public class SimpleSecurityFilter extends HttpFilter {
    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        UsernamePasswordToken token = extractUsernameAndPasswordFrom(request);  // (1)
        if (notAuthenticated(token)) {  // (2)
            // 用户名密码错误
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // HTTP 401.
            return;
        }
        if (notAuthorized(token, request)) { // (3)
            // 当前登录用户的权限不足
            response.setStatus(HttpServletResponse.SC_FORBIDDEN); // HTTP 403
            return;
        }
        // 通过了身份验证和权限校验,继续执行其它Filter,最终到达Servlet
        chain.doFilter(request, response); // (4)
    }
}
  • 从HTTP请求中获取用户名和密码,来源包括标准的Basic Auth HTTP Header,表单字段或者cookie等等。
  • 身份认证,也就是校验用户名和密码。
  • 认证通过后,需要检查当前登录的用户有没有访问当前HTTP请求的权限,也就是鉴权逻辑。
  • 权限校验也通过后,就继续执行其它Filter,所有Filter都通过后,进入Servlet,最终到达具体的Controller。
  • FilterChain

    在安全领域,由于攻防手段的多样性和认证鉴权方式的复杂性,将所有功能都放在一个Filter中会导致该Filter迅速演变为一个庞大而复杂的类。

    因此,在实际应用场景中,我们常常将这个庞大的Filter拆分成多个小Filter,并将它们链接在一起。每个Filter都只负责特定领域的功能,比如CsrfFilterAuthenticationFilterAuthorizationFilter等。

    这种概念被称为FilterChain,实际上JarkataEE规范也有相识的概念。通过使用FilterChain,你就可以以插拔的方式添加或移除特定功能的Filter,而无需改动现有的代码。

    Spring Security框架的基本架构和原理

    上一节其实已经说明了Spring Security框架的基本思路,下面我们深入分析其实现原理和架构。

    实现原理

    一个应用引入了Spring Security Starter包后,再启动应用,你会发现控制台多了下面这条日志,说明已经开启了Security特性。

    2023-07-12T10:05:23.168+08:00  INFO 680540 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@46e3559f, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@3b83459e, org.springframework.security.web.context.SecurityContextHolderFilter@26837057, org.springframework.security.web.header.HeaderWriterFilter@2d74c81b, org.springframework.security.web.csrf.CsrfFilter@3a17b2e3, org.springframework.security.web.authentication.logout.LogoutFilter@5f5827d0, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@4ed5a1b0, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@3b332962, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@32118208, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@67b355c8, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@991cbde, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@dd4aec3, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@414f87a9, org.springframework.security.web.access.ExceptionTranslationFilter@59939293, org.springframework.security.web.access.intercept.AuthorizationFilter@f438904]
    

    从这条日志可以观察到,Spring Security通过DefaultSecurityFilterChain类来完成安全相关的功能,而该类本身又由其它Filter组成。默认情况下,Spring Security Starter引入了15个Filter,下面我们简要介绍下其中几个重要的Filter:

  • CsrfFilter:这个Filter用于防止跨站点请求伪造攻击,这也是导致所有POST请求都失败的原因。基于Token验证的API服务可以选择关闭CsrfFilter,而一般Web页面需要开启。
  • BasicAuthenticationFilter:支持HTTP的标准Basic Auth的身份验证模块。
  • UsernamePasswordAuthenticationFilter:支持Form表单形式的身份验证模块。
  • DefaultLoginPageGeneratingFilter和DefaultLogoutPageGeneratingFilter:用于自动生成登录页面和注销页面。
  • AuthorizationFilter: 这个Filter负责授权模块。值得注意的是,在老版本中鉴权模块是FilterSecurityInterceptor.
  • 这些Filter构成了Spring Security的核心功能,通过它们,我们可以实现身份验证、授权、防护等安全特性。根据应用的需求,我们可以选择启用或禁用特定的Filter,以定制和优化安全策略。

    SecurityFilterChain

    DefaultSecurityFilterChain类实现了SecurityFilterChain接口,我们打开这个接口的源码,会发现它只有两个方法,matches用于匹配特定的Http请求(比如特定规则的URL),getFilters 用于获取可用的所有Security Filter。

    public interface SecurityFilterChain {
        boolean matches(HttpServletRequest request); // 规则匹配
        List getFilters(); // 该FilterChain下的所有Security Filter
    }
    

    从这段代码可以得出两个结论:

  • 不同的Http请求可以对应不同的SecurityFilterChain(通过matches方法)。
  • SecurityFilterChain不是我们以为的JakartaEE的Servlet Filter实现,它仅仅是一个包含多个Filter的容器,本身不负责调度和执行。它只是一个配置项,用于指定一组Filter,以实现特定的安全需求。
  • DelegatingFilterProxy

    实际上,JakartaEE层面上的Filter实现是DelegatingFilterProxy类,它在Spring Security中起到了一个重要的桥梁作用,连接了Servlet容器和Spring容器。Servlet容器不了解Spring定义的Beans,而Spring Security的大部分组件及其依赖都是注册到Spring容器中的Bean。

    DelegatingFilterProxy核心代码的主要工作就是从WebApplicationContext获取指定名称的Filter Bean,然后委托给这个Bean的doFilter方法。以下是简化后的伪代码:

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        Filter delegateToUse = this.delegate;
        if (delegateToUse == null) {
            synchronized (this.delegateMonitor) {
                delegateToUse = this.delegate;
                if (delegateToUse == null) {
                    WebApplicationContext wac = findWebApplicationContext();
                    // 获取Filter Bean并初始化
                    delegateToUse = initDelegate(wac);
                }
                this.delegate = delegateToUse;
            }
        }
        // 委托给的delegate对象完成实际的doFilter
        invokeDelegate(delegateToUse, request, response, filterChain);
    }
    
    protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
        // Bean名称配置在SecurityFilterAutoConfiguration.DEFAULT_FILTER_NAME = "springSecurityFilterChain"
        String targetBeanName = getTargetBeanName();
        // 从容器中获取指定名称的Filter类型Bean
        Filter delegate = wac.getBean(targetBeanName, Filter.class);
        if (isTargetFilterLifecycle()) {
            delegate.init(getFilterConfig());
        }
        return delegate;
    }
    

    通过这种方式,DelegatingFilterProxy实现了将Servlet容器中的Filter请求委托给Spring容器中的具体Filter Bean处理,从而实现了Servlet容器和Spring容器之间的无缝连接。

    FilterChainProxy

    而这个被委托的Filter Bean的类型就是FilterChainProxy,是在WebSecurityConfiguration中配置的:

    // name = "springSecurityFilterChain"
    @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
    public Filter springSecurityFilterChain() throws Exception {
        // 配置SecurityFilterChain
        boolean hasFilterChain = !this.securityFilterChains.isEmpty();
        if (!hasFilterChain) {
            this.webSecurity.addSecurityFilterChainBuilder(() -> {
                this.httpSecurity.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated());
                this.httpSecurity.formLogin(Customizer.withDefaults());
                this.httpSecurity.httpBasic(Customizer.withDefaults());
                return this.httpSecurity.build();
            });
        }
        for (SecurityFilterChain securityFilterChain : this.securityFilterChains) {
            this.webSecurity.addSecurityFilterChainBuilder(() -> securityFilterChain);
        }
        // WebSecurity自定义配置
        for (WebSecurityCustomizer customizer : this.webSecurityCustomizers) {
            customizer.customize(this.webSecurity);
        }
        // FilterChainProxy最终是由WebSecurity构建出来的
        return this.webSecurity.build();
    }
    

    从上面代码可以发现,FilterChainProxy对象最终是由WebSecurity根据SecurityFilterChain和其它一些配置构建出来的。

    FilterChainProxy主要作用就是查找匹配当前Http请求规则的SecurityFilterChain,然后将工作委派给SecurityFilterChain的所有Filter。简化后的伪代码如下所示:

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 获取匹配的所有Filter
        List filters = getFilters(request); 
        // 按顺序执行Filter
        Filter nextFilter = this.filters.get(this.currentPosition - 1);
        nextFilter.doFilter(request, response, this);
    }
    
    private List getFilters(HttpServletRequest request) {
        for (SecurityFilterChain chain : this.filterChains) {
            // 返回匹配规则的SecurityFilterChain的Filter列表
            if (chain.matches(request)) { 
                return chain.getFilters();
            }
        }
        return null;
    }
    

    【Tips】FilterChainProxy可以认为是整个Spring Security处理请求的一个起点,如果你遇到Security相关问题,又不清楚是具体哪个Filter导致的,就可以从这里开始Debug。

    基本架构

    从上一节的内容,我们可以得出下面这一副架构图(图中蓝色和橘红色的部分代表Security Security)。从图中可以看出,Spring Security框架通过DelegatingFilterProxy建立起了Servlet容器和Spring容器的链接,FilterChainProxy基于匹配规则(比如URL匹配),决定使用哪个SecurityFilterChain。而SecurityFilterChain又由零到多个Filter组成,这些Filter完成实际的功能。

    Security Filter和配置DSL

    Spring Security是基于Jakarta EE的Filter实现的,而在此基础上,它提供了一套自身的Filter机制,相当于两层的Filter嵌套。为了不混淆这两种Filter,我们把Spring Security框架提供的Filter称为Security Filter。在下文中,我们所提及的配置,扩展和自定义的Filter都指的是Security Filter,如果没有特别说明,都默认指的是Security Filter。

    通过一系列Security Filter,Spring Security提供了丰富的开箱即用的安全功能,包括身份认证,鉴权,Csrf等等。每个功能都是通过一个或者多个Security Filter实现的。有些复杂的Filter,例如身份认证和鉴权,拥有自己的特定架构,并且会依赖Filter的顺序和执行过程中的上下文信息,这也是导致Spring Security在使用上相对复杂的原因之一。

    Spring Securiy的基本配置都是通过自定义SecurityFilterChain的Bean来实现的。下面是一个示例配置,它提供了自定义的登录页面,并且针对不同的URL配置了不同的角色权限,这些配置方法实际上就是配置不同的Security Filter,更详细的解释会在后面讲解具体特性的时候时展开说明。

    @Bean
    static SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {
        // 鉴权相关配置
        http.authorizeHttpRequests((requests) ->
                request.requestMatchers("/admin").hasAuthority("ROLE_ADMIN") // "/admin"要求有“ROLE_ADMIN"角色权限
                        .requestMatchers("/hello").hasRole("USER") // "/hello"要求有"ROLE_USER"角色权限
                        .anyRequest().authenticated()); // 其它只需要身份认证通过即可,不需要其它特殊权限
        // 登录相关配置
        http.formLogin(formLogin -> formLogin
                .loginPage("/authentication") // 自定义登录页面,不再使用内置的自动生成页面
                .permitAll() // 允许自定义页面的匿名访问,不需要认证和鉴权
        );
        return http.build(); // 返回构建的SecurityFilterChain实例
    }
    

    【版本兼容性】Spring Security 6.0在配置方面引入了许多改变。在之前的老版本中,可以选择废弃的WebSecurityConfigurerAdapter进行配置,但从6.0版本开始,这个废弃类已经被删除了。而目前很多老项目以及网上的教程仍在使用WebSecurityConfigurerAdapter。 另外,配置DLS也发生了变化。Spring Security 6.0采用了基于Lambda表达式的DSL配置方式,取代了之前的纯链式调用方式,使得配置更加灵活和直观。一些方法名称也进行了修改,例如antMatchers替换为requestMatchers

    除了Spring Boot的专有配置,Spring Security自身也提供了默认配置,这些默认配置在HttpSecurityConfiguration#httpSecurity方法中,它默认添加了很多Security Filter,核心代码如下:

    @Bean(HTTPSECURITY_BEAN_NAME)
    @Scope("prototype")
    HttpSecurity httpSecurity() throws Exception {
        // ... //
        http
            .csrf(withDefaults())
            .addFilter(webAsyncManagerIntegrationFilter)
            .exceptionHandling(withDefaults())
            .headers(withDefaults())
            .sessionManagement(withDefaults())
            .securityContext(withDefaults())
            .requestCache(withDefaults())
            .anonymous(withDefaults())
            .servletApi(withDefaults())
            .apply(new DefaultLoginPageConfigurer());
        http.logout(withDefaults());
        // ... //
        return http;
    }
    

    以上解释了Spring Security的实现原理和基本架构,而具体到特定的Security Filter,又有各种的框架,下面将展开说明认证和鉴权两个核心模块。

    Authentication身份认证

    身份认证有很多种方式,大致可以分为以下4类:

  • 标准的账号密码认证:这是很多网站都支持的方式,也是大家最熟悉的认证模式;
  • 调用第三方服务或内部其它API进行认证:当服务自身无法直接获取用户的密码时,需要借助第三方服务或者内部API进行认证;
  • 基于Token的认证:这是API服务一般使用的认知方式,通过令牌来进行身份验证;
  • OAuth2或其它OpenID认证:这种方式广泛用于允许用户使用其它平台的身份信息进行登录,例如微信登录,Google登录等。
  • Spring Security支持大部分的认证方式,但不同的认证方式需要配置不同的Bean及其依赖Bean,否则很容易遇到各种异常和空指针。

    本文重点讨论标准的账号密码认证方式。

    实现原理

    如果你使用的是Spring Boot,那么Spring Boot Starter Security默认就配置了Form表单和Basic认证方式,其配置代码如下所示:

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.SERVLET)
    class SpringBootWebSecurityConfiguration {
        @Configuration(proxyBeanMethods = false)
        @ConditionalOnDefaultWebSecurity
        static class SecurityFilterChainConfiguration {
            @Bean
            @Order(SecurityProperties.BASIC_AUTH_ORDER)
            SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
                http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()); // 所有URL都需要认证用户
                http.formLogin(withDefaults()); // 支持form表单认证,默认配置提供了自动生成的登录和注销页面
                http.httpBasic(withDefaults()); // 支持HTTP Basic Authentication
                return http.build();
            }
    
        }
        // ...其它配置...
    }
    

    为了讨论方便,我们用下面的配置覆盖Spring Boot默认的配置,只支持Form表单认证方式,讨论它具体是如何实现的。

    @Configuration()  
    public class MySecurityConfig {
        @Bean
        SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()); // (1)
            http.formLogin(withDefaults()); // (2)
            return http.build();
        }
    }
    
  • authorizeHttpRequests方法用于配置每个请求的权限控制,这里要求所有请求都要通过认证后才能访问。实际上,这个方法配置的更多是鉴权相关的内容,跟身份认证的关联较小,它本质上是增加了一个AuthorizationFilter用于鉴权,具体细节在鉴权部分会详细说明。
  • http.formLogin方法提供了Form表单认证的方式,withDefaults方法是Form表单认证的默认配置。这段配置的作用就是增加了用于账号密码认证的UsernamePasswordAuthenticationFilter,以及自动生成登录页面和注销页面的DefaultLogoutPageGeneratingFilterDefaultLogoutPageGeneratingFilter共3个Security Filter。值得注意的是,登录页面和注销页面这两个Filter是配合DefaultLoginPageConfigurer配置一起注册的。如果你通过formLogin.loginPage提供了自定义的登录页面,那么这两个Filter就不会被注册。
  • 在本节中,我们主要讨论身份认证的实现,因此,接下来将详细探究Form表单认证方式中UsernamePasswordAuthenticationFilter的实现。

    AbstractAuthenticationProcessingFilter

    对于Filter,我们重点分析它的doFilter方法的源码。实际上,它继承了抽象类AbstractAuthenticationProcessingFilter,而这个抽象类的doFilter是一个模板方法,定义了整个认证流程。其核心流程非常简单,伪代码如下:

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 首先判断该请求是否是认证请求或者登录请求
        if (!requiresAuthentication(request, response)) { // (1)
            chain.doFilter(request, response);
            return;
        }
        try {
            Authentication authenticationResult = attemptAuthentication(request, response); // (2) 实际认证逻辑
            // 认证成功
            successfulAuthentication(request, response, chain, authenticationResult); // (3)
        }
        catch (AuthenticationException ex) {
            // 认证失败
            unsuccessfulAuthentication(request, response, ex); // (4)
        }
    }
    
  • 首先requiresAuthentication方法用于判断当前请求是否为认证请求或者登录请求,例如通常是POST /login。只有在登录认证的情况下,才需要通过这个Filter;
  • attempAuthentication方法是实际的认证逻辑,这是一个抽象方法,具体的逻辑由子类重写实现。它的规范行为是,如果认证成功,应该返回认证结果Authentication,否则以抛出异常AuthenticationException的方式表示认证失败;
  • successfulAuthentication:认证成功后,该方法会将Authentication对象放到Security Context中,这是非常关键的一步,后续需要认证结果的时候都是从Security Context获取的,比如鉴权Filter。此外,该方法还会处理其它一些相关功能,比如RememberMe,事件发布,最后再调用AuthenticationSuccessHandler
  • unsuccessfulAuthentication :在认证失败后,它会清空Security Context,调用RememberMe相关服务和AuthenticationFailureHandler来处理认证失败后的回调逻辑,比如跳转到错误页面。
  • Authentication模型

    在这里,我们涉及到了一个非常重要的数据模型——Authentication,它是一个接口类型,它既是对认证结果的一个抽象表示,同时也是对认证请求的一个抽象,通常也被称为认证Token。它的方法都比较抽象,定义如下:

    public interface Authentication extends Principal, Serializable {
    // 当前认证用户拥有的权限列表
    Collection

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论