Spring Security 底层原理 #1. Servlet Filter 链

我们先来看下最基础的 Servlet Filter 体系,在 Servlet Filter 体系中客户端发起一个请求过程是经过 0 到 N 个 Filter 然后交给 Servlet 处理。

Filter 不但可以修改 HttpServletRequest 和 HttpServletResponse ,可以让我们在请求响应的前后做一些事情,甚至可以终止过滤器链 FilterChain 的传递。

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { 
    // 请求被 Servlet 触发前执行
    // do something ...

    if (condition) { 
        // 放行请求,进入下一个 Filter
        chain.doFilter(request, response); 
    }

    // 请求被执行完毕后处理一些事情 
    // do something ...
}

Copied!

由于 Filter 仅影响下游 Filters 和 Servlet ,因此每个 Filter 调用的顺序非常重要。

#2. Spring Security 接入 Servlet Filter

从上图我们可以看出 Spring Security 以一个单 Servlet Filter:FilterChainProxy 存在于整个过滤器链中,而这个 FilterChainProxy 实际内部代理着众多的 Spring Security Filter 。

简单来说,当请求 “顺着” Servlet Filter Chain 走,走到 FilterChainProxy 时,会被它 “引流” 到 Spring Security Chain ,“顺着” Spring Security Chain 走,当整个 Spring Security Chain “走” 完之后,再顺着 FilterChainProxy 继续 Servlet Filter Chain “走” 下去。

#3. Spring Security 的 Filter 链

Spring Security 会创建内置的 Spring Security Filter ,并将它们组建成 Filter Chain (有人称它们为虚拟链,以便于与 Servlet 的 Filter Chain 区别),并通过 FilterChainProxy 接入到 Servlet 的 Fialter Chain 中。

我们之前通过 HttpSecurity 做出的配置只生成了一条链。简单来说,在 Spring Security 中每一个 HttpSecurity 生成一条过滤器链。

补充

Spring Security 允许同时存在多个 Security Filter Chain ,不过要实现这种功效不能用普通的配置方式配置。

每条过滤链就是一个 SecurityFilterChain :

public interface SecurityFilterChain { 

    // 判断请求是否符合该过滤器链的要求 
    boolean matches(HttpServletRequest request); 

    // 对应的过滤器链 
    List<Filter> getFilters(); 

}

Copied!

默认的过滤器链中包含以下过滤器:

你在配置类中的配置,会影响到 Seucrity Filter Chain 上的 Filter 的数量的多少。例如,你关闭如下 3 个功能,本质上就是取消了对应的 3 个过滤器:

http.csrf().disable();
http.sessionManagement().disable();
http.exceptionHandling().disable();

Copied!

注意

虽然我们可以通过配置,将 ExceptionTranslationFilter 从 Security Filter Chain 上移除,但是一般情况下我们将它视为必要过滤器,因为没有它的话,Spring Security 是直接将原始的异常信息直接暴露给用户。这样,既不友好又不方便。

#4. 多个 Spring Security Filter Chain(了解、自学)

Spring Security 允许有多条过滤器链并行,Spring Security 的 FilterChainProxy 可以代理多条过滤器链并根据不同的 URI 匹配策略进行分发。但是每个请求每次只能被分发到一条过滤器链。

在这种情况下,每个 Security Filter Chain 就只对自己负责的请求有作用,而对其它请求则视而不见。或者说,其它请求也轮不到它来『滤』。不同的 Security FilterChain 之间是互斥而且平等的。

每个 Security Filer Chain 在代码层面仍然也都是一个 SecurityFilterChain 对象。

另外,这多个 Security Filter Chain 和 FilterChainProxy 的关系如下图:

不过,要实现多个过滤器链的同时存在,需要经过特殊配置(而非我们之前的那种配置),一般情况下这种需求并不多。

#5. 认证
UsernamePasswordAuthenticationFilter
└──> ProviderManager
     └──> Provider
          └──> UserDetailsService

Copied!

认证的最终成果:在 Spring Security 上下文(Context) 中存入一个 “已认证” 状态的 Authentication Token ,这个 Token 中还含有当前用户的用户信息(包括所具有的权限)。

#5.1 UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter 是 Security Filter Chain 上最常见一员。它实现了 HTTP 登录认证功能。

它的作用是拦截登录请求并获取账号和密码,然后把账号密码封装到认证凭据 UsernamePasswordAuthenticationToken 中,然后把凭据交给特定配置的 AuthenticationManager 去作认证。

当然,此时凭证(Authentication)的状态是 “未认证” 状态。

那么,我们的问题是:AuthenticationManager 从哪儿来,它又是什么,它是如何对凭据进行认证的,认证成功的后续细节是什么,认证失败的后续细节是什么?

#5.2 AuthenticationManager

AuthenticationManager 会被 UsernamePasswordAuthenticationFilter 调用,它完成了用户信息的比对工作。

这个接口方法非常奇特,入参和返回值的类型都是 Authentication 。该接口的作用是对用户的『未认证』Token 进行认证,认证通过则返回『已认证』状态的 Token ,否则将抛出认证异常 AuthenticationException 。

提示

凭据(Authentication)就是由 UsernamePasswordAuthenticationFilter(通过 request.getParameter("username") 和 request.getParameter("password"))构造出来的。

也就是说,一个 Authentication Token 在『通过』AuthenticationManager 之后,它的状态会改变,从『未授信』变成『已授信』,或者抛出异常(表示认证失败)。

AuthenticationManager 是一个接口,我们用到的实际是它的实现类 ProviderManager 。

#5.3 AuthenticationProvider(了解)

明面上做用户信息的比对工作的是 AuthenticationManager,但是实际上干活的是 AuthenticationProvider 。从名称上显而易见,AuthenticationProvider 是 ProviderManager 的『小弟』。

ProviderManager 管理着多个 AuthenticationProvider ,而每一个 AuthenticationProvider 都代表着一个验证过程。

当某个 Provider 认为当前的凭证(Authentication)合法,可以从『未授信』转变为『已授信』,ProviderManager 就认定凭证合法,其状态可转变;

当所有的 Provider 都不认为凭证(Authentication)合法,ProviderManger 则以抛出异常的方式表明凭证非法。

注意

和 SecurityFilterChain 一样,常规项目(以普通的配置方式进行配置)中,你的 Provider 只有一个。

如果你需要你的环境中有多个 Provider ,那么需要以特殊的方式进行配置。

每一个 AuthenticationProvider 都只支持特定类型的 Authentication ,然后是对适配到的 Authentication 进行认证,只要有一个 AuthenticationProvider 认证成功,那么就认为认证成功,所有的都没有通过才认为是认证失败。

在 Spring Security 中 SecurityProvider 和 UserDetailsPasswordService 的关系是 1:1 的关系:

// 不能有多个。否则, 就中断。
UserDetailsService userDetailsService = getBeanOrNull( UserDetailsService.class); 
if (userDetailsService == null) { 
    return; 
}

...

DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); 
provider.setUserDetailsService(userDetailsService);

Copied!

DaoAuthenticationProvider 是 ProviderManager 的『小弟』之一。

注意

在一套配置中如果你存在多个 UserDetailsService 的 Spring Bean 将会影响 DaoAuthenticationProvider 的注入。

#6. 鉴权

FilterSecurityInterceptor 
├──> SecurityMetadataSource
└──> AccessDecisionManager
     └──> Voker

Copied!

鉴权的最终成果:请求被放行。

Security Filter Chain 中的最后一个过滤器是 FilterSecurityInterceptor(上图中橙色的那个过滤器),它是整个过滤器链的『守门员』,它的背后就是 Controller(在不考虑其它 Web Filter 的情况下)。

FilterSecurityInterceptor 完成了对凭证(Authentication)的鉴权工作。

#6.1 FilterSecurityInterceptor

当请求走到 FilterSecurityInterceptor 时,FilterSecurityInterceptor 会根据配置去判断当前的 Authentiion 是否满足当前 URI 的放行要求。

  • 如果当前的 Authentication 不满足通行要求,那么 FilterSecurityInterceptor 会抛出异常;
  • 如果当前的 Authentication 满足通行要求,那么 FilterSecurityInterceptor 就会放行请求,Controller 中的代码就会被触发执行。

我们来梳理一下接口权限的授权的流程:

  1. 当一个请求过来,我们先得知道这个请求的规则,即需要怎样的权限才能访问;
  2. 然后获取当前登录用户所拥有的权限;
  3. 再校验当前用户是否拥有该请求的权限;
  4. 用户拥有这个权限则正常返回数据,没有权限则拒绝请求。

Spring Security 将流程功能分得很细,每一个小功能都会有一个组件专门去做,我们要做的就是去配置或自定义这些组件!Spring Security 针对上述流程也提供了许多组件。

Spring Security 的授权发生在 FilterSecurityInterceptor 过滤器中:

  1. 首先调用的是 SecurityMetadataSource ,来获取当前请求的鉴权规则;

SecurityMetadataSource 中有一个 requestMap 中记录了我们的配置中所配置的所有的路径应该具有的访问权限。

SecurityMetadataSource 会遍历这个 Map ,去判断当前的 URI,符合那个 URI 规则(一旦发现有符合的,就立即结束循环遍历),并返回这个 URI 规则需要的访问权限。

  1. 然后通过上下文环境中的 Authentication 获取当前登录用户所有具有的权限: List<GrantedAuthority> 。上下文环境中的 Authentication 对象来在于认证功能;
  2. 再调用 AccessDecisionManager 来校验当前用户是否拥有该权限;

和 ProviderManager 一样,AccessDecisionManager 其实也是找它手下的『小弟(Voter)』来做校验工作。它手下的『小弟(Voter)』但凡有一个校验通过,那么 AccessDecisionManager 有认定通过校验。

  1. 如果有访问权就放行接口,没有则抛出异常,该异常会被 AccessDeniedHandler 处理。

#6.2 ExceptionTranslationFilter

Spring Security 有一个很有意思、很巧妙的设计,当 FilterSecurityInterceptor 判断 Authentication 不满足通行条件时,它会抛出异常(不同的『不满足』情况下会抛出不同的异常)。由于 filter 实质上是嵌套的内外层关系,所以这个异常就抛给了 FilterSecurityInterceptor 的前一个 filter:ExceptionTranslationFilter ,就上图中蓝色的那个。

ExceptionTranslationFilter 的核心代码逻辑就是一个 try ... catch ...,在捕获到异常之后,做不同的处理,例如跳转显示登陆页面,要求用户登录。

#7. 内置过滤器

spring security 在 org.springframework.security.config.annotation.web.builders.FilterComparator 中提供的规则进行比较按照比较结果进行排序注册。

FilterComparator() {
    Step order = new Step(INITIAL_ORDER, ORDER_STEP);
    put(ChannelProcessingFilter.class, order.next());
    put(ConcurrentSessionFilter.class, order.next());
    put(WebAsyncManagerIntegrationFilter.class, order.next());
    put(SecurityContextPersistenceFilter.class, order.next());
    ...
}

Copied!

spring security 中的默认的过滤器(如果被激活、启用的话),它在过滤器链上的位置和顺序就一定如上述规则所述。序号越小优先级越高。

接下来我们就对这些内置过滤器中常见的过滤器进行一个系统的认识。我们将按照默认顺序进行讲解。

#7.1 SecurityContextPersistenceFilter

SecurityContextPersistenceFilter 主要控制 SecurityContext 的在一次请求中的生命周期 。

  • 请求来临时,创建 SecurityContext 安全上下文信息;
  • 请求结束时清空 SecurityContextHolder 。

SecurityContextPersistenceFilter 通过 HttpScurity#securityContext() 及相关方法引入其配置对象 SecurityContextConfigurer 来进行配置。

#7.2 CsrfFilter

CsrfFilter 用于防止 csrf 攻击,前后端使用 json 交互需要注意的一个问题。

你可以通过 HttpSecurity.csrf() 来开启或者关闭它。在你使用 jwt 等 token 技术时,是不需要这个的。

#7.3 LogoutFilter

很明显它是处理注销的过滤器。

你可以通过 HttpSecurity.logout() 来定制注销逻辑,非常有用。

#7.4 UsernamePasswordAuthenticationFilter

处理用户以及密码认证的核心过滤器。认证请求提交的 username 和 password ,被封装成 token 进行一系列的认证,便是主要通过这个过滤器完成的,在表单认证的方法中,这是最最关键的过滤器。

你可以通过 HttpSecurity#formLogin() 及相关方法引入其配置对象 FormLoginConfigurer 来进行配置。

#7.5 DefaultLoginPageGeneratingFilter

生成默认的登录页。默认情况下,你访问 /login 所看到的内容就是它生成的 。

#7.6 DefaultLogoutPageGeneratingFilter

生成默认的退出页。 默认情况下,你访问 /logout 所看到的内容就是它生成的 。

#7.7 BasicAuthenticationFilter

Basic 身份验证是 Web 应用程序中流行的可选的身份验证机制。

BasicAuthenticationFilter 负责处理 HTTP 头中显示的基本身份验证凭据。这个 Spring Security 的 Spring Boot 自动配置默认是启用的 。

BasicAuthenticationFilter 通过 HttpSecurity#httpBasic() 及相关方法引入其配置对象 HttpBasicConfigurer 来进行配置。

#7.8 RequestCacheAwareFilter

用于用户认证成功后,重新恢复因为登录被打断的请求。当匿名访问一个需要授权的资源时。会跳转到认证处理逻辑,此时请求被缓存。在认证逻辑处理完毕后,从缓存中获取最开始的资源请求进行再次请求。

RequestCacheAwareFilter 通过 HttpScurity#requestCache() 及相关方法引入其配置对象 RequestCacheConfigurer 来进行配置。

#7.9 RememberMeAuthenticationFilter

处理『记住我』功能的过滤器。

RememberMeAuthenticationFilter 通过 HttpSecurity.rememberMe() 及相关方法引入其配置对象 RememberMeConfigurer 来进行配置。

#7.10 AnonymousAuthenticationFilter

匿名认证过滤器。对于 Spring Security 来说,所有对资源的访问都是有 Authentication 的。对于无需登录(UsernamePasswordAuthenticationFilter)直接可以访问的资源,会授予其匿名用户身份。

AnonymousAuthenticationFilter 通过 HttpSecurity.anonymous() 及相关方法引入其配置对象 AnonymousConfigurer 来进行配置。

#7.11 SessionManagementFilter

Session 管理器过滤器,内部维护了一个 SessionAuthenticationStrategy 用于管理 Session 。

SessionManagementFilter 通过 HttpScurity#sessionManagement() 及相关方法引入其配置对象 SessionManagementConfigurer 来进行配置。

#7.12 ExceptionTranslationFilter

我们常说的倒数第二个(实际上在注册表中是倒数第三个)过滤器。用来处理下一个过滤器(FilterSecurityInterceptor)所抛出的异常。

通常,它主要处理 2 种异常:

  • 如果下一个过滤器(FilterSecurityInterceptor)抛出的是 AuthenticationException 异常,ExceptionTranslationFilter 会触发 AuthenticationEntryPoint 的执行;
  • 如果下一个过滤器(FilterSecurityInterceptor)抛出的是 AccessDeniedException 异常,ExceptionTranslationFilter 会触发 AccessDeniedHandler 的执行;

#7.13 FilterSecurityInterceptor

我们常说的最后一个(实际上在注册表中是倒数第二个)过滤器。

这个过滤器决定了访问特定路径应该具备的权限,访问的用户的角色,权限是什么?访问的路径需要什么样的角色和权限?这些判断和处理都是由该类进行的。

FilterSecurityInterceptor 的大体放行逻辑如下:

  1. 『前面的』过滤器要在 SecurityContext 中存入一个 Authentication Token 。
  2. Authentication Token 的状态需要是『已认证』(isAuthenticated() == true);
  3. Authentication Token 中所包含的当前用户的权限信息,满足他所访问的当前 URI 的权限要求。