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

1.密码加密方案进化史

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

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

密码加盐之后,彩虹表的作用就大打折扣了,因为唯一的盐和明文密码总会生成唯一的Hash字符。

然而,随着计算机硬件的发展,每秒执行数十亿次Hash计算已经变得轻轻松松,这意味着即使给密码加密加盐也不再安全。

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

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

2.PasswordEncoder详解

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

public interface PasswordEncoder {String encode(CharSequence rawPassword);boolean matches(CharSequence rawPassword, String encodedPassword);default boolean upgradeEncoding(String encodedPassword) {return false;}
}

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

  1. encode:该方法用来对明文密码进行加密。

  2. matches:该方法用来进行密码比对。

  3. upgradeEncoding:该方法用来判断当前密码是否需要升级,默认返回false表示不需要升级。

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

2.1 PasswordEncoder常见实现类

BCryptPasswordEncoder

BCryptPasswordEncoder使用bcrypt算法对密码进行加密,为了提高密码的安全性,bcrypt算法故意降低运行速度,以增强密码破解的难度。同时BCryptPasswordEncoder “为自己带盐”,开发者不需要额外维护一个“盐”字段,使用BCryptPasswordEncoder加密后的字符串就已经“带盐”了,即使相同的明文每次生成的加密字符串都不相同。

BCryptPasswordEncoder的默认强度为10,开发者可以根据自己的服务器性能进行调整,以确保密码验证时间约为1秒钟(官方建议密码验证时间为1秒钟,这样既可以提高系统安全性,又不会过多影响系统运行性能)。

Argon2PasswordEncoder

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

Pbkdf2PasswordEncoder

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

SCryptPasswordEncoder

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

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

除了上面介绍的这几种之外,还有一个非常重要的密码加密工具类,那就是DelegatingPasswordEncoder。

2.2 DelegatingPasswordEncoder

根据前文的介绍,读者可能会认为Spring Security中默认的密码加密方案应该是四种自适应单向加密函数中的一种,其实不然,在Spring Security 5.0之后,默认的密码加密方案其实是DelegatingPasswordEncoder。

从名字上来看,DelegatingPasswordEncoder是一个代理类,而并非一种全新的密码加密方案。

DelegatingPasswordEncoder主要用来代理上面介绍的不同的密码加密方案。为什么采用DelegatingPasswordEncoder而不是某一个具体加密方式作为默认的密码加密方案呢?主要考虑了如下三方面的因素:

  1. 兼容性:使用DelegatingPasswordEncoder可以帮助许多使用旧密码加密方式的系统顺利迁移到Spring Security中,它允许在同一个系统中同时存在多种不同的密码加密方案。

  2. 便捷性:密码存储的最佳方案不可能一直不变,如果使用DelegatingPasswordEncoder作为默认的密码加密方案,当需要修改加密方案时,只需要修改很小一部分代码就可以实现。

  3. 稳定性:作为一个框架,Spring Security不能经常进行重大更改,而使用Delegating PasswordEncoder可以方便地对密码进行升级(自动从一个加密方案升级到另外一个加密方案)。

那么DelegatingPasswordEncoder到底是如何代理其他密码加密方案的?又是如何对加密方案进行升级的?我们就从PasswordEncoderFactories类开始看起,因为正是由它里边的静态方法createDelegatingPasswordEncoder提供了默认的DelegatingPasswordEncoder实例:

public class PasswordEncoderFactories {public static PasswordEncoder createDelegatingPasswordEncoder() {String encodingId = "bcrypt";Map<String, PasswordEncoder> encoders = new HashMap<>();encoders.put(encodingId, new BCryptPasswordEncoder());encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());encoders.put("scrypt", new SCryptPasswordEncoder());encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());encoders.put("argon2", new Argon2PasswordEncoder());return new DelegatingPasswordEncoder(encodingId, encoders);}private PasswordEncoderFactories() {}
}

可以看到,在createDelegatingPasswordEncoder方法中,首先定义了encoders变量,encoders中存储了每一种密码加密方案的id和所对应的加密类,例如bcrypt对应着BcryptPassword Encoder、argon2对应着Argon2PasswordEncoder、noop对应着NoOpPasswordEncoder。

encoders创建完成后,最终新建一个DelegatingPasswordEncoder实例,并传入encodingId和encoders变量,其中encodingId默认值为bcrypt,相当于代理类中默认使用的加密方案是BCryptPasswordEncoder。

我们来分析一下DelegatingPasswordEncoder类的源码,由于源码比较长,我们就先从它的属性开始看起:

public class DelegatingPasswordEncoder implements PasswordEncoder {private static final String PREFIX = "{";private static final String SUFFIX = "}";private final String idForEncode;private final PasswordEncoder passwordEncoderForEncode;private final Map<String, PasswordEncoder> idToPasswordEncoder;private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
}
  1. 首先定义了前缀PREFIX和后缀SUFFIX,用来包裹将来生成的加密方案的id。

  2. idForEncode表示默认的加密方案id。

  3. passwordEncoderForEncode表示默认的加密方案(BCryptPasswordEncoder),它的值是根据idForEncode从idToPasswordEncoder集合中提取出来的。

  4. idToPasswordEncoder用来保存id和加密方案之间的映射。

  5. defaultPasswordEncoderForMatches是指默认的密码比对器,当根据密码加密方案的id无法找到对应的加密方案时,就会使用默认的密码比对器。defaultPasswordEncoderForMatches的默认类型是UnmappedIdPasswordEncoder,在UnmappedIdPasswordEncoder的matches方法中并不会做任何密码比对操作,直接抛出异常。

  6. 最后看到的DelegatingPasswordEncoder也是PasswordEncoder接口的子类,所以接下来我们就来重点分析PasswordEncoder接口中三个方法在DelegatingPasswordEncoder中的具体实现。首先来看encode方法:

@Override
public String encode(CharSequence rawPassword) {return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
}

encode方法的实现逻辑很简单,具体的加密工作还是由加密类来完成,只不过在密码加密完成后,给加密后的字符串加上一个前缀{id},用来描述所采用的具体加密方案。因此,encode方法加密出来的字符串格式类似如下形式:

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}123
{pbkdf2}23b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4

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

再来看密码比对方法matches:

@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {if (rawPassword == null && prefixEncodedPassword == null) {return true;}String id = extractId(prefixEncodedPassword);PasswordEncoder delegate = this.idToPasswordEncoder.get(id);if (delegate == null) {return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);}String encodedPassword = extractEncodedPassword(prefixEncodedPassword);return delegate.matches(rawPassword, encodedPassword);
}
private String extractId(String prefixEncodedPassword) {if (prefixEncodedPassword == null) {return null;}int start = prefixEncodedPassword.indexOf(PREFIX);if (start != 0) {return null;}int end = prefixEncodedPassword.indexOf(SUFFIX, start);if (end < 0) {return null;}return prefixEncodedPassword.substring(start + 1, end);
}

在matches方法中,首先调用extractId方法从加密字符串中提取出具体的加密方案id,也就是{}中的字符,具体的提取方式就是字符串截取。拿到id之后,再去idToPasswordEncoder集合中获取对应的加密方案,如果获取到的为null,说明不存在对应的加密实例,那么就会采用默认的密码匹配器defaultPasswordEncoderForMatches;如果根据id获取到了对应的加密实例,则调用其matches方法完成密码校验。

可以看到,这里的matches方法非常灵活,可以根据加密字符串的前缀,去查找到不同的加密方案,进而完成密码校验。同一个系统中,加密字符串可以使用不同的前缀而互不影响。

最后,我们再来看一下DelegatingPasswordEncoder中的密码升级方法upgradeEncoding:

@Override
public boolean upgradeEncoding(String prefixEncodedPassword) {String id = extractId(prefixEncodedPassword);if (!this.idForEncode.equalsIgnoreCase(id)) {return true;}else {String encodedPassword = extractEncodedPassword(prefixEncodedPassword);return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);}
}

可以看到,如果当前加密字符串所采用的加密方案不是默认的加密方案(BcryptPassword Encoder),就会自动进行密码升级,否则就调用默认加密方案的upgradeEncoding方法判断密码是否需要升级。至此,我们将Spring Security中的整个加密体系向读者简单介绍了一遍,接下来我们通过几个实际的案例来看一下加密方案要怎么用。


以上内容节选自松哥的新书《深入浅出 Spring Security》,他和磊哥是老乡,也是认识很久的朋友了,同时他也是《Spring Boot+Vue全栈开发实战》一书的作者,非常低调和务实的技术大佬 ,最后推荐一波他的新书,非常值得一读。

彩蛋

为了感谢各位读者朋友的长期支持,此评论区下留言,磊哥送 5 本松哥的新书《深入浅出 Spring Security》,需要的小伙伴赶紧留言吧,当然,脸熟和经常留言的朋友中奖几率更大。

附彩蛋|Spring Security 竟然故意延长登录时间?知道真相的我惊呆了!相关推荐

  1. 5.Spring Security 短信验证码登录

    Spring Security 短信验证码登录 在 Spring Security 添加图形验证码一节中,我们已经实现了基于 Spring Boot + Spring Security 的账号密码登录 ...

  2. Spring Security默认的用户登录表单 页面源代码

    Spring Security默认的用户登录表单 页面源代码 <html><head><title>Login Page</title></hea ...

  3. 关于Spring Security框架 关于单点登录sso

    1.Spring Security的作用 Spring Security主要解决了认证和授权相关的问题. 认证(Authenticate):验证用户身份,即登录. 授权(Authorize):允许用户 ...

  4. Spring Security 短信验证码登录(5)

    在Spring Security添加图形验证码中,我们已经实现了基于Spring Boot + Spring Security的账号密码登录,并集成了图形验证码功能.时下另一种非常常见的网站登录方式为 ...

  5. Spring Security OAuth2 SSO 单点登录

    基于 Spring Security OAuth2 SSO 单点登录系统 SSO简介 单点登录(英语:Single sign-on,缩写为 SSO),又译为单一签入,一种对于许多相互关连,但是又是各自 ...

  6. thinkcmf5调用指定分类的二级_Tengine快速上手系列教程amp;视频:基于Python API的图片分类应用入门丨附彩蛋...

    前言:近期,Tengine团队加班加点,好消息接踵而来,OpenCV 4.3.0发布,OPEN AI LAB AIoT智能开发平台Tengine与OpenCV合作共同加速边缘智能,Tengine再获业 ...

  7. 东方明珠胡俊:「东方明珠数据中台」四年发展历史全解(内附彩蛋)

    在以"矩·变"为主题的 2019 神策数据驱动大会现场,东方明珠新媒体资深研发总监胡俊发表了名为<东方明珠数据中台>的主题演讲.以下内容根据其现场演讲整理所得,文末可免 ...

  8. ChatGPT会砸了谷歌的饭碗吗?(附彩蛋)

    按:本来早上准备好了文章<ChatGPT创始人自述成功的13个法宝>,但是微信公众号无法发表,所以只能发到刘教链的知识星球(链接:https://t.zsxq.com/0arpqby2N[ ...

  9. 开启sketchup超速云渲染模式,文末附彩蛋!

    2019年3月,渲云效果图客户端开放支持sketchup软件--一款专门配合CG工作人员设计过程研发的3D模型设计软件,可以创作出草稿.线稿.透视.渲染等不同显示模式,可将自己的手绘风格融入到模型表现 ...

最新文章

  1. centos 7 安装docker 并设置阿里云镜像仓库
  2. 数字图像处理频域滤波实现低通与高通滤波(包含matlab代码)
  3. Android官方开发文档Training系列课程中文版:手势处理之记录手指移动的轨迹
  4. Go1.17新特性 ,给我们带来了10%的性能提升
  5. CUDA TOOlkit Programming Guide 2. Programming Model
  6. 排查Linux机器是否已经被入侵
  7. java textarea 自动滚动条_月光软件站 - 编程文档 - Java - 如何实现滚动条的自动滚动到textarea的末尾...
  8. 程序员没有那么多996!
  9. gif动图怎么制作?gif动图制作教程大全
  10. MATLAB矩阵基础知识(二)
  11. 一文了解驱动程序及更新方法
  12. Sql Server Report Builder 计算标准偏差
  13. 1.12 DICOM彩色图像
  14. ML_12 Sum-Produkt Networks 和积网络
  15. android11obb,exagear安卓11数据包obb
  16. Win10系统bhound7.sys蓝屏故障修复
  17. java timsort_JDK(二)JDK1.8源码分析【排序】timsort
  18. 粒子群优神经网络优化
  19. 工业4.0下IOT融合技术核心OT节点
  20. android miui优化,现在的小米手机怎么优化MIUI

热门文章

  1. SCRUM Beta Day 2
  2. adonis命令serve
  3. 机器人首次自主实施对猪腹腔镜手术
  4. 计算机的云是什么意思_什么是云计算云计算是什么意思
  5. 副驾驶的意义_副驾驶在飞行中的作用与地位
  6. Perl:化繁为简 (转载)
  7. 磁盘无法打开,格式是RAW
  8. 阿里云 龙珠机器学习训练营Task1:机器学习算法(一): 基于逻辑回归的分类预测
  9. 3CLpro-抗新冠病毒药物研究首选靶点
  10. 天魔心法之——识人篇