Appearance
第1章 Spring Security架构概览
Spring Security 虽然历史悠久,但是从来没有像今天这样受到开发者这么多的关注。究其原因,还是沾了微服务的光。作为Spring家族中的一员,在和 Spring 家族中的其他产品如 Spring Boot、Spring Cloud 等进行整合时,Spring Security 拥有众多同类型框架无可比拟的优势。本章我们就先从整体上了解一下 Spring Security 及其工作原理。
本章涉及的主要知识点有:
- Spring Security 简介。
- Spring Security 整体架构。
1.1 Spring Security简介
Java 企业级开发生态丰富,无论你想做哪方面的功能,都有众多的框架和工具可供选择,以至于 SUN 公司在早些年不得不制定了很多规范,这些规范在今天依然影响着我们的开发,安全领域也是如此。然而,不同于其他领域,在Java 企业级开发中,安全管理方面的框架非常少,一般来说,主要有三种方案:
- Shiro
- Spring Security
- 开发者自己实现
Shiro 本身是一个老牌的安全管理框架,有着众多的优点,例如轻量、简单、易于集成、可以在 JavaSE 环境中使用等。不过,在微服务时代,Shiro 就显得力不从心了,在微服务面前,它无法充分展示自己的优势。
也有开发者选择自己实现安全管理,据笔者所知,这一部分人不在少数。但是一个系统的安全,不仅仅是登录和权限控制这么简单,我们还要考虑各种各样可能存在的网络攻击以及防御策略,从这个角度来说,开发者自己实现安全管理也并非是一件容易的事情,只有大公司才有足够的人力物力去支持这件事情。
Spring Security 作为 Spring 家族的一员,在和 Spring 家族的其他成员如 Spring Boot、Spring Cloud 等进行整合时,具有其他框架无可比拟的优势,同时对 OAuth2 有着良好的支持,再加上 Spring Cloud 对 Spring Security 的不断加持(如推出 Spring Cloud Security),让 Spring Security不知不觉中成为微服务项目的首选安全管理方案。
陈年旧事:
Spring Security 最早叫 Acegi Security,这个名称并不是说它和 Spring 就没有关系,它依然是为 Spring 框架提供安全支持的。Acegi Security 基于 Spring,可以帮助我们为项目建立丰富的角色与权限管理系统。AcegiSecurity 虽然好用,但是最为人诟病的则是它臃肿烦琐的配置,这一问题最终也遗传给了 Spring Security。
Acegi Security 最终被并入 Spring Security 项目中,并于 2008年4月发布了改名后的第一个版本 Spring Security 2.0.0,截止本书写作时,Spring Security 的最新版本已经到了 5.3.4。
和 Shiro 相比,Spring Security 重量级并且配置烦琐,直至今天,依然有人以此为理由而拒绝了解 Spring Security。其实,自从 Spring Boot推出后,就彻底颠覆了传统了 JavaEE 开发,自动化配置让许多事情变得非常容易,包括Spring Security 的配置。在一个Spring Boot 项目中,我们甚至只需要引入一个依赖,不需要任何额外配置,项目的所有接口就会被自动保护起来了。在 Spring Cloud 中,很多涉及安全管理的问题,也是一个 Spring Security 依赖两行配置就能搞定,在和 Spring 家族的产品一起使用时,Spring Security 的优势就非常明显了。
因此,在微服务时代,我们不需要纠结要不要学习 Spring Security,我们要考虑的是如何快速掌握 Spring Security,并且能够使用 Spring Security 实现我们微服务的安全管理。
1.2 Spring Security核心功能
对于一个安全管理框架而言,无论是 Shiro 还是 Spring Security,最核心的功能,无非就是如下两方面:
- 认证
- 授权
通俗点说,认证就是身份验证(你是谁?),授权就是访问控制(你可以做什么?)
。
1.2.1 认证
Spring Security 支持多种不同的认证方式,这些认证方式有的是 Spring Security 自己提供的认证功能,有的是第三方标准组织制订的。SpringSecurity 集成的主流认证机制主要有如下几种:
- 表单认证。
- OAuth2.0 认证
- SAML2.0 认证。
- CAS 认证。
- RememberMe 自动认证。
- JAAS 认证。
- OpenI 去中心化认证。
- Pre-Authentication Scenarios 认证
- X509 认证。
- HTTP Basic 认证
- HTTP Digest 认证。
作为一个开放的平台,Spring Security 提供的认证机制不仅仅包括上面这些,我们还可以通过引入第三方依赖来支持更多的认证方式,同时,如果这些认证方式无法满足我们的需求,我们也可以自定义认证逻辑,特别是当我们和一些“老破旧”的系统进行集成时,自定义认证逻辑就显得非常重要了。
1.2.2 授权
无论采用了上面哪种认证方式,都不影响在 Spring Security 中使用授权功能。Spring Security 支持基于 URL 的请求授权、支持方法访问授权、支持 SpEL 访问控制、支持域对象安全(ACL),同时也支持动态权限配置、支持 RBAC 权限模型等,总之,我们常见的权限管理需求,Spring Security 基本上都是支持的。
1.2.3 其他
在认证和授权这两个核心功能之外,Spring Security 还提供了很多安全管理的“周边功能”这也是一个非常重要的特色。
大部分 Java 工程师都不是专业的 Web 安全工程师,自己开发的安全管理框架可能会存在大大小小的安全漏洞。而 Sprimg Security 的强大之处在于,即使你不了解很多网络攻击,只要使用了 Spring Security,它会帮助我们自动防御很多网络攻击,例如 CSRF 攻击、会话固定攻击等,同时 Spring Security 还提供了 HTTP 防火墙来拦截大量的非法请求。由此可见,研究Spring Security,也是研究常见的网络攻击以及防御策略。
对于大部分的 Java 项目而言,无论是从经济性还是安全性来考虑,使用 Spring Security 无疑是最佳方案。
1.3 Spring Security整体架构
在具体学习 Spring Security 各种用法之前,我们先介绍一下 Spring Security 中常见的概念,以及认证、授权思路,方便读者从整体上把握 Spring Security 架构,这里涉及的所有组件,在后面的章节中还会做详细介绍。
1.3.1 认证和授权
1.3.1.1 认证
在 Spring Security 的架构设计中,认证(Authentication)和授权(Authorization)是分开的,在本书后面的章节中读者可以看到,无论使用什么样的认证方式,都不会影响授权,这是两个独立的存在,这种独立带来的好处之一,就是 Spring Security 可以非常方便地整合一些外部的认证方案。
在 Spring Security 中,用户的认证信息主要由 Authentication 的实现类来保存,Authentication 接日定义如下:
java
package org.springframework.security.core;
import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
这里接口中定义的方法如下:
getAuthorities 方法:用来获取用户的权限。
getCredentials 方法:用来获取用户凭证,一般来说就是密码。
getDetails 方法:用来获取用户携带的详细信息,可能是当前请求之类等。
getPrincipal方法:用来获取当前用户,例如是一个用户名或者一个用户对象。
isAuthenticated:当前用户是否认证成功。
当用户使用用户名/密码登录或使用Remember-me 登录时,都会对应一个不同的Authentication 实例。
Spring Security 中的认证工作主要由 AuthenticationManager 接口来负责,下面来看一下该接口的定义:
java
package org.springframework.security.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public interface AuthenticationManager {
Authentication authenticate(Authentication var1) throws AuthenticationException;
}
AuthenticationManager 只有一个 authenticate 方法可以用来做认证,该方法有三个不同的返回值:
- 返回 Authentication,表示认证成功。
- 抛出 AuthenticationException 异常,表示用户输入了无效的凭证。
- 返回 mull,表示不能断定。
AuthenticationManager 最主要的实现类是 ProviderManager,ProviderManager 管理了众多的 AuthenticationProvider 实例,AuthenticationProvider 有点类似于 AuthenticationManager,但是它多了一个 supports 方法用来判断是否支持给定的 Authentication 类型。
java
package org.springframework.security.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public interface AuthenticationProvider {
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
由于 Authentication 拥有众多不同的实现类,这些不同的实现类又由不同的AuthenticationProvider 来处理,所以 AuthenticationProvider 会有一个 supports 方法,用来判当前的 Authentication Provider 是否支持对应的 Authentication。
在一次完整的认证流程中,可能会同时存在多个 AuthenticationProvider(例如,项目同时支持 form 表单登录和短信验证码登录),多个 AuthenticationProvider 统一由 ProviderManagel来管理。同时,ProviderManager 具有一个可选的 parent,如果所有的 AuthenticationProvidei都认证失败,那么就会调用 parent 进行认证。parent 相当于一个备用认证方式,即各个AuthenticationProvider 都无法处理认证问题的时候,就由 parent 出场收拾残局。
1.3.1.2 授权
当完成认证后,接下来就是授权了。在 Spring Security 的授权体系中,有两个关键接口:
- AccessDecisionMManager
- AccessDecisionVoter
AccessDecisionVoter 是一个投票器,投票器会检査用户是否具备应有的角色,进而投出赞成、反对或者弃权票;AccessDecisionManager 则是一个决策器,来决定此次访问是否被允许AccessDecisionVoter 和 AccessDecisionManager 都有众多的实现类,在 AccessDecisionManagei中会挨个遍历 AccessDecisionVoter,进而决定是否允许用户访问,因而 AccessDecisionVoter和 AccessDecisionManager 两者的关系类似于 AuthenticationProvider 和 ProviderManager 的关系。
在 Spring Security 中,用户请求一个资源(通常是一个网络接口或者一个 Java 方法)所需要的角色会被封装成一个 ConfigAttribute 对象,在 ConfigAttribute 中只有一个 getAttribute方法,该方法返回一个 String 字符串,就是角色的名称。一般来说,角色名称都带有一个 ROLE 前缀,投票器 AccessDecisionVoter 所做的事情,其实就是比较用户所具备的角色和请求某个资源所需的 ConfigAttribute 之间的关系。
1.3.2 Web安全
在 Spring Security 中,认证、授权等功能都是基于过滤器来完成的。表 1-1 列出了 Spring Security 中常见的过滤器,注意这里说的是否默认加载是指引入 Spring Security 依赖之后,开发者不做任何配置时,会自动加载的过滤器。
开发者所见到的 Spring Security 提供的功能,都是通过这些过滤器来实现的,这些过滤器按照既定的优先级排列,最终形成一个过滤器链。开发者也可以自定义过滤器,并通过@Order注解去调整自定义过滤器在过滤器链中的位置。
需要注意的是,默认过滤器并不是直接放在 Web 项目的原生过滤器链中,而是通过一个FilterChainProxy 来统一管理。Spring Security 中的过滤器链通过 FilterChainProxy 嵌入到 Web项目的原生过滤器链中,如图 1-1 所示。
在 Spring Security 中,这样的过滤器链不仅仅只有一个,可能会有多个,如图 1-2 所示。当存在多个过滤器链时,多个过滤器链之间要指定优先级,当请求到达后,会从FilterChainProxy 进行分发,先和哪个过滤器链匹配上,就用哪个过滤器链进行处理。当系统中存在多个不同的认证体系时,那么使用多个过滤器链就非常有效。
FilterChainProxy 作为一个顶层管理者,将统一管理 Security Filter。FilterChainProxy 本身将通过 Spring 框架提供的 DelegatingFilterProxy 整合到原生过滤器链中,所以图 1-2 还可以做进一步的优化,如图 1-3 所示。
1.3.3 登录数据保存
如果不使用 Spring Security 这一类的安全管理框架,大部分的开发者可能会将登录用户数据保存在 Session中,事实上,Spring Security也是这么做的。但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。
当用户登录成功后,Spring Security 会将登录成功的用户信息保存到 SecurityContextHolder中。SecurityContextHolder 中的数据保存默认是通过 ThreadLocal 来实现的,使用 ThreadLocal创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContextHolder 中的数据清空。以后每当有请求到来时,Spring Security 就会先从 Session 中取出用户登录数据,保存到 SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将 SecurityContextHolder 中的数据清空。
这一策略非常方便用户在 Controller 或者 Service 层获取当前登录用户数据,但是带来的另外一个问题就是,在子线程中想要获取用户登录数据就比较麻烦。Spring Security 对此也提供了相应的解决方案,如果开发者使用@Async 注解来开启异步任务的话,那么只需要添加如下配置,使用 Spring Security 提供的异步任务代理,就可以在异步任务中从 SecurityContextHolder 里边获取当前登录用户的信息:
java
@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {
@Override
public Executor getAsyncExecutor() {
return new DelegatingSecurityContextExecutorService(
Executors.newFixedThreadPool(5));
}
}
1.4 小结
本章主要介绍了 Spring Security 的基本原理与整体架构,方便读者从整体上把握 Spring Security 认证、授权的实现原理。在接下来的章节中,我们将继续详细介绍 Spring Security 认证与授权的每一个实现细节。