Spring Security
零、概述
Spring Security(简称SS)是一个高可用的、可自定义的身份认证和鉴权控制的框架。
类似的框架还有Shiro。
需求场景:
现今流行的web开发中,安全的第一位。
原本的鉴权开发流程:springweb自带的过滤器、拦截器等等。涉及到的方面
- 功能权限
- 访问权限
- 菜单权限
使用过滤器需要大量的原生代码——冗余。
所以为了解决这些问题,就需要框架来帮助我们实现。
SS 官网地址以及官方文档
官方地址spring.io/projects/spring-security
官方文档阅读地址:https://docs.spring.io/spring-security/reference/servlet/authentication/index.html#servlet-authentication
学习地址:kuangshenshuo 的B站视频
【【狂神说Java】SpringBoot整合SpringSecurity】https://www.bilibili.com/video/BV1KE411i7bC?vd_source=939c126663135132623f2393e41d7a8a
一、Spring Security快速开始
Spring Security使用的是面向切面编程的思想,也就是说不需要再刻意改动业务逻辑代码,只需要简单的配置,就可以快速接入使用。
1.1 Maven引入SS
直接上pom文件,推荐结合Springboot使用
<dependencies><!-- ... other dependency elements ... --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
</dependencies>
属性参数
<properties><!-- ... --><spring-security.version>6.1.4</spring-security.version><spring.version>6.0.11</spring.version>
</properties>
其他引入方式查看官网:https://docs.spring.io/spring-security/reference/getting-spring-security.html
官方提供的示例:
The completed application can be found in our samples repository. For your convenience, you can download a minimal Reactive Spring Boot + Spring Security application by [clicking here](https://start.spring.io/starter.zip?type=maven-project&language=java&packaging=jar&jvmVersion=1.8&groupId=example&artifactId=hello-security&name=hello-security&description=Hello Security&packageName=example.hello-security&dependencies=webflux,security).
完整的应用程序可以在我们的示例存储库中找到。 为了您的方便,您可以通过单击此处下载最小的反应式 Spring Boot + Spring 安全性应用程序。
1.2 Hello Web Security
二、认识Spring Security的重要特征(Features)
2.1 鉴权认证(Authentication)和 密码存储(Password Strorage)
SS提供了全面的身份验证,可以验证任何尝试访问特定资源的人员身份。一般指当用户输入账户密码后进行验证,验证身份后执行授权。
SS提供了单向转换的密码安全存储功能(无法提供双向密码验证),通常,使用PasswordEncoder
存储需要在身份验证时与用户提供的密码比较的密码。
2.1.1 密码存储历史
了解即可。
小结密码的发展史如下:
明文——单向哈希——盐+哈希——自适应函数——长期凭证+短期凭据(比如bcrypt密码加密配合token令牌)
多年来,存储密码的标准机制已经发展。 最初,密码以明文形式存储。 密码被认为是安全的,因为数据存储密码保存在访问它所需的凭据中。 但是,恶意用户能够通过使用SQL注入等攻击找到获取用户名和密码的大型“数据转储”的方法。 随着越来越多的用户凭据公开,安全专家意识到我们需要做更多的事情来保护用户的密码。
然后鼓励开发人员在通过单向哈希(例如SHA-256)运行密码后存储密码。 当用户尝试进行身份验证时,哈希密码将与他们键入的密码的哈希进行比较。 这意味着系统只需要存储密码的单向哈希。 如果发生违规,则仅公开密码的单向哈希。 由于哈希是单向的,并且在计算上很难猜测给定哈希的密码,因此不值得努力找出系统中的每个密码。 为了击败这个新系统,恶意用户决定创建称为彩虹表的查找表( Rainbow Tables)。 他们不是每次都猜测每个密码,而是计算一次密码并将其存储在查找表中。
为了降低彩虹表的有效性,鼓励开发人员使用加盐密码。 将为每个用户的密码生成随机字节(称为盐),而不是仅使用密码作为哈希函数的输入。 盐和用户的密码将通过哈希函数运行以生成唯一的哈希。 盐将以明文形式与用户密码一起存储。 然后,当用户尝试进行身份验证时,哈希密码将与存储的盐的哈希值和他们键入的密码进行比较。 独特的盐意味着彩虹表不再有效,因为每个盐和密码组合的哈希值都不同。
在现代,我们意识到加密哈希(如SHA-256)不再安全。 原因是使用现代硬件,我们可以每秒执行数十亿次哈希计算。 这意味着我们可以轻松地单独破解每个密码
现在鼓励开发人员利用自适应单向函数来存储密码。 使用自适应单向函数验证密码是有意占用大量资源的(它们有意使用大量 CPU、内存或其他资源)。 自适应单向功能允许配置一个“工作因子”,该因子可以随着硬件的改进而增长。 我们建议将“工作因子”调整为大约需要一秒钟来验证系统上的密码。 这种权衡是使攻击者难以破解密码,但又不会太昂贵,以免给您自己的系统带来过多的负担或激怒用户。
Spring Security 试图为“工作因素”提供一个良好的起点,但我们鼓励用户为自己的系统自定义“工作因素”,因为性能因系统而异。 应该使用的自适应单向函数的示例包括 bcrypt, PBKDF2, scrypt 以及 argon2。
由于自适应单向函数有意占用大量资源,因此验证每个请求的用户名和密码可能会显著降低应用程序的性能。 Spring 安全性(或任何其他库)无法加快密码验证的速度,因为安全性是通过使验证资源密集来获得的。 建议用户将长期凭据(即用户名和密码)交换为短期凭据(例如会话和 OAuth 令牌等)。 可以快速验证短期凭据,而不会造成任何安全性损失。
2.1.2 DelegatingPasswordEncoder委派密码编码器
SS提供了自己的解决方案
在 Spring Security 5.0 之前,默认的 PasswordEncoder
是 NoOpPasswordEncoder
(需要纯文本密码)。
相当于“密码存储历史”部分的 “BCryptPasswordEncoder”。 但是,这忽略了三个现实世界的问题:
- 许多应用程序使用无法轻松迁移(easily migrate)的旧密码。
- 密码存储的最佳做法永远都在更新。
- 作为一个框架,Spring Security不能经常进行重大更改。
为了解决这些问题,Spring Security 引入了 “DelegatingPasswordEncoder”,它通过以下方式解决了所有问题:
- 确保使用当前密码存储建议对密码进行编码
- 允许验证现代和传统格式的密码
- 允许未来升级编码方式
以下是官方提供的案例https://docs.spring.io/spring-security/reference/features/authentication/password-storage.html,只做了简单的搬运
您可以使用PasswordEncoderFactories
方法轻松构建一个DelegatingPasswordEncoder
的实例:
-
创建一个默认的委派密码编码器
PasswordEncoder passwordEncoder =PasswordEncoderFactories.createDelegatingPasswordEncoder();
-
或者,可以创建一个定义的委派密码编码器
String idForEncode = "bcrypt"; Map encoders = new HashMap<>(); encoders.put(idForEncode, new BCryptPasswordEncoder()); encoders.put("noop", NoOpPasswordEncoder.getInstance()); encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5()); encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()); encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1()); encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()); encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2()); encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()); encoders.put("sha256", new StandardPasswordEncoder());PasswordEncoder passwordEncoder =new DelegatingPasswordEncoder(idForEncode, encoders);
2.1.3 SS的密码存储格式
密码一般的存储格式如下(委派密码编码器的存储格式:
{id}encodedPassword
-
id
是用于查找"PasswordEncoder"应使用的标识符 -
encodedPassword
是所"PasswordEncoder"的原始编码密码。 -
id
必须位于密码的开头,以 开头{
,以 结尾}
。 -
如果没有找到
id
,意味着id被设置为空
以下具体举例,所有的原始密码都是“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
显而易见,**密码前段的大括号{}
作为标识符,存储的该密码的加密方式。**决定"PasswordEncoder"使用哪个来编码密码。
Note:一些用户可能担心存储格式是为潜在的黑客提供的。这不是一个问题,因为密码的存储不依赖于算法的秘密。此外,大多数格式在没有前缀的情况下很容易被攻击者识别出来。例如,BCrypt 密码通常以" 2 a 2a 2a"。
2.1.4 密码匹配问题
对应的,密码匹配问题也是根据大括号中的``id来决定使用哪种
PasswordEncoder`进行匹配的。
默认情况下,执行方法matches(CharSequence, String)
校验密码以及调用未映射的id
(包括 null id)的结果为产生异常IllegalArgumentException
可以使用以下方法进行定制化:
DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)
通过使用"id",我们可以匹配任何密码编码,甚至使用最现代的密码编码对密码进行编码。
这很重要,因为与加密不同,密码哈希的设计使得没有简单的方法可以恢复明文。由于无法恢复明文,因此很难迁移密码。为此我们选择了默认包含方便用户迁移的编码方式NoOpPasswordEncoder
"(根据上面的案例可以发现,noop是就是明文),以简化入门体验
2.1.5 快速体验SS的委派密码
如果您正在制作演示或样本,那么花时间对用户的密码进行哈希处理会有点麻烦。有一些便利的机制可以使这变得更容易,但这仍然不适合生产。
案例1:默认的密码编码案例(bcrypt)
UserDetails user = User.withDefaultPasswordEncoder().username("user").password("password").roles("user").build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
如果需要创建多个用户,也是可以复用这个构建器(不需要创建新的实例)
// 只创建一个UserBuilder
UserBuilder users = User.withDefaultPasswordEncoder();
// 构建不同的角色
UserDetails user = users.username("user").password("password").roles("USER").build();
UserDetails admin = users.username("admin").password("password").roles("USER","ADMIN").build();
尽管这已经对存储的密码进行哈希处理,但密码仍然暴露在内存和编译的源代码中。
因此,对于生产环境来说,它仍然不被认为是安全的。对于生产,您应该在外部对密码进行哈希处理(hash your passwords externally),原文推荐使用SpringBoot CLI。
2.1.6 常用故障排查
2.1.6.1 IllegalArgumentException
当存储的密码之一没有 id
时,会发生以下错误
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
解决方法:
解决此问题的最简单方法是弄清楚您的密码当前是如何存储的,并明确提供正确的PasswordEncoder
-
可以通过公开密码(noop)恢复到之前的加密方式。(个人理解就是跳过ss的用户加密,只套一个
{noop}
的壳 -
给所有的密码上加上正确的前缀(已知加密方式的情况下)
从 $2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 改成 {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
详细的映射列表参考: PasswordEncoderFactories
2.1.7 常见的案例
简单总结一下:使用的方法大致是以下几个方法
- 创建一个对应的编码器
- 使用编码器的
encode(String)
方法得到SS的加密结果- 使用编码器的
matches(原密码,加密后的密码)
方法进行密码匹配
2.1.7.1 BCryptPasswordEncoder
广泛支持的 bcrypt 算法对密码进行哈希处理。 为了使其更能抵抗密码破解,bcrypt故意变慢。 与其他自适应单向函数一样,应将其调整为大约需要 1 秒来验证系统上的密码。
推荐:根据系统硬件水平测试调整,设置密码强度使得校验密码需要约1秒。
// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
2.1.7.2 Argon2PasswordEncoder
Argon2是密码哈希竞赛的获胜者。 为了防止自定义硬件上的密码破解,Argon2 是一种故意缓慢的算法,需要大量内存。 与其他自适应单向函数一样,应将其调整为大约需要 1 秒来验证系统上的密码。
// 创建一个全默认的编码器
Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
2.1.7.3 其他PasswrodEncoder
还有大量其他 “PasswordEncoder” 实现完全是为了向后兼容而存在的。
它们都已弃用,以指示它们不再被视为安全。 但是,没有计划删除它们,因为很难迁移现有的遗留系统。
2.1.7.4 密码存储配置
SS默认使用委派密码编码器。
如果要使用原来的密码编码方式,就使用无操作密码编译器即可:
@Bean
public static NoOpPasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();
}
// 声明一个"NoOpPasswordEncoder"的bean 名称为"passwordEncoder".
2.1.8 修改密码配置
大多数应用不仅用户提交密码,还必须支持修改密码。
正常我们需要提供一个端口给管理员进行密码管理,你可以通过配置SS来提供该端口,比如原来的应用中修改密码的端口是/change-password
,你可以像这样配置SS:
http.passwordManagement(Customizer.withDefaults())
如果不是/change-password
,也可以像这样配置
http.passwordManagement((management) -> management.changePasswordPage("/update-password"))
具体用法如下:
要使这段代码生效,您需要在Spring Security的配置文件中进行相应的配置。具体步骤如下:
打开Spring Security的配置文件(通常是
SecurityConfig.java
或SecurityConfig.kt
)。在配置文件中找到适当的位置,添加以下代码片段:
http.passwordManagement(Customizer.withDefaults());
或者,如果您使用XML配置:
<http><password-management customizer-ref="org.springframework.security.config.Customizer#withDefaults" /> </http>
如果您的应用程序中的密码更改端点是
/change-password
,则无需进行其他配置。否则,您可以使用.changePassword().changePasswordUrl("/your-change-password-url")
方法来指定自定义的密码更改端点URL。保存并关闭配置文件。
这样,当您的应用程序启动时,Spring Security将根据您的配置自动启用密码管理功能,并提供一个标准的密码更改URL供密码管理器使用。