当前位置:首页 > 科技  > 软件

Spring Security 6.0:深度剖析其核心实现与工作原理

来源: 责编: 时间:2024-07-11 17:35:18 637观看
导读导言Spring Security是一个功能强大且高度且可定制的身份验证和访问控制框架,除了标准的身份认证和授权之外,它还支持点击劫持,CSRF,XSS,MITM(中间人)等常见攻击手段的保护,并提供密码编码,LDAP认证,Session管理,Remember Me认证

导言

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

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

为此,本文将基于Spring Boot 3.1.x依赖的Spring Security 6.1.x版本,深入探讨Spring Security的架构和实现原理。本文将着重解释Spring Security的设计思想,而不会过多涉及具体的实现细节。9il28资讯网——每日最新资讯28at.com

文章的目标是让读者在阅读完本文之后,能够对整个Spring Security框架有个清晰的理解,并在面对问题时知道如何着手排查。另外,本文重点关注Spring Security的总体架构,以及身份认证(Authentication)和鉴权控制(Authorization)的实现。9il28资讯网——每日最新资讯28at.com

9il28资讯网——每日最新资讯28at.com

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

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

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

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

这也是Spring Security的实现方式。本质上,Spring Security的实现原理很简单,就是提供了一个用于安全验证的Filter。假如我们自己实现一个简化版的Filter,它的大概逻辑应该是这样的:9il28资讯网——每日最新资讯28at.com

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)    }}
  1. 从HTTP请求中获取用户名和密码,来源包括标准的Basic Auth HTTP Header,表单字段或者cookie等等。
  2. 身份认证,也就是校验用户名和密码。
  3. 认证通过后,需要检查当前登录的用户有没有访问当前HTTP请求的权限,也就是鉴权逻辑。
  4. 权限校验也通过后,就继续执行其它Filter,所有Filter都通过后,进入Servlet,最终到达具体的Controller。

FilterChain9il28资讯网——每日最新资讯28at.com

在安全领域,由于攻防手段的多样性和认证鉴权方式的复杂性,将所有功能都放在一个Filter中会导致该Filter迅速演变为一个庞大而复杂的类。9il28资讯网——每日最新资讯28at.com

因此,在实际应用场景中,我们常常将这个庞大的Filter拆分成多个小Filter,并将它们链接在一起。每个Filter都只负责特定领域的功能,比如CsrfFilter,AuthenticationFilter,AuthorizationFilter等。9il28资讯网——每日最新资讯28at.com

这种概念被称为FilterChain,实际上JarkataEE规范也有相识的概念。通过使用FilterChain,你就可以以插拔的方式添加或移除特定功能的Filter,而无需改动现有的代码。9il28资讯网——每日最新资讯28at.com

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

上一节其实已经说明了Spring Security框架的基本思路,下面我们深入分析其实现原理和架构。9il28资讯网——每日最新资讯28at.com

实现原理

一个应用引入了Spring Security Starter包后,再启动应用,你会发现控制台多了下面这条日志,说明已经开启了Security特性。9il28资讯网——每日最新资讯28at.com

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:9il28资讯网——每日最新资讯28at.com

  1. CsrfFilter:这个Filter用于防止跨站点请求伪造攻击,这也是导致所有POST请求都失败的原因。基于Token验证的API服务可以选择关闭CsrfFilter,而一般Web页面需要开启。
  2. BasicAuthenticationFilter:支持HTTP的标准Basic Auth的身份验证模块。
  3. UsernamePasswordAuthenticationFilter:支持Form表单形式的身份验证模块。
  4. DefaultLoginPageGeneratingFilter和DefaultLogoutPageGeneratingFilter:用于自动生成登录页面和注销页面。
  5. AuthorizationFilter: 这个Filter负责授权模块。值得注意的是,在老版本中鉴权模块是FilterSecurityInterceptor.

这些Filter构成了Spring Security的核心功能,通过它们,我们可以实现身份验证、授权、防护等安全特性。根据应用的需求,我们可以选择启用或禁用特定的Filter,以定制和优化安全策略。9il28资讯网——每日最新资讯28at.com

SecurityFilterChain

DefaultSecurityFilterChain类实现了SecurityFilterChain接口,我们打开这个接口的源码,会发现它只有两个方法,matches用于匹配特定的Http请求(比如特定规则的URL),getFilters 用于获取可用的所有Security Filter。9il28资讯网——每日最新资讯28at.com

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

从这段代码可以得出两个结论:9il28资讯网——每日最新资讯28at.com

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

DelegatingFilterProxy

实际上,JakartaEE层面上的Filter实现是DelegatingFilterProxy类,它在Spring Security中起到了一个重要的桥梁作用,连接了Servlet容器和Spring容器。Servlet容器不了解Spring定义的Beans,而Spring Security的大部分组件及其依赖都是注册到Spring容器中的Bean。9il28资讯网——每日最新资讯28at.com

DelegatingFilterProxy核心代码的主要工作就是从WebApplicationContext获取指定名称的Filter Bean,然后委托给这个Bean的doFilter方法。以下是简化后的伪代码:9il28资讯网——每日最新资讯28at.com

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容器之间的无缝连接。9il28资讯网——每日最新资讯28at.com

FilterChainProxy

而这个被委托的Filter Bean的类型就是FilterChainProxy,是在WebSecurityConfiguration中配置的:9il28资讯网——每日最新资讯28at.com

// 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和其它一些配置构建出来的。9il28资讯网——每日最新资讯28at.com

FilterChainProxy主要作用就是查找匹配当前Http请求规则的SecurityFilterChain,然后将工作委派给SecurityFilterChain的所有Filter。简化后的伪代码如下所示:9il28资讯网——每日最新资讯28at.com

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

9il28资讯网——每日最新资讯28at.com

【Tips】FilterChainProxy可以认为是整个Spring Security处理请求的一个起点,如果你遇到Security相关问题,又不清楚是具体哪个Filter导致的,就可以从这里开始Debug。9il28资讯网——每日最新资讯28at.com

基本架构

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

图片图片9il28资讯网——每日最新资讯28at.com

Security Filter和配置DSL

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

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

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

@Beanstatic 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。9il28资讯网——每日最新资讯28at.com

除了Spring Boot的专有配置,Spring Security自身也提供了默认配置,这些默认配置在HttpSecurityConfiguration#httpSecurity方法中,它默认添加了很多Security Filter,核心代码如下:9il28资讯网——每日最新资讯28at.com

@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,又有各种的框架,下面将展开说明认证和鉴权两个核心模块。9il28资讯网——每日最新资讯28at.com

Authentication身份认证

身份认证有很多种方式,大致可以分为以下4类:9il28资讯网——每日最新资讯28at.com

  1. 标准的账号密码认证:这是很多网站都支持的方式,也是大家最熟悉的认证模式;
  2. 调用第三方服务或内部其它API进行认证:当服务自身无法直接获取用户的密码时,需要借助第三方服务或者内部API进行认证;
  3. 基于Token的认证:这是API服务一般使用的认知方式,通过令牌来进行身份验证;
  4. OAuth2或其它OpenID认证:这种方式广泛用于允许用户使用其它平台的身份信息进行登录,例如微信登录,Google登录等。

Spring Security支持大部分的认证方式,但不同的认证方式需要配置不同的Bean及其依赖Bean,否则很容易遇到各种异常和空指针。9il28资讯网——每日最新资讯28at.com

本文重点讨论标准的账号密码认证方式。9il28资讯网——每日最新资讯28at.com

实现原理

如果你使用的是Spring Boot,那么Spring Boot Starter Security默认就配置了Form表单和Basic认证方式,其配置代码如下所示:9il28资讯网——每日最新资讯28at.com

@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表单认证方式,讨论它具体是如何实现的。9il28资讯网——每日最新资讯28at.com

@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();    }}
  1. authorizeHttpRequests方法用于配置每个请求的权限控制,这里要求所有请求都要通过认证后才能访问。实际上,这个方法配置的更多是鉴权相关的内容,跟身份认证的关联较小,它本质上是增加了一个AuthorizationFilter用于鉴权,具体细节在鉴权部分会详细说明。
  2. http.formLogin方法提供了Form表单认证的方式,withDefaults方法是Form表单认证的默认配置。这段配置的作用就是增加了用于账号密码认证的UsernamePasswordAuthenticationFilter,以及自动生成登录页面和注销页面的DefaultLogoutPageGeneratingFilter和DefaultLogoutPageGeneratingFilter共3个Security Filter。值得注意的是,登录页面和注销页面这两个Filter是配合DefaultLoginPageConfigurer配置一起注册的。如果你通过formLogin.loginPage提供了自定义的登录页面,那么这两个Filter就不会被注册。

在本节中,我们主要讨论身份认证的实现,因此,接下来将详细探究Form表单认证方式中UsernamePasswordAuthenticationFilter的实现。9il28资讯网——每日最新资讯28at.com

AbstractAuthenticationProcessingFilter

对于Filter,我们重点分析它的doFilter方法的源码。实际上,它继承了抽象类AbstractAuthenticationProcessingFilter,而这个抽象类的doFilter是一个模板方法,定义了整个认证流程。其核心流程非常简单,伪代码如下:9il28资讯网——每日最新资讯28at.com

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

Authentication模型

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

public interface Authentication extends Principal, Serializable {    // 当前认证用户拥有的权限列表    Collection<? extends GrantedAuthority> getAuthorities();    // 用户的一个身份标识,通常就是用户名    Object getPrincipal();    // 可用于证明用户身份的一个凭证,通常就是用户密码    Object getCredentials();    // 当前用户是否认证通过    boolean isAuthenticated();    // 更新用户的认证状态    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;    // 获取附加的详情信息,比如原始的Http请求体等。    Object getDetails();}

具体的Authentication实现一般都命名为XXXToken,大部分都继承自抽象类AbstractAuthenticationToken,比如表示标准的用户名密码认证结果的UsernamePasswordAuthenticationToken,表示匿名登录用户认证结果的AnonymousAuthenticationToken等等,你也可以完全实现自己的Authentication。9il28资讯网——每日最新资讯28at.com

attempAuthentication方法

接下来,我们看下UsernamePasswordAuthenticationFilter的认证具体实现方法attempAuthentication,它的源码如下:9il28资讯网——每日最新资讯28at.com

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)        throws AuthenticationException {    // 默认只支持POST请求    if (this.postOnly && !request.getMethod().equals("POST")) {        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());    }    // 从form表单获取用户名和密码    String username = obtainUsername(request);    username = (username != null) ? username.trim() : "";    String password = obtainPassword(request);    password = (password != null) ? password : "";    // 构建一个用于认证的请求    UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,            password);    // 附加详细信息,比如请求体,有些认证方式需要除了用户名密码外更多的信息    setDetails(request, authRequest);    // 委托给AuthenticationManager做具体的认证    return this.getAuthenticationManager().authenticate(authRequest);}

这个方法非常简单,它主要进行一些前置校验工作,从请求体中获取用户名和密码,并构建认证请求对象。然后,剩余的认证工作都是委托给AuthenticationManager接口来完成的,该接口的定义如下:9il28资讯网——每日最新资讯28at.com

public interface AuthenticationManager {    Authentication authenticate(Authentication authentication) throws AuthenticationException;}

AuthenticationManager和AuthenticationProvider

AuthenticationManager接口只有一个方法,它的入参和出参都是Authentication对象。通常情况下,入参提供了必要的认证信息,例如用户名和密码。而在认证成功后,该方法会返回认证结果,并附加认证状态,用户拥有的权限列表等信息。如果认证失败,它会抛出AuthenticationException异常类的子类,其中包括DisabledException,LockedException和BadCredentialsException等账号相关的异常。9il28资讯网——每日最新资讯28at.com

AuthenticationManager接口定义了Spring Security的认证行为。你可以提供自定义的实现,Spring Security也提供了一个通用的实现类ProviderManager。ProviderManager将具体的认证工作委托给一系列的AuthenticationProvider。9il28资讯网——每日最新资讯28at.com

每个AuthenticationProvider对应不同的认证方式。比如最常见的用户名密码的认证实现是DaoAuthenticationProvider,而JwtAuthenticationProvider提供了JWT Token的认证。你可以通过添加不同的AuthenticationProvider的方式,在同一个服务内支持多种类型的认证方式,比如需要调用其它API检验密码的情况,就需要自定义AuthenticationProvider。9il28资讯网——每日最新资讯28at.com

此外,ProviderManager还可以配置父级AuthenticationManager,当这个ProviderManager的所有AuthenticationProvider都不支持所需的认证方式时,它会继续委托给父级的AuthenticationManager,而该父级通常也是一个ProviderManager类型。9il28资讯网——每日最新资讯28at.com

UserDetailsService和PasswordEncoder9il28资讯网——每日最新资讯28at.com

DaoAuthenticationProvider是最常用的认证实现之一,它通过UserDetailsService和PasswordEncoder来验证用户名和密码。9il28资讯网——每日最新资讯28at.com

UserDetailsService的作用是查找用户信息UserDetails,这些信息包括用户密码,状态,权限列表等。用户信息可以存储在内存,数据库或者其它任何地方。Spring Security默认的配置是内存存储,对应的UserDetailsService实现是InMemoryUserDetailsManager,而数据库存储则对应JdbcUserDetailsManager。9il28资讯网——每日最新资讯28at.com

从UserDetailsService获取到用户密码后,需要通过PasswordEncoder来验证密码的正确性。因为密码一般都不应该以明文形式存储,实际存储的是按一定规则编码后的文本,Spring Security支持多种编码方式,例如bcrypt,argon2,scrypt,pbkdf2等。你可以配置PasswordEncoder Bean来选择不同的编码方式。都是请注意,内置的编码方式默认对编码后的文本有一个格式要求,就是必须有类似{bcrypt}的前缀来表示编码方式。9il28资讯网——每日最新资讯28at.com

基本架构

架构图

上一节中,我们讲述了用户名密码认证的实现细节,现在,让我们以用户名密码认证方式为例,从整体上来看下身份认证的架构和流程。它的整体架构如下:9il28资讯网——每日最新资讯28at.com

  1. 当一个HTTP请求进来后,UsernamePasswordAuthenticationFilter会从HTTP请求体中获取用户名和密码,然后使用这些信息创建一个UsernamePasswordAuthenticationToken对象作为认证请求的参数。
  2. 接下来,AuthenticationManager(其实现类是ProviderManager)负责对接受到的UsernamePasswordAuthenticationToken进行认证。
  3. ProviderManager会遍历配置的所有AuthenticationProvider,查找支持UsernamePasswordAuthenticationToken类型的AuthenticationProvider,然后委托其进行实际的认证工作,而在这里,匹配的就是DaoAuthenticationProvider。
  4. DaoAuthenticationProvider首先调用UserDetailService获取用户信息,然后将获取到的密码(通常是编码后的密码)委托给PasswordEncoder进行验证。如果认证失败,DaoAuthenticationProvider会抛出AuthenticationException的子类表示认证失败。
  5. 当认证成功时,AuthenticationManager会返回一个UsernamePasswordAuthenticationToken对象作为认证结果,这个对象除了包含用户的基本信息外,最重要的是认证通过状态以及该用户拥有的权限列表,这些信息在后续的鉴权模块会用到。
  6. 认证结果会被放入SecurityContext,这样后续的模块(包括鉴权和用户业务模块等)如果需要这个结果(包括用户信息和权限列表),就可以通过以下方法获取:SecurityContextHolder.getContext().getAuthentication()。

组件替换

这个架构非常灵活,大部分组件都是可配置和可替换的,自底向上,我们分别可以替换以下组件来满足特定需求:9il28资讯网——每日最新资讯28at.com

  1. UserDetailsService:根据用户名查找用户信息的组件,默认配置的是内存存储InMemoryUserDetailsManager,你也可以配置为内置的数据库存储JdbcUserDetailsManager,但是它有很多默认的约定要遵守,对未来的扩展也不够灵活。通常会根据公司的规范要求或数据库存储的方式提供自定义的实现。
  2. PasswordEncoder:对密码进行编码的组件,建议根据公司的编码要求或当前数据库中已使用的编码来配置。如果没有特殊要求,建议采用默认的BCryptPasswordEncoder。
  3. AuthenticationProvider: 为了安全需要,公司内部很多应用是不允许直接访问用户的密码的,而通常会提供一个认证的API。此时,就需要自定义AuthenticationProvider,它的核心逻辑就是调用API做认证,然后把结果再包装成Authentication返回给AuthenticationManager。
  4. AuthenticationManager:它的默认实现ProviderManager适用于大部分场景,通常不需要替换,除非你不想引入太多的概念。
  5. UsernamePasswordAuthenticationFilter:如果你不想引入过多的概念和复杂度,可以提供自己的Security Filter,从而完全脱离该框架。但是需要确保认证结果模型Authentication仍然被正确处理,并且将结果通过方法SecurityContextHolder.getContext().setAuthentication放入Security Context中。

【Tips】从整个Security框架的角度来看,认证模块的核心概念只有两个,分别是认证结果Authentication和Security Context。其它概念都可以认为是认证模块的内部实现细节。9il28资讯网——每日最新资讯28at.com

鉴权模块Authorization

认证模块证明了用户的身份,但显然普通用户不应该可以随意访问管理页面或敏感资源,因此还需要有个模块来确保只有授权的用户才能执行特定的操作,这个模块称之为鉴权或者授权(Authorization)。9il28资讯网——每日最新资讯28at.com

当你通过HttpSecurity.authorizeHttpRequests方法来配置请求的访问权限控制时,就会自动添加鉴权的Security Filter:AuthorizationFilter,它是整个SecurityFilterChain的最后一个Filter。9il28资讯网——每日最新资讯28at.com

【版本兼容性】在Spring Security 6.0版本中,鉴权模块发生了很大变化。以前的版本中,鉴权模块使用FilterSecurityInterceptor,而6.0版本之后,这个被废弃了,取而代之的是AuthorizationFilter。同时,还有一些相关的依赖组件,如AccessDecisionManager和AccessDecisionVoter也被AuthorizationManger替换了。因此,本节的内容只限于6.0以及之后的版本。9il28资讯网——每日最新资讯28at.com

实现原理

我们先看下鉴权模块的入口,也就是AuthorizationFilter的doFilter方法:9il28资讯网——每日最新资讯28at.com

@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)        throws ServletException, IOException {    // ...其它非核心逻辑... //    try {        AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request); // (1)        this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);        if (decision != null && !decision.isGranted()) { // (2)            throw new AccessDeniedException("Access Denied");        }        chain.doFilter(request, response);    }    finally {        request.removeAttribute(alreadyFilteredAttributeName);    }}private Authentication getAuthentication() {    Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication(); // (3)    if (authentication == null) {        throw new AuthenticationCredentialsNotFoundException(                "An Authentication object was not found in the SecurityContext");    }    return authentication;}
  1. 这个方法本身很简单,核心逻辑都委托给了AuthorizationManager。AuthorizationManager会校验Authentication的权限,并返回鉴权的结果AuthorizationDecision。
  2. 如果当前认证用户没有访问权限,就会抛出AccessDeniedException异常,表示拒绝访问。
  3. 待校验的Authentication是从Security Context获取的,通常是在前面的认证阶段设置的。在这里,实际上传给AuthorizationManager的是一个获取Authentication的方法,而不是Authentication本身,这样就把实际的获取操作延后到了真正进行授权的时候,这在某些场景下可以提高性能,比如permitAll,实际上它根本用不到Authentication。

AuthorizationManager

AuthorizationManager才是真正执行鉴权逻辑的类,最常用的实现类是AuthorityAuthorizationManager,它的实现逻辑很简单,它会调用Authentication的getAuthorities方法,获取当前登录用户的权限列表,然后将这些权限与请求需要的权限进行匹配。9il28资讯网——每日最新资讯28at.com

实际上,选择使用哪个AuthorizationManager是开发手动设置的。我们来分析一个常用的权限配置代码片段:9il28资讯网——每日最新资讯28at.com

@Beanstatic SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {    http.authorizeHttpRequests((requests) -> // (1)            requests                    .requestMatchers("/admin").hasAuthority("ROLE_ADMIN") // (2)                    .requestMatchers("/hello").hasRole("USER") // (3)                    .anyRequest().authenticated()); // (4)    // ... 其它配置 ... //    return http.build();}
  1. 调用authorizeHttpRequests方法就相当于打开了鉴权模块,它会注册AuthorizationFilter到SecurityFilterChain的最后。
  2. 对于匹配/admin的请求,要求有ROLE_ADMIN权限。hasAuthority的底层就是配置了一个要求ROLE_ADMIN权限的AuthorityAuthorizationManager对象。
  3. 对于匹配/hello的请求,要求有USER角色,等价于ROLE_USER权限。hasRole会自动在角色名称前面加上前缀ROLE_。hasRole的底层就是配置了一个要求ROLE_USER权限的AuthorityAuthorizationManager对象。
  4. 对于其它的请求,只要通过身份认证就可以访问,不需要特定的权限。类似的,authenticated方法的底层配置了一个AuthenticatedAuthorizationManager对象。

在Spring Security中,很多初学者都容易混淆Role和Authority的区别,实际上在技术实现层面上,这两者没有本质区别,底层都仅仅是一个表示权限的字符串标识符。更多的区别在于权限管理的概念上,一般情况下,Authority表示细粒度的操作权限,比如ADD_USER,DELETE_USER等,通常是动词;而Role则会与实际业务角色想对应,比如管理员ADMIN,普通员工STAFF等,通常是名称。此外,一般一个Role会对应多个Authority,同时角色之间可以存在继承关系,比如ADMIN可以继承STAFF的所有权限。9il28资讯网——每日最新资讯28at.com

我们来看下hasAuthority的源码,以分析它是如何配置AuthorizeManager的:9il28资讯网——每日最新资讯28at.com

public AuthorizationManagerRequestMatcherRegistry hasAuthority(String authority) {    return access( // (3)      withRoleHierarchy( //(2)          AuthorityAuthorizationManager.hasAuthority(authority) // (1)      )    );}public static <T> AuthorityAuthorizationManager<T> hasAuthority(String authority) {    Assert.notNull(authority, "authority cannot be null");    return new AuthorityAuthorizationManager<>(authority);}public AuthorizationManagerRequestMatcherRegistry access(        AuthorizationManager<RequestAuthorizationContext> manager) {    Assert.notNull(manager, "manager cannot be null");    return AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager);}private AuthorityAuthorizationManager<RequestAuthorizationContext> withRoleHierarchy(           AuthorityAuthorizationManager<RequestAuthorizationContext> manager) {    manager.setRoleHierarchy(AuthorizeHttpRequestsConfigurer.this.roleHierarchy.get());    return manager;}
  1. AuthorityAuthorizationManager.hasAuthority方法简单地创建了一个要求特定authority权限的AuthorityAuthorizationManager实例。
  2. withRoleHierarchy是一个装饰器方法,它打开了角色继承的功能。角色继承允许一个角色继承另一个角色的所有权限,从而简化权限配置。
  3. 最后,access方法将这个AuthorityAuthorizationManager实例注册到权限控制中。

access方法是公开的,你可以自己实现一个AuthorizationManager,然后通过这个方法进行注册。例如,我们可以提供一个拒绝所有请求的实现:9il28资讯网——每日最新资讯28at.com

http.authorizeHttpRequests((requests) ->    requests.anyRequest().access((authentication, object) -> null));

【Tips】通过自定义AuthorizationManager,我们可以完全接管鉴权的逻辑,实现更加灵活和复杂的权限控制。9il28资讯网——每日最新资讯28at.com

基本架构

相比认证模块,鉴权模块不需要太多的灵活性和扩展性需求,因此它的架构相对简单。9il28资讯网——每日最新资讯28at.com

同样,我们以一个标准的鉴权流程为例,来看整体的架构和流程图。9il28资讯网——每日最新资讯28at.com

  1. 一个HTTP请求进来,经过了一系列Security Filter后,最终来到AuthorizationFilter,进而调用AuthorizationManager#check方法进行权限校验。
  2. 实际的校验工作继续委托给AuthoritiesAuthorizationManager。
  3. AuthoritiesAuthorizationManager先从Security Context中获取到Authentication对象(这个对象一般是前面的某个认证Filter设置的),然后基于其Authorites权限列表构建GrantedAuthority列表,用于权限项的匹配。
  4. 最终会返回一个AuthorizationDecision表示权限校验结果。

总结

本文重点分析了Spring Security的源码和架构,帮助读者理解其实现原理。由于篇幅有限,本文只覆盖了身份认证和鉴权模块的核心逻辑,很多特性没有涉及,包括Session管理,Remember Me服务,异常分支和错误处理等等,不过有了上述的基础知识,读者完全可以自己分析源码并深入理解这些特性。9il28资讯网——每日最新资讯28at.com

FAQ

认证和鉴权失败抛出的异常是如何处理的?9il28资讯网——每日最新资讯28at.com

当发生认证或鉴权失败时,Spring Security有专门的Security Filter ExceptionTranslationFilter来捕获并处理这些异常。如果是认证异常错误AuthenticationException及其子类,会触发AuthenticationEntryPoint#commence方法,而如果是鉴权错误AccessDeniedException及其子类,则会触发AccessDeniedHandler#handle方法。9il28资讯网——每日最新资讯28at.com

一个请求被Security拒绝了,应该如何Debug排查?9il28资讯网——每日最新资讯28at.com

如果遇到身份认证错误,建议直接Debug相关Filter的doFilter方法,比如Form表单登录的Filter就是UsernamePasswordAuthenticationFilter;而如果是鉴权错误,可以从AuthorizationFilter开始Debug。9il28资讯网——每日最新资讯28at.com

但需要注意的是,出于安全考虑,Security相关的错误通常不会提供明确的错误信息,甚至不会显示错误信息,而是直接跳转到登录页面,比如CsrfFilter可能会导致这种情况。在这种情况下,可以从第一个Filter开始Debug,启动日志搜索Will secure any request with,就可以找到所有Security Filter列表。或者直接从入口FilterChainProxy#doFilter开始Debug。9il28资讯网——每日最新资讯28at.com

SecurityFilterChain的配置方法底层是如何实现的?9il28资讯网——每日最新资讯28at.com

SecurityFilterChain是通过HttpSecurity提供的一套DSL进行配置的。诸如formLogin,csrf,authorizeHttpRequests等方法的逻辑都类似,参数都是一个lambda表达式,用于做各种自定义配置。而每个方法都会对应一个特定的配置类,比如FormLoginConfigurer,CsrfConfigurer等,在执行HttpSecurity#build方法的时候,会调用这些配置类的configure方法,该方法的作用就是根据用户的自定义配置,创建一个或者多个Security Filter,并将其注册到SecurityFilterChain。9il28资讯网——每日最新资讯28at.com

此外,开发者还可以通过HttpSecurity.addFilter方法直接添加自定义的Security Filter。而对于复杂且有许多配置选项的Filter,也可以自定义SecurityConfigurerAdapter类,并通过HttpSecurity#apply方法来配置和注册Filter。9il28资讯网——每日最新资讯28at.com

Spring Security Starter有哪些默认配置?9il28资讯网——每日最新资讯28at.com

Spring Security Starter默认配置在spring-boot-autoconfigure-x.x.x包下的文件META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中可以找到。而具体的核心配置类有HttpSecurityConfiguration和SpringBootWebSecurityConfiguration。9il28资讯网——每日最新资讯28at.com

Spring Security版本跟本文的不一样,遇到问题如何排查?9il28资讯网——每日最新资讯28at.com

每次Spring Security升级,尤其是大版本升级,都可能引入破坏性或者不兼容的更新。不过,基于Filter和SecuiryFilterChain的框架和架构通常是不会改变的。但是,通常会废弃掉老的配置方法,引入新的配置,某些特定模块的实现也有可能完全替换,比如6.0的鉴权模块AuthorizationFilter就完全替换了老的鉴权模块。9il28资讯网——每日最新资讯28at.com

你可以先从Security Filter列表开始排查,也可以通过入口FilterChainProxy#doFilter来Debug。9il28资讯网——每日最新资讯28at.com

Spring Security整体太复杂了,能不能不使用它,而完全自己实现?9il28资讯网——每日最新资讯28at.com

Security是个一个非常复杂的领域,很多开发者对其了解不深。使用Spring Security不仅提供了大部分的安全特性,还包含了很多安全领域的最佳实践。自己从头实现安全功能成本很高,并可能缺乏一些重要的安全特性。不过Spring Security的复杂设计以及频繁的破坏性更新,的确给开发带来了很大的学习成本和维护成本。9il28资讯网——每日最新资讯28at.com

Spring Security的架构非常灵活,因此作者的建议是,不需要完全照搬整体框架,对于不同的应用类型和场景,可以选择性地引入部分功能。比如Admin应用可以提供自定义的AuthenticationProvider,而API服务完全可以自定义Securiy Filter,只要维护好Security Context的Authentication,就可以很好的集成到Spring Security框架里,同时开发的学习和维护成本也能降到最低。9il28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-100473-0.htmlSpring Security 6.0:深度剖析其核心实现与工作原理

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 使用Spring Boot 3.x实现多平台购票信息一致性保障

下一篇: 威刚 2024 上半年累计营收达 209.1 亿新台币,同比大增 48.56%

标签:
  • 热门焦点
  • Find N3入网:最高支持16+1TB

    Find N3入网:最高支持16+1TB

    OPPO将于近期登场的Find N3折叠屏目前已经正式入网,型号为PHN110。本次Find N3在外观方面相比前两代有很大的变化,不再是小号的横向折叠屏,而是跟别的厂商一样采用了较为常见的
  • 7月安卓手机好评榜:三星S23Ultra好评率第一

    7月安卓手机好评榜:三星S23Ultra好评率第一

    性能榜和性价比榜之后,我们来看最后的安卓手机好评榜,数据来源安兔兔评测,收集时间2023年7月1日至7月31日,仅限国内市场。第一名:三星Galaxy S23 Ultra好评率:95.71%在即将迎来新
  • 5月iOS设备好评榜:iPhone 14仅排第43?

    5月iOS设备好评榜:iPhone 14仅排第43?

    来到新的一月,安兔兔的各个榜单又重新汇总了数据,像安卓阵营的榜单都有着比较大的变动,不过iOS由于设备的更新换代并没有那么快,所以相对来说变化并不大,特别是iOS好评榜,老款设
  • 为什么你不应该使用Div作为可点击元素

    为什么你不应该使用Div作为可点击元素

    按钮是为任何网络应用程序提供交互性的最常见方式。但我们经常倾向于使用其他HTML元素,如 div span 等作为 clickable 元素。但通过这样做,我们错过了许多内置浏览器的功能。
  • 中国家电海外掘金正当时|出海专题

    中国家电海外掘金正当时|出海专题

    作者|吴南南编辑|胡展嘉运营|陈佳慧出品|零态LT(ID:LingTai_LT)2023年,出海市场战况空前,中国创业者在海外纷纷摩拳擦掌,以期能够把中国的商业模式、创业理念、战略打法输出海外,他们依
  • 苹果公司要求三星和LG Display生产「无边框」OLED iPhone显示屏

    苹果公司要求三星和LG Display生产「无边框」OLED iPhone显示屏

    据 The Elec 报道,苹果已要求其供应商为未来的 iPhone 型号开发「无边框」OLED 显示面板。苹果显然已要求三星和 LG Display 开发新的 OLED 显示面
  •  首发天玑9200+ iQOO Neo8系列发布首销售价2299元起

    首发天玑9200+ iQOO Neo8系列发布首销售价2299元起

    2023年5月23日晚,iQOO Neo8系列正式发布。其中,Neo系列首款Pro之作——iQOO Neo8 Pro强悍登场,限时售价3099元起;价位段最强性能手机iQOO Neo8同期上市
  • iQOO Neo8 Pro抢先上架:首发天玑9200+ 安卓性能之王

    iQOO Neo8 Pro抢先上架:首发天玑9200+ 安卓性能之王

    经过了一段时间的密集爆料,昨日iQOO官方如期对外宣布:将于5月23日推出全新的iQOO Neo8系列新品,官方称这是一款拥有旗舰级性能调校的作品。随着发布时
  • 上海举办人工智能大会活动,建设人工智能新高地

    上海举办人工智能大会活动,建设人工智能新高地

    人工智能大会在上海浦江两岸隆重拉开帷幕,人工智能新技术、新产品、新应用、新理念集中亮相。8月30日晚,作为大会的特色活动之一的上海人工智能发展盛典人工
Top