Spring Security 是一个提供身份验证、授权和防止常见攻击的框架。凭借对保护命令式和反应式应用程序的一流支持,它是保护基于 Spring 的应用程序的事实标准。
Spring Security 为身份验证提供了全面的支持。身份验证是我们验证试图访问特定资源的人的身份的方式。验证用户的常用方法是要求用户输入用户名和密码。一旦执行了身份验证,我们就知道身份并可以执行授权。
1、直接明文保存,比如用户设置的密码是“123456”,直接将“123456”保存在数据库中,这种是最简单的保存方式,也是最不安全的方式。但实际上不少互联网公司,都可能采取的是这种方式。
2、使用对称加密算法来保存,比如3DES、AES等算法,使用这种方式加密是可以通过解密来还原出原始密码的,当然前提条件是需要获取到密钥。不过既然大量的用户信息已经泄露了,密钥很可能也会泄露,当然可以将一般数据和密钥分开存储、分开管理,但要完全保护好密钥也是一件非常复杂的事情,所以这种方式并不是很好的方式。
3、使用MD5、SHA1等单向HASH算法保护密码,使用这些算法后,无法通过计算还原出原始密码,而且实现比较简单,因此很多互联网公司都采用这种方式保存用户密码,曾经这种方式也是比较安全的方式,但随着彩虹表技术
的兴起,可以建立彩虹表进行查表破解,目前这种方式已经很不安全了。
4、特殊的单向HASH算法,由于单向HASH算法在保护密码方面不再安全,于是有些公司在单向HASH算法基础上进行了加盐、多次HASH等扩展,这些方式可以在一定程度上增加破解难度,对于加了“固定盐”的HASH算法,需要保护“盐”不能泄露,这就会遇到“保护对称密钥”一样的问题,一旦“盐”泄露,根据“盐”重新建立彩虹表可以进行破解,对于多次HASH,也只是增加了破解的时间,并没有本质上的提升。
5、PBKDF2算法,该算法原理大致相当于在HASH算法基础上增加随机盐,并进行多次HASH运算,随机盐使得彩虹表的建表难度大幅增加,而多次HASH也使得建表和破解的难度都大幅增加。该算法也是美国国家标准与技术研究院推荐使用的算法。
6、bcrypt、scrypt等算法,这两种算法也可以有效抵御彩虹表,使用这两种算法时也需要指定相应的参数,使破解难度增加。
Spring Security 的PasswordEncoder接口用于执行密码的单向转换,以允许安全地存储密码。
现在鼓励开发人员利用自适应单向函数来存储密码。使用自适应单向函数验证密码是有意占用资源(即 CPU、内存等)的。自适应单向功能允许配置“工作因数”,该“工作因数”会随着硬件变得更好而增长。建议将“工作因素”调整为大约需要 1 秒来验证系统上的密码。这种权衡是为了让攻击者难以破解密码,但成本不会太高,它会给您自己的系统带来过多的负担。Spring Security 试图为“工作因素”提供一个良好的起点,但鼓励用户为自己的系统自定义“工作因素”,因为性能会因系统而异。bcrypt、PBKDF2、scrypt和argon2。
由于自适应单向函数有意占用大量资源,因此为每个请求验证用户名和密码会显着降低应用程序的性能。Spring Security(或任何其他库)无法加速密码验证,因为通过使验证资源密集来获得安全性。鼓励用户将长期凭证(即用户名和密码)交换为短期凭证(即会话、OAuth 令牌等)。可以快速验证短期凭证,而不会损失任何安全性。
示例 1. 创建默认 DelegatingPasswordEncoder
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
示例 2. 创建自定义 DelegatingPasswordEncoder
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);
示例 3. DelegatingPasswordEncoder 存储格式
{id}encodedPassword
示例 4. DelegatingPasswordEncoder 编码密码示例
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
传递给构造函数的idForEncode参数决定了哪个PasswordEncoder将用于编码密码。在DelegatingPasswordEncoder我们上面构造的中,这意味着编码的结果password将被委托给BCryptPasswordEncoder并带有前缀{bcrypt}
如果您正在制作演示或示例,花时间对用户的密码进行哈希处理会有点麻烦。有一些便利机制可以使这更容易,但这仍然不适合生产。
示例 6. withDefaultPasswordEncoder 示例
User user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
.build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
如果您要创建多个用户,您还可以重用构建器。
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();
这确实对存储的密码进行哈希处理,但密码仍然暴露在内存和编译的源代码中。因此,对于生产环境,它仍然不被认为是安全的。对于生产,您应该在外部散列您的密码。
正确编码密码的最简单方法是使用Spring Boot CLI。
该BCryptPasswordEncoder实现使用广泛支持的bcrypt算法来散列密码。为了使其更能抵抗密码破解,bcrypt 故意放慢了速度。与其他自适应单向功能一样,应该将其调整为大约 1 秒来验证系统上的密码。使用BCryptPasswordEncoderBCryptPasswordEncoder的 Javadoc 中提到的强度 10的默认实现。鼓励您在自己的系统上调整和测试强度参数,以便大约需要 1 秒来验证密码。
// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
Spring Security 默认使用DelegatingPasswordEncoder。但是,这可以通过将 公开PasswordEncoder为 Spring bean 来定制。
例如
@Bean
public static PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
Spring Security 提供针对常见漏洞的保护。只要有可能,默认情况下就会启用保护。您将在下面找到 Spring Security 所防御的各种漏洞的高级描述。
Spring 为防止跨站请求伪造 (CSRF)攻击提供全面支持。
Spring 提供了两种机制来防止 CSRF 攻击:
即token方案。
防止 CSRF 攻击的主要和最全面的方法是使用Synchronizer Token Pattern。这个解决方案是为了确保每个 HTTP 请求除了我们的会话 cookie 之外,还需要一个名为 CSRF 令牌的安全随机生成值必须存在于 HTTP 请求中。
提交 HTTP 请求时,服务器必须查找预期的 CSRF 令牌并将其与 HTTP 请求中的实际 CSRF 令牌进行比较。如果值不匹配,则应拒绝 HTTP 请求。
这个工作的关键是实际的 CSRF 令牌应该在浏览器不自动包含的 HTTP 请求的一部分中。例如,在 HTTP 参数或 HTTP 标头中要求实际的 CSRF 令牌将防止 CSRF 攻击。在 cookie 中要求实际的 CSRF 令牌不起作用,因为 cookie 会自动包含在浏览器的 HTTP 请求中。
一种防止CSRF 攻击的新兴方法是在 cookie 上指定SameSite 属性。服务器可以SameSite在设置 cookie 时指定该属性,以指示当来自外部站点时不应发送 cookie。
例如,带有属性的 HTTP 响应标头SameSite可能如下所示:
示例 6. SameSite HTTP 响应
Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax
SameSite该属性的有效值为:
将SameSite属性设置为Strict提供更强的防御,但可能会使用户感到困惑。考虑一个保持登录在https://social.example.com上的社交媒体网站的用户。用户在https://email.example.org收到一封电子邮件,其中包含指向社交媒体网站的链接。如果用户点击该链接,他们理所当然地期望通过社交媒体网站的身份验证。但是,如果SameSite属性是Strictcookie,则不会发送,因此不会对用户进行身份验证。
另一个明显的考虑是,为了SameSite保护用户的属性,浏览器必须支持该SameSite属性。大多数现代浏览器都支持 SameSite 属性。但是,仍在使用的旧浏览器可能不会。
因此,一般建议将SameSite属性作为纵深防御,而不是单独防御 CSRF 攻击。