• Spring Security密码加密


    本文内容来自王松老师的《深入浅出Spring Security》,自己在学习的时候为了加深理解顺手抄录的,有时候还会写一些自己的想法。

            在前面的学习中,凡是涉及到密码的地方我们都采用明文加密存储,实际项目中肯定是不可取的,因为这会带来极高的安全风险。在企业级应用中,密码不仅需要加密,还需要加盐,最大程度上保证密码的安全。学完这一篇之后,大家就会明白前面我们一直使用的{noop}是什么意思了。

    密码为什么要加密

            2011年12月21日,有人在网络上公布了一个包含600万用户资料的数据库,数据库全部为明文存储,包含用户名、密码以及注册的邮箱。事件发生后CSDN在微博、官方等网站渠道发出了声明,解释说此数据库系2009年备份所用,因不明原因泄漏,已向警方报案,后来又在官网发出了公开道歉信。整个事件最触目惊心的莫过于CSDN把用户的密码明文的方式存储,由于很多用户是多个网站共用一个密码,因此一个网站密码泄漏就会造成很大的安全隐患。由于有了这么多的前车之鉴,我们现在做系统时,密码都要加密存储。

    密码加密方案进化史

            最早期我们使用类似SHA-256这样的单向的Hash算法。用户注册成功后,保存在数据库中欧的不再是用户明文密码,而是经过SHA-256加密计算后的一个字符串,当用户登录时,我们在将用户输入的明文密码用SHA-256进行加密,加密完成之后,在和存储在数据库中的密码进行对比,进而确定登录信息是否正确。如果系统遭到攻击,最多只是存储在数据库中的密文被泄漏。

            这样就安全了吗?当然不是。彩虹表是一个用于加密Hash函数逆运算的表,通常用于破解加密过的Hash字符串。为了降低彩虹表对系统安全性的影响,现在再添加一个随机数(即盐)和密码明文混合在一起进行加密。这样即使密码明文相同,生成的加密字符串也是不同的。当然,这个随机数也需要以明文的方式和密码一起存储在数据库中。当用户登录时,用到用户输入的明文密码和存储在数据库中的盐一起进行Hash运算,在将运算结果和存储在数据库中的密文进行比较,进而确定用户的登录信息是否有效。

            密码加了盐之后彩虹表就大打折扣了,因为盐和明文密码总会生成唯一的Hash字符。然而,随着计算机每秒数十亿次的Hash计算已经变的很轻松,这就意味着即使给密码加盐也不再安全。

            在Spring Security中,我们现在是用一种自适应单向函数(Adaptive One-way Function)来处理密码问题,这种自适应单向函数在进行密码匹配时,会有意占用大量系统资源(例如CPU、内存),这样可以增加恶意用户攻击系统的难度。在Spring Security中,开发者可以通过bcrypt、PBKDF2、scrypt以及argon2来体验这种自适应单向函数加密。

            由于自适应单向函数有意占用大量系统资源,因此每次登陆认证请求都会大大降低应用程序的性能,但是Spring Security不会采用任何措施来提高密码验证速度,因为它正是通过这种方式来增强系统的安全性。当然,开发者也可以将用户名/密码这种长期凭证兑换成短期凭证,如会话、OAuth2令牌等,这样既可快速验证用户凭证信息,又不会损失系统的安全性。

    PasswordEncoder详解

            Spring Security中通过PasswordEncoder接口定义了密码加密和对比的相关操作:

    1. public interface PasswordEncoder {
    2. String encode(CharSequence rawPassword);
    3. boolean matches(CharSequence rawPassword, String encodedPassword);
    4. default boolean upgradeEncoding(String encodedPassword) {
    5. return false;
    6. }
    7. }

            可以看到,PasswordEncoder接口中一共三个方法:

    • encod方法:该方法用来给明文密码进行加密
    • matches方法:该方法用来进行密码对比
    • upgradeEncoding方法:该方法用来判断当前密码是否需要升级,默认返回false表示不需要升级

            针对密码的所有操作,PasswordEncoder接口中都定义好了,不同的实现类将采用不同的密码加密方案对密码进行处理。

    PasswordEncoder常见实现类

    1. BCryptPasswordEncoder:BCryptPasswordEncoder 使用bcrypt算大对密码进行加密,为了提高密码的安全性,bcrypt算法故意降低了运算速度,以增强破解密码的难度。同时BCryptPasswordEncoder “为自己带盐”,开发者不需要额外维护一个“盐”字段,使用BCryptPasswordEncoder 加密后的字符串就已经“带盐”了,实际相同的明文每次加密生成的密文字符串都不相同。BCryptPasswordEncoder 默认的强度为10,开发者可以根据自己服务器的性能进行调整,以确保验证密码时间约为1秒钟(官方建密码验证时间为1秒钟,这样既可以提高系统的安全性,又不会过多的影响系统的运行性能)。

    2. Argon2PasswordEncoder:Argon2PasswordEncoder使用Argon2算法对密码进行加密,Argon2 曾在Password Hashing Competition竞赛中获胜。为了解决在定制硬件上 密码容易被破解的问题,Argon2也是故意降低运算速度,同时需要大量 内存,以确保系统的安全性。

    3. Pbkdf2PasswordEncoder:Pbkdf2PasswordEncoder使用PBKDF2算法对密码进行加密,和前面 几种类似,PBKDF2算法也是一种故意降低运算速度的算法,当需要 FIPS(Federal Information Processing Standard,美国联邦信息处理标 准)认证时,PBKDF2算法是一个很好的选择。

    4. SCryptPasswordEncoder:SCryptPasswordEncoder使用scrypt算法对密码进行加密,和前面的 几种类似,scrypt也是一种故意降低运算速度的算法,而且需要大量内存。

            这四种就是我们前面所说的自适应单向函数加密。除了这几种,还 有一些基于消息摘要算法的加密方案,这些方案都已经不再安全,但是 出于兼容性考虑,Spring Security并未移除相关类,主要有 LdapShaPasswordEncoder、MessageDigestPasswordEncoder、 Md4Password Encoder、StandardPasswordEncoder以及 NoOpPasswordEncoder(密码明文存储),这五种皆已废弃,这里对这 些类也不做过多介绍。

            

    DelegatingPasswordEncoder

            除了上面学习的几种加密方式,还有一个非常重要的加密工具类,那就是:DelegatingPasswordEncoder。

            从名字上看DelegatingPasswordEncoder是一个代理类,而并非一种全新的密码加密方案。DelegatingPasswordEncoder主要是用来代理前面学习的密码加密方案的。为什么采用DelegatingPasswordEncoder而不是具体某一种加密方式作为默认的加密方案呢?主要是考虑了如下因素:

    • 兼容性:使用DelegatingPasswordEncoder可以帮助许多旧的密码加密方式的系统迁移到Spring Security中,它允许在同一个系统中同时存在多种不同的密码加密方案。
    • 便捷性:密码存储的最佳方案不可能一层不变,如果使用DelegatingPasswordEncoder作为默认的密码加密方案,当需要修改加密方案时,只需要修改很小的一部分就可以实现。
    • 稳定性:作为一个框架,Spring Security不能经常进行重大的更改,而是用DelegatingPasswordEncoder可以方便的进行升级(自动从一个加密方案升级到另外一个加密方案)

            那么DelegatingPasswordEncoder到底是如何代理其他密码加密方案的呢?我们就从PasswordEncoderFactories来看起,应为正式由它的静态方法createDelegatingPasswordEncoder提供了默认的DelegatingPasswordEncoder实例:

    1. public final class PasswordEncoderFactories {
    2. private PasswordEncoderFactories() {
    3. }
    4. public static PasswordEncoder createDelegatingPasswordEncoder() {
    5. String encodingId = "bcrypt";
    6. Map encoders = new HashMap();
    7. encoders.put(encodingId, new BCryptPasswordEncoder());
    8. encoders.put("ldap", new LdapShaPasswordEncoder());
    9. encoders.put("MD4", new Md4PasswordEncoder());
    10. encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
    11. encoders.put("noop", NoOpPasswordEncoder.getInstance());
    12. encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
    13. encoders.put("scrypt", new SCryptPasswordEncoder());
    14. encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
    15. encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
    16. encoders.put("sha256", new StandardPasswordEncoder());
    17. encoders.put("argon2", new Argon2PasswordEncoder());
    18. return new DelegatingPasswordEncoder(encodingId, encoders);
    19. }
    20. }

            这里的代码非常明确,声明了一个Map,用来装各种加密方案的实例和它的id。例如BCryptPasswordEncoder示例对应的id为bcrypt。创建好encoders这个Map对象后,最终会创建一个DelegatingPasswordEncoder的实例,将encoders这个装着各种加密方案实例的Map传给DelegatingPasswordEncoder,并且DelegatingPasswordEncoder的默认加密方案为:BCryptPasswordEncoder。

            我们来学习下DelegatingPasswordEncoder的源码,由于DelegatingPasswordEncoder的源码比较长,我们先来看看他的属性:

    1. public class DelegatingPasswordEncoder implements PasswordEncoder {
    2. private static final String DEFAULT_ID_PREFIX = "{";
    3. private static final String DEFAULT_ID_SUFFIX = "}";
    4. private final String idPrefix;
    5. private final String idSuffix;
    6. private final String idForEncode;
    7. private final PasswordEncoder passwordEncoderForEncode;
    8. private final Map idToPasswordEncoder;
    9. private PasswordEncoder defaultPasswordEncoderForMatches;
    10. }
    • 首先定义了前缀PREFIX和SUFFIX,用来包裹将来生成的加密方案的id
    • idForEncode表示默认的加密方案的id
    • passwordEncoderForEncode表示系统默认的加密方案(BCryptPasswordEncoder),它的值是根据idForEncode从idToPasswordEncoder集合中提取出来的
    • idToPasswordEncoder用来保存id和加密方案之间的映射
    • defaultPasswordEncoderForMatches 是指默认的密码比对器,
      当根据密码加密方案的 id 无法找到对应的加密方案时,就会使用默认的
      密码比对器。 defaultPasswordEncoderForMatches 的默认类型是 UnmappedIdPasswordEncoder ,在 UnmappedIdPasswordEncoder matches
      方法中并不会做任何密码比对操作,直接抛出异常
    • 最后我们看到DelegatingPasswordEncoder也是PasswordEncoder接口的实现类。所以我们重点来看看DelegatingPasswordEncoder的encode方法和matches方法

            首先来看encode方法:

    1. public String encode(CharSequence rawPassword) {
    2. return this.idPrefix + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword);
    3. }

            这里比较简单,具体的加密工作还是又加密类来完成,只不过在加密完成之后,在给密文前面加上一个前缀+id+后缀,形如:{id}。用来描述具体的加密方案,因此,encode方法加密出来的字符串格式形如:

    1. {bcrypt}$2a$10$cfuvD57gmX6lWE2W1ztUcev7TwngI90N8hMFxpPI6.sIPlWzr6bJ.
    2. {noop}123

            不同的前缀后面代表了的字符串采用了不同的加密方案。

            我们再来看看matches方法:

    1. public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
    2. if (rawPassword == null && prefixEncodedPassword == null) {
    3. return true;
    4. } else {
    5. String id = this.extractId(prefixEncodedPassword);
    6. PasswordEncoder delegate = (PasswordEncoder)this.idToPasswordEncoder.get(id);
    7. if (delegate == null) {
    8. return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
    9. } else {
    10. String encodedPassword = this.extractEncodedPassword(prefixEncodedPassword);
    11. return delegate.matches(rawPassword, encodedPassword);
    12. }
    13. }
    14. }

            matches方法也是比较简单的,首次用extractId方法从加密的字符串里面提取出加密方案的id,也就是{}中间包裹的加密方案的id。拿到id之后从idToPasswordEncoder集合中获取到加密方案的实例,如果没有获取到就调用默认defaultPasswordEncoderForMatches的matches方法,如果获取到对应的实例,则调用其matches方法完成密码校验。

            到此,Spring Security中的大部分加密体系已经学习的差不多了。下一篇文章我们来学习下PasswordEncoder如何使用。

  • 相关阅读:
    OpenGL ES freeglut 下载和使用
    Vue 前端 实现 HTML 转 PDF 并导出(方案一:html2canvas + jspdf 前端直接实现)
    动画详解常用排序算法(1)
    adb shell 指令集
    从new File("")到jdk源码
    win11开机动画关闭教程
    利用 Docker 编译 OpenWrt
    GP与LP的区别,有限责任、无限责任、无限连带责任
    前后端分离页面(从数据库到前端、后端手把手教你如何搭建 -- 功能实现:增加查询)
    js-20网络编程
  • 原文地址:https://blog.csdn.net/qq_27062249/article/details/128027585