Spring Security密码加密
admin
2024-02-13 16:18:07
0

本文内容来自王松老师的《深入浅出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接口定义了密码加密和对比的相关操作:

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

        可以看到,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实例:

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

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

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

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

        首先来看encode方法:

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

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

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

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

        我们再来看看matches方法:

 public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {if (rawPassword == null && prefixEncodedPassword == null) {return true;} else {String id = this.extractId(prefixEncodedPassword);PasswordEncoder delegate = (PasswordEncoder)this.idToPasswordEncoder.get(id);if (delegate == null) {return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);} else {String encodedPassword = this.extractEncodedPassword(prefixEncodedPassword);return delegate.matches(rawPassword, encodedPassword);}}}

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

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

相关内容

热门资讯

linux入门---制作进度条 了解缓冲区 我们首先来看看下面的操作: 我们首先创建了一个文件并在这个文件里面添加了...
C++ 机房预约系统(六):学... 8、 学生模块 8.1 学生子菜单、登录和注销 实现步骤: 在Student.cpp的...
A.机器学习入门算法(三):基... 机器学习算法(三):K近邻(k-nearest neigh...
数字温湿度传感器DHT11模块... 模块实例https://blog.csdn.net/qq_38393591/article/deta...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
Redis 所有支持的数据结构... Redis 是一种开源的基于键值对存储的 NoSQL 数据库,支持多种数据结构。以下是...
win下pytorch安装—c... 安装目录一、cuda安装1.1、cuda版本选择1.2、下载安装二、cudnn安装三、pytorch...
MySQL基础-多表查询 文章目录MySQL基础-多表查询一、案例及引入1、基础概念2、笛卡尔积的理解二、多表查询的分类1、等...
keil调试专题篇 调试的前提是需要连接调试器比如STLINK。 然后点击菜单或者快捷图标均可进入调试模式。 如果前面...
MATLAB | 全网最详细网... 一篇超超超长,超超超全面网络图绘制教程,本篇基本能讲清楚所有绘制要点&#...
IHome主页 - 让你的浏览... 随着互联网的发展,人们越来越离不开浏览器了。每天上班、学习、娱乐,浏览器...
TCP 协议 一、TCP 协议概念 TCP即传输控制协议(Transmission Control ...
营业执照的经营范围有哪些 营业执照的经营范围有哪些 经营范围是指企业可以从事的生产经营与服务项目,是进行公司注册...
C++ 可变体(variant... 一、可变体(variant) 基础用法 Union的问题: 无法知道当前使用的类型是什...
血压计语音芯片,电子医疗设备声... 语音电子血压计是带有语音提示功能的电子血压计,测量前至测量结果全程语音播报࿰...
MySQL OCP888题解0... 文章目录1、原题1.1、英文原题1.2、答案2、题目解析2.1、题干解析2.2、选项解析3、知识点3...
【2023-Pytorch-检... (肆十二想说的一些话)Yolo这个系列我们已经更新了大概一年的时间,现在基本的流程也走走通了,包含数...
实战项目:保险行业用户分类 这里写目录标题1、项目介绍1.1 行业背景1.2 数据介绍2、代码实现导入数据探索数据处理列标签名异...
记录--我在前端干工地(thr... 这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前段时间接触了Th...
43 openEuler搭建A... 文章目录43 openEuler搭建Apache服务器-配置文件说明和管理模块43.1 配置文件说明...