Spring Security 官方文档学习(1)—— 初步认知及PasswordEncoder

1,158

Spring Security 官网文档

简介: 在官方介绍中,我们可以感受到 Spring Security 是一个验证以及访问控制框架,为 Java 项目提供验证授权、防范常见攻击的功能。它被作为 Spring 项目的安全标准,集成了 servlet API,并可以灵活定制。

前提条件:

  • Spring Security 需要 Java 8 或更高的运行环境。
  • 官方特意提了一下该框架是自包含的(self-contained manner)。理解为我们可以将某个 jar/war 直接从一个系统拷贝到另一个系统,可立即正常工作,即不需要额外的配置文件。

社区: Spring Security Community 可用于查找相关问题、小例子、直接提问反馈Bug

源码: 托管在 GitHub,源码

5.5.1 版本: 增加了 JWT 验证、并完整支持 JDK 11。

1、验证(Authentication)

刚刚看到验证、授权我并没有很清晰了解两者的区别:正好看一下官网的解释:

  • 验证:Authentication is how we verify the identity of who is trying to access a particular resource. A common way to authenticate users is by requiring the user to enter a username and password. Once authentication is performed we know the identity and can perform authorization.

image.png

上述介绍可以理解到,这里存在一个先后顺序。

  • 验证完成后,我们确定了身份信息。
  • 授权,即指定这个身份可以做什么。

2、密码存储

上面提到通过用户名、密码的方式来验证用户,用户名密码是存在数据库中的,Spring Security 应该可以将从数据库中读取的用户信息先存起来,用于后续的对比。

PasswordEncoder接口:通过一个单向的密码转换,保证安全的密码存储。当需要双向转换时就不用这个接口了。通常,PasswordEncoder 用于存储密码,在身份验证时需要与用户提供的密码进行比较。

密码存储的发展过程: 1、以纯文本形式存储,但是恶意用户通过SQL注入等方式转存数据。2、以密码的单向哈希值存储,即使暴露也是哈希过的密码,但是恶意用户通过建立查找表仍可破解。3、在用户密码的基础上加上盐(随机字节)再进行单向哈希,存储哈希值,这样盐和用户密码的组合很多,无法建立查找表。

  • 上述的密码存储方法对于现代的计算能力来说都不安全了,直接暴力破解。所以我们开始考虑一种自适应的单向函数去存储密码。

所谓自适应,就是为相应的单向函数设计了一个可变的“工作因子”,它将与当今计算能力成正比。所以开发者将控制采用什么程度的工作因子,官方建议采用 1S 的验证时间作为标准,这样比较均衡。

自适应单向函数: bcrypt、PBKDF2、scrypt、argon2 等。

注意: 因为自适应单向函数有意占用系统计算资源,故会明显降低系统性能。任何框架都无法在这里加速验证,因为验证的安全性就是与计算量成正比的。那么一个比较好的方式就是,将这种长期凭证 (i.e. username and password) 尽量用短期凭证 (i.e. session, OAuth Token, etc) 替换,因为可以快速验证短期凭证,而不造成安全损失。

3、DelegatingPasswordEncoder

在 Spring Security 5.0 之前默认一直使用的 PasswordEncoder 就是NoOpPasswordEncoder 也就是明文密码。这直接导致了最新的 Spring Security 默认还是它。官方说了一系列现实的原因,一个框架毕竟不能做太多破坏性的操作,要考虑到老用户的感受,以及框架的全面性。

所以 Spring Security 团队给出的解决方案是现在的默认编码器为 DelegatingPasswordEncoder。在我看来它就是一个综合性的编码器:

创建默认的 DelegatingPasswordEncoder:

PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

创建自定义的 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);

从这里可以看出,DelegatingPasswordEncode 采用一个 HashMap 来存储编码器,其实就是单向转换函数的名字以及编码器的对应存储,相应的密码存储格式也是如此:

{id}encodedPassword

id 就是名字,encodedPassword 就是相应编码器编码后的字符串。

如果原始密码为 “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,编码后的密码为 $2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG,在匹配时这一整体字符串将委托给BCryptPasswordEncoder


密码编码: 传递给构造函数的 idForEncode 确定将使用哪个 PasswordEncoder 来编码密码。在我们上面构造的 DelegatingPasswordEncoder,这意味着 BCryptPasswordEncoder 进行编码任务,并以 {bcrypt} 为前缀。最终结果如下所示:

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

来一个编码小例子:

User user = User.withDefaultPasswordEncoder()
  .username("user")
  .password("password")
  .roles("user")
  .build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

这样做在内存中以及编译后代码中会暴露密码,在开发环境中是不安全的!所以我们应该从外部散列密码(我理解为直接在数据库密码列中直接存编译后的密码)

4、BCryptPasswordEncoder

BCryptPasswordEncoder 实现使用广泛支持的 bcrypt 算法来散列密码。为了使 bcrypt 更能抵抗密码破解,它故意放慢速度。与其他自适应单向函数一样,应该将其调整为在系统上验证密码大约需要1秒的时间。BCryptPasswordEncoder 的默认实现使用强度10,如 BCryptPasswordEncoder。建议在自己的系统上调优和测试强度参数,这样验证密码大约需要 1 秒的时间。

// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

这里,myPassword 可以是用户从前端输入来的明文密码,result 可以是数据库中的编译后密码。

5、存储配置

Spring Security 默认使用 DelegatingPasswordEncoder。但是,这可以通过将 PasswordEncoder 暴露为 Spring bean 来进行定制。