当前内容基于 spring-security 5.3 ,在升级到 6.0 后内容会随之变更。

这篇文字就站在整个 spring-security 的层面整体,快速地聊一下,目的是对这个框架有个比较感性的了解,大致知道这个框架有什么用,以及如何使用。

TL;DR点击列表跳转

  1. Spring Security
  2. 运行流程
  3. 构建流程
  4. 知识点
  5. 最后

1. Spring Security

提到安全我想如果简单的概括,我们要做的就是 “访问控制”,而访问控制的目标就是 “受保护的资源”。那么安全管理的功能点就可以简单的概括为 “认证管理”,“授权管理” 和 “资源管理”。

资源管理这个概念从编码角度来说是比较难以概括的,因为资源这个概念的划分本身就有很强的主观性以及具体的业务逻辑特色。在很多情况下,特别是对后端系统来说,资源被抽象成为访问路径(rest 接口)。这就一度让编码人员忽略“资源”这个概念的准确意义。(所以这里先不讨论资源管理)。

“认证管理”,“授权管理” 不仅是中文还是英文,如果没有思考过这两个概念我想都会感到困惑。

  1. 认证 :authentication 字面意思,就是允许进入系统。 认证逻辑在 spring-security 中由 AuthenticationManager 管理。
  2. 授权 :authorisation 在进入系统后授予指定资源的访问权限,根据资源的定义不同,可能是接口访问,方法调用,数据读取等。授权逻辑在 spring-security 中由 AccessDecisionManager 管理。

在一些入口管理比较严格的系统中,权限管理可能全靠编码人员的代码逻辑来实现,这种时候整个安全框架可能只需要 AuthenticationManager 就可以实现。不过这样的缺点就是缺少一个框架层面的强制性规范,且动态性较差。

由于大部分的应用场景都是 web 环境,这里就先忽略代码层级的访问控制(MethodSecurityMetadataSource),从过滤器 Filter 的角度开始说明。

我想大部分提到 spring-security 的地方都会出现这个图,就像这个图所描述的那样 spring-security 是一个基于过滤器 Filter 实现的框架。在 spring-web 核心模块内增加了 spring-security 的核心过滤器 FilterChaniProxy ,再由这个过滤器分发请求到框架内部的 SecurityFilterChain 中。在此基础上我们自己实现的各种逻辑最终都是由各种保存在 SecurityFilterChain 中的各种过滤器实现的。

2. 运行流程

如上图所示在框架启动后,spring-security 增加了上述十个过滤器,这些过滤器是实现 spring-security 功能的基础。

在 spring-security 5.3 中我们可以在初始化中控制不要这些过滤器,但是在比较新的 6.0 版本中为初始化必须。不过应该是可以在初始化完成后强制删除的(仅推测未验证),这点在后面 建造器配置器 章节中详细说明。

HttpServletRequestHttpServletResposne 所对应的网络交互经过这一系列的过滤器就完成了 spring-security 的逻辑和相应功能。这其中前九个过滤器就像上图描述的那样,要么提供协议层面的功能,要么提供框架层面的必须功能,第十个过滤器才是最终提供认证和授权,或者说是与我们的业务逻辑代码息息相关的部分。

AuthenticationManager 的最终实现 ProviderManager 向内部的 AuthenticationProvider 集合转发认证请求,将未认证的 Authentication 转换为已认证的 Authentication 并保存在上下文 SecurityContext 中供后续使用。我们要做的是增加自己的 AuthenticationProvider 实例对特定类型的 Authentication 提供认证功能。

在一些做业务隔离的场景中,可以通过定义不同的 Authentication 和与之搭配的 AuthenticationProvider 将认证流程转交不同的子模块。

在上面过滤器链全部执行完并且没有抛出 AuthenticationExceptionAccessDeniedException 时请求就会来到 rest 接口,也就是 Controller 层代码。正是基于过滤器的原因,使得整个认证与授权流程相对系统已有功能逻辑代码并没有直接影响和侵入。

在这套规范性下如果变更认证与授权,只需要操作过滤器链变更其中的过滤器即可。如 spring-security-oauth2 增加的 Oauth2 功能就添加了一系列 Filter ,这其中值得注意的是还会强制增加 BasicAuthenticationFilter (在 LogoutFilter 之后)。

3. 构建流程

如果想要了解 spring-security 的构建流程,实际上只需要从 @EnableWebSecurity 源码开始看即可。在这个过程中可以先忽略掉非主流程的对象注入与托管,因为框架中许多对象是动态创建和托管,又因为存在大量的概念以及对应的对象和代理对象,这在不熟悉源码时会显得非常杂乱。

图中 WebSecurityConfigurerAdapter 作为用户配置的核心对象 WebSecurity 以及 HttpSecurity 的入口,在 5.7.0 中已被弃用。在较新的 6.0 源码中将其拆分出 HttpSecurityConfiguration 配置类,并精简了大量之间对象的托管和获取,使得大量代码精简的同时也显得更加易读。

针对 web 的安全存在大量的配置项,其中包括大量的网络协议,以及 spring-security 本身功能。这就造成了核心配置对象 WebSecurityHttpSecurity 使用较为繁琐的问题。也就是在这个基础上增加了建造器(建造者模式 SecurityBuilder)以及配置器(建造者模式 SecurityConfigurer)专门进行对象构建。

这两个类的命名便于使用和理解,但是在分析代码逻辑之初可能会被大量的建造方法影响的无所适从。这里只需要记得从编码的角度来说,这两个类的命名应该是 WebSecurityBuilderHttpSecurityBuilder ,它们最终的建造目的就是构建出 FilterChainProxySecurityFilterChain

如果需要针对某个功能进行配置,就需要从建造器获得某个功能对应的配置器 XxxConfigurer ,并在配置完成后返回配置器形成一个较为流畅的链式调用。当然了如果要增加新的功能也可以增加自定义的配置器,将配置器添加大主建造器中。这样在建造器执行构建时 #build() 就可以进入框架的配置器生命周期,进而在自己的配置器中执行私有逻辑(大部分是增回自己的过滤器 Filter )。

4. 知识点

我觉得使用了建造者模式的 SecurityBuilder 可能是一开始接触 spring-security 印象较深的概念。这里罗列一下接触框架逻辑时可能需要注意的一些概念:

  1. ObjectPostProcessor :可以理解为对象后置处理器,不过需要注意的是尽管看起来有那么点眼熟,不过这不是 spring-framework 的内容。这个类在源码中出现可能一时间引起一些误解,不过它的作用仅仅是包装或替换输入的对象。而中其中管理的实例作用是将动态对象交由容器托管。
  2. FilterChainProxy :spring-security 的顶层过滤器(入口)。
  3. SecurityFilterChainFilterChainProxy 中管理的过滤器链(概念上),其作用是将过滤器根据配置分组。在匹配条件满足后将过滤器组交由 FilterChainProxy 处理。
  4. WebSecurityHttpSecurity :核心配置的建造器,它们本质上是一个保留配置的中间对象,一旦构建完成本身就失去了意义。不过在最新源码中可能是便于管理这两个类的实例已交由容器管理。
  5. AuthenticationManager :认证管理器。在接入认证功能时一定会接触的概念。这里的异常操作一般抛出 AuthenticationException
  6. AccessDecisionManager :授权管理器。匹配当前请求对应的权限标识,并通过投票的方式判断是否允许访问。这里的异常操作一般抛出 AccessDeniedException

最后由于最新的 spring-security 源码已经废弃了 WebSecurityConfigurerAdapter 类,将 WebSecurityHttpSercurity 的创建流程放入 WebSecurityConfigurationHttpSercurityConfiguration 管理,这样使得整个框架的初始化流程都变得清晰和简洁了起来。

5. 后记

这个系列文字的初衷是前一段时间思考访问控制的时候偶有所得,想重构一部分代码验证最终的可行性。由于之前认证部分基于 spring-security-oauth2 实现,这就导致了又看不懂之前的代码是什么逻辑。翻阅之前的笔记与文档时看起来也比较郁闷,所以想借此机会整个重新梳理一下 spring-security 的脉络。希望通过画图和不同层级视角的分级笔记来更加清晰的认识这个框架,以及在需要时迅速地做知识点定位。

当然还有一个原因是准备进行框架升级,不过框架升级除了框架本身代码的变更可能影响一部分功能外就是 JDK 的缘故,JDK 的升级会带来以下问题:

  1. 开发环境:从个人角度来说还是很希望看到新版 JDK 的功能与特性,这其中特别想体验一下本地镜像的功能。
  2. 运行环境:运行环境的 JDK 可能是最麻烦的问题,因为升级可能会影响其它使用 JRE 的应用。
  3. 环境兼容:正是因为运行环境的不确定性(可能至少是 JDK8),所以牵扯到运行时代码兼容问题。这点虽然之前在 Android 环境下有些规范的东西,也看到一些后端框架做的兼容尝试,但是总归没亲自做过还需验证。

顺带说一下也有一部分是 spring-authorization-server 这个项目的原因(小声)。