前言
Spring Security
是一个提供认证(authentication)、授权(authorization)和防御各种Web攻击的框架,它对命令式和反应式应用程序都提供了一流的支持,是保护基于spring的应用程序的事实标准。
在项目中使用Spring Security
要求你有Java8
或更高的运行环境。由于Spring Security
旨在以自包含的方式操作,所以不需要在Java运行时环境中放置任何特殊的配置文件。特别是,你不需要配置特殊的Java身份验证和授权服务(JAAS
)策略文件,也不需要将Spring安全性放在公共类路径位置。
类似地,如果使用EJB
容器或Servlet
容器,则不需要将任何特殊配置文件放在任何地方,也不需要将Spring安全性包含在服务器类加载器中。所有必需的文件都包含在你的应用程序中。
这种设计提供了最大的部署时间灵活性,因为您可以将目标包(可能是JAR、WAR或EAR)从一个系统复制到另一个系统,并且可以立即工作。
Spring Security
遵循Apache 2.0协议,github
上的源码地址: https://github.com/spring-projects/spring-security/,有志研究源码的读者可以直接克隆下来好好研究。spring-security
的最新版本为5.4.1版本,需要进一步了解新版本特性的读者可移步官网(https://docs.spring.io/spring-security/site/docs/current/reference/html5/#features)查看
Spring Security 集成到 Maven 项目中
SpringBoot
提供了spring-boot-starter-security
启动器,它包含了与spring-security
相关的所有依赖。最简便和优先的方式是通过IDE
集成工具(Eclipse, IntelliJ, NetBeans)中使用Spring Initializr 或者通过Spring
官网 https://start.spring.io 在线构建依赖spring-boot-starter-security
模块的项目,然后下载到本地后使用IDE工具导入工程。
当然,也可以在pom.xml
手动加入spring-security
的依赖,示例如下
(1) spring boot项目中手工引入依赖项:
pom.xml
<properties>
<spring-security.version>5.4.1</spring-security.version>
</properties>
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-security.version}</version>
</dependency>
</dependencies>
(2) SSM 项目中手工引入依赖项:
pom.xml
<properties>
<spring-security.version>5.4.1</spring-security.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-bom</artifactId>
<version>{spring-security-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
使用Spring-Security的最小Maven依赖集合如下:
pom.xml
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
</dependencies>
Spring Security 5.4.1
构建于Spring Framework 5.2.9
版本之上。但通常应该与Spring Framework 5.x
的任何新版本兼容。为了避免spring security
依赖不同版本的spring framework
产生冲突,最简单的方法是在你的pom .xml
的部分中使用spring-framework-bom
,如下面的例子所示:
pom.xml
<dependencyManagement>
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>5.2.9.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Maven 仓库
(1) 使用快照仓库
pom.xml
<repositories>
<!-- ... possibly other repository elements ... -->
<repository>
<id>spring-snapshot</id>
<name>Spring Snapshot Repository</name>
<url>https://repo.spring.io/snapshot</url>
</repository>
</repositories>
(2) 使用里程碑仓库
代码语言:javascript复制<repositories>
<!-- ... possibly other repository elements ... -->
<repository>
<id>spring-milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
Spring Security 集成到 Gradle项目中
(1) 在 Spring Boot项目中引入依赖
build.gradle
ext['spring-security.version']='5.4.1'
ext['spring.version']='5.2.9.RELEASE'
dependencies {
compile "org.springframework.boot:spring-boot-starter-security"
}
(2) 在 SSM 项目中引入依赖
build.gradle
plugins {
id "io.spring.dependency-management" version "1.0.6.RELEASE"
}
dependencyManagement {
imports {
mavenBom 'org.springframework.security:spring-security-bom:5.4.1'
}
}
使用spring-security-bom
是为了确保在整个项目中使用一致的spring-security
版本
在 SSM 项目中使用spring-security
的最小依赖集合如下:
build.gradle
dependencies {
compile "org.springframework.security:spring-security-web"
compile "org.springframework.security:spring-security-config"
}
Maven
仓库
1) 使用GA版本(版本号以.RELEASED结尾)仓库
build.gradle
repositories {
mavenCentral()
}
(2) 使用快照版本仓库
build.gradle
repositories {
maven { url 'https://repo.spring.io/snapshot' }
}
(3) 使用里程碑版本仓库
build.gradle
repositories {
maven { url 'https://repo.spring.io/milestone' }
}
Spring Security中的Authentication(认证)
spring security
提供了用于认证、授权和保护应用受到常见的各种恶意攻击的全面支持,同时也提供了与第三方库的集成,并简化了其应用。
Authentication
(认证) 是指我们以何种方式识别访问特定资源者的身份,常用的方式是要求用户在访问前输入用户名和密码。一旦执行完了认证操作,我们就确认了访问者的身份,并可以进一步执行授权操作。
Spring Security中的密码存储
Spring Security
的PasswordEncoder
接口是用来执行密码单向加密后安全存储的一种方式。既然PasswordEncoder
是单向加密,那么当密码需要反向解密时时就不打算使用它。PasswordEncoder
的典型使用场景是存储的密码需要在用户认证时与用户提供的密码进行比对。
当前,互联网用户的账户安全越来越重要,很多用户的账号绑定了自己的邮箱、手机号、第三方支付等,而且很多用户为图省事会把多个网站的账户登录密码设置成同一个密码。这时候一旦用户的密码被黑客攻破,那么黑客就可以尝试使用获得的用户名和密码登录用户与银联相关的账户,一旦破解就很可能会给用户带来巨大的财产损失。因此,用户账号的安全问题是web开发者需要重点关注的地方。
密码加密存储历史
多年来,存储密码的标准机制一直在发展。在开始时,密码以明文存储。密码被认为是安全的,因为密码保存需要凭据才能访问的数据库中。然而,恶意用户能够通过SQL
注入之类的攻击找到获取用户名和密码大量“数据转储”的方法。随着越来越多的用户凭证成为公共安全专家意识到我们需要做更多的保护用户的密码。
然后,开发人员被鼓励通过单向哈希(如SHA-256)来存储密码。当用户尝试进行身份验证时,散列后的密码将与他们键入的密码的散列进行比较。这意味着系统只需要存储密码的单向散列。如果发生了泄露,那么只有一种方式的密码散列被暴露。由于散列是一种方法,而且计算上很难猜测给定的散列密码,因此不值得花力气计算系统中的每个密码。为了破解这个新系统,恶意用户决定创建名为彩虹表的查找表。他们不会每次都猜测每个密码,而是计算一次密码并将其存储在一个查找表中。
为了降低彩虹表的有效性,鼓励开发人员使用加盐密码。不是只使用密码作为哈希函数的输入,而是为每个用户的密码生成随机字节(称为盐)。盐和用户的密码将通过哈希函数运行,该函数将生成唯一的哈希值。盐将以明文与用户密码一起存储。然后,当用户尝试进行身份验证时,将把散列后的密码与存储的盐的散列和用户键入的密码进行比较。惟一的盐意味着彩虹表不再有效,因为每种盐和密码组合的哈希值都不同。
在现代,我们意识到加密哈希(如SHA-256)不再安全。原因是,使用现代硬件,我们可以在一秒钟内执行数十亿次哈希计算。这意味着黑客可以轻松地破解每个密码。
现在鼓励开发人员利用自适应单向函数来存储密码。使用自适应单向函数验证密码是有意的资源密集型(例如CPU、内存等)。一个自适应的单向函数允许配置一个“工作系数”,它可以随着硬件的改进而增长。建议将“工作系数”调优为用1秒左右的时间验证系统上的密码。这种权衡使得攻击者很难破解密码,但又不会给您自己的系统带来过多的负担。Spring Security
试图为“工作系数”提供一个良好的起点,但鼓励用户为自己的系统定制“工作因素”,因为不同系统的性能会有很大差异。应该使用的自适应单向函数的例子包括bcrypt、PBKDF2、scrypt和argon2
。
由于自适应单向函数有意地耗费资源,因此为每个请求验证用户名和密码将显著降低应用程序的性能。Spring Security
(或任何其他库)无法加速密码的验证,因为安全性是通过强化验证资源来获得的。鼓励用户将长期凭证(即用户名和密码)交换为短期凭证(即会话、OAuth令牌等)。短期证书可以快速验证而不损失安全性。
Spring Security 中的默认密码编码器 DelegatingPasswordEncoder
在 spring security 5.0
之前,默认的PasswordEncoder
接口实现类是NoOpPasswordEncoder
,它要求纯文本密码。了解了密码加密存储历史,你可能会认为默认的PasswordEncoder
接口实现类是BCryptPasswordEncoder
。然而这样想忽略了三个现实的问题:
- 有许多使用旧密码编码的应用程序不能轻易迁移
- 密码存储的最佳实践将再次更改
- 作为一个框架,
Spring Security
不能频繁地进行破重大更改
因此Spring Security
引入了DelegatingPasswordEncoder
这个加密器通过以下三种方式解决了以上存在的三个问题:
- 确保使用当前密码存储建议对密码进行编码
- 允许验证现代和传统格式的密码
- 允许在将来升级编码
你可以通过PasswordEncoderFactories
类轻易构造DelegatingPasswordEncoder
实例,示例如下:
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
同样,你可以通过下面的方式创建自定义的实例:
代码语言:javascript复制String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());
PasswordEncoder passwordEncoder =
new DelegatingPasswordEncoder(idForEncode, encoders);
密码存储格式
常规的密码存储格式如下:
代码语言:javascript复制{id}encodedPassword
"id "是用来查找使用哪种PasswordEncoder
的标识符,"encodedPassword"是使用选中的密码编码器编码后的密码;id 必须包裹在 "{ }"之中,如果 id 找不到,则 id 会为空。下面的例子是使用不同的 id 编码后的列表,原始密码都i是 "password":
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05 bXxvuu/1qZ6NUR xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
- id 为 bcrypt 匹配的密码编码器为
BCryptPasswordEncoder
- id 为 noop 匹配的密码编码器为
NoOpPasswordEncoder
- id 为 pbkdf2 匹配的密码编码器为
Pbkdf2PasswordEncoder
- id 为 scrypt 匹配的密码编码器为
SCryptPasswordEncoder
- id 为 sha256 匹配的密码编码器为
StandardPasswordEncoder
密码匹配
匹配是基于{id}以及id到构造函数中提供的PasswordEncoder的映射来完成的。我们的密码存储格式示例提供了一个工作示例,说明如何做到这一点。默认情况下,调用matches(CharSequence, String)
方法时传入一个password
参数和一个未匹配的id 参数(包括Id 为null)将导致IllegalArgumentException
异常。可以使用DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)
自定义此行为。
通过使用id,我们可以匹配任何密码编码,但是使用最现代的密码编码来编码密码。这一点很重要,因为与加密不同,密码散列被设计成没有恢复明文的简单方法。由于无法恢复明文,因此很难迁移密码。虽然用户迁移NoOpPasswordEncoder
很简单,但我们选择默认包含它,以便于入门体验。
如果您正在准备一个演示或示例,那么花时间散列用户的密码会有点麻烦。有一些方便的机制可以简化这一点,但不建议用于生产环境。
代码语言:javascript复制User user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
.build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
如果你需要创建多个用户,你也可以重用builder
UserBuilder users = User.withDefaultPasswordEncoder();
User user = users
.username("user")
.password("password")
.roles("USER")
.build();
User admin = users
.username("admin")
.password("password")
.roles("USER","ADMIN")
.build();
几种常用密码编码器
(1) BCryptPasswordEncoder
BCryptPasswordEncoder
实现了使用广泛支持的bcrypt算法对密码进行散列。为了使它更抵抗密码破解,bcrypt故意缓慢。与其他自适应单向函数一样,应该将其调优为大约1秒来验证系统上的密码。
BCryptPasswordEncoder
的默认实现使用强度10,正如在BCryptPasswordEncoder
的Javadoc中提到的那样。建议你在自己的系统上调优和测试强度参数,以便验证密码大约需要1秒。
BCryptPasswordEncoder
的用法示例如下:
// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
(2) Argon2PasswordEncoder
Argon2PasswordEncoder
实现了使用Argon2算法对密码进行散列。Argon2是密码哈希竞争的获胜者。为了在定制的硬件上击败密码破解,Argon2是一个故意缓慢的算法,它需要大量的内存。与其他自适应单向函数一样,应该将其调优为大约1秒来验证系统上的密码。Argon2PasswordEncoder
的当前实现需要BouncyCastle
接口
Argon2PasswordEncoder
的用法示例如下:
// Create an encoder with all the defaults
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
(3) Pbkdf2PasswordEncoder
Pbkdf2PasswordEncoder
实现了使用PBKDF2算法对密码进行散列。为了破解密码,PBKDF2是一个故意缓慢的算法。与其他自适应单向函数一样,应该将其调优为大约1秒来验证系统上的密码。当需要FIPS
认证时,该算法是一个很好的选择。
Pbkdf2PasswordEncoder
的用法示例如下:
// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
(4)SCryptPasswordEncoder
SCryptPasswordEncoder
实现了使用scrypt算法对密码进行散列。为了破解自定义硬件上的密码,scrypt是一种故意缓慢的算法,它需要大量的内存。与其他自适应单向函数一样,应该将其调优为大约1秒来验证系统上的密码。
SCryptPasswordEncoder
的用法示例如下:
// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
小结
本文主要介绍了Spring Security
中的认证和密码编码器等重要概念,概括为以下几点:
Spring Security
安全框架集成到Maven
构建和Gradle
构建的Spring Boot
项目 和非Spring Boot
项目中的方式`Spring Security
安全框架通常通过用户名和密码认证用户访问资源的合法性,并进一步确定受否给认证用户授权- 为保护用户的信息安全,
Spring Security
要求对密码存储采用密码编码器,框架默认的密码编码器是DelegatingPasswordEncoder
Spring Security
中有4种重要的密码编码器,它们分别是BCryptPasswordEncoder、Argon2PasswordEncoder、Pbkdf2PasswordEncoder、SCryptPasswordEncoder
,四种编码器都是PasswordEncoder
接口的实现类,各自采用了不同的加密算法。
在下一篇Spring Security
系列的文章中,笔者将结合Spring Boot
项目演示使用spring security
框架对访问用户进行 Basic 认证和表单登录认证
参考阅读
spring security
官方文档: https://docs.spring.io/spring-security/site/docs/current/reference/html5/
---END---