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

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》,需要的小伙伴赶紧留言吧,当然,脸熟和经常留言的朋友中奖几率更大。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544882.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DirectX 矩阵

基础&#xff1a; 下标&#xff1a;第一个下标为该元素所在行的索引&#xff0c;第二个下标为该元素所在列的索引。如下图所示 行向量和列向量&#xff1a;只有单行的向量称为行向量&#xff0c;只有单列的称之为列向量。 相等 维数和元素都相等 数乘(与标量相乘) 每一个元素与…

div 图片滚动 / 文字滚动

今天研究了一下图片滚动&#xff0c;网上有很多可以使用的例子&#xff0c;所以先是找了一个用的是表格布局的&#xff0c;如下&#xff1a;<!DOCTYPE html> <html xmlns"http://www.w3.org/1999/xhtml"> <head><meta http-equiv"Content-…

为什么阿里内部不允许用Executors创建线程池?

来源&#xff1a;cnblogs.com/zjfjava/p/11227456.html1. 通过Executors创建线程池的弊端在创建线程池的时候&#xff0c;大部分人还是会选择使用Executors去创建。下面是创建定长线程池&#xff08;FixedThreadPool&#xff09;的一个例子&#xff0c;严格来说&#xff0c;当使…

Java FilePermission暗含()方法与示例

FilePermission类implies()方法 (FilePermission Class implies() method) implies() method is available in java.io package. implies()方法在java.io包中可用。 implies() method is used to check whether this FilePermission implies the given permission (perm) or no…

填充

1. $number 68 {0:d7} -f $number2. $number1 68$number1.PadLeft(7,0)转载于:https://www.cnblogs.com/IvanChen/p/4492976.html

RabbitMQ中7种消息队列和保姆级代码演示!

blog.csdn.net/qq_32828253/article/details/110450249七种模式介绍与应用场景简单模式&#xff08;Hello World&#xff09;做最简单的事情&#xff0c;一个生产者对应一个消费者&#xff0c;RabbitMQ相当于一个消息代理&#xff0c;负责将A的消息转发给B应用场景&#xff1a;…

如何在使用ASPMVC4的分部视图中获取数据展示

如何在使用ASPMVC4的分部视图中获取数据展示在ASPMVC4中&#xff0c;创建的网站项目会用到分部视图&#xff0c;通过Html.Partial("视图名")来加载到页面上&#xff1b;但是如何把数据附加到分部视图中在加载到主页上&#xff0c;是个新的问题。暂时发现这个问题有两…

Java枚举getDeclaringClass()方法与示例

枚举类getDeclaringClass()方法 (Enum Class getDeclaringClass() method) getDeclaringClass() method is available in java.lang package. getDeclaringClass()方法在java.lang包中可用。 getDeclaringClass() method is used to return the Class object denoting the enum…

项目已被os x使用 不能打开-黑苹果之路

之前复制了几个视频文件到NTFS的盘上&#xff0c;在mac中始终无法使用&#xff08;甚至是chmod&#xff09;&#xff0c;无论是哪种播放软件&#xff0c;甚至改成dmg类型都无法打开&#xff0c;报“项目已被os x使用 不能打开”&#xff0c;用ls命令发现文件属性中多了个标志&a…

CyclicBarrier:人齐了,老司机就发车了!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;上一篇咱讲了 CountDownLatch 可以解决多个线程同步的问题&#xff0c;相比于 join 来说它的应用范围更广&#xff0c;不仅可…

Leetcode 2975. Maximum Square Area by Removing Fences From a Field

Leetcode 2975. Maximum Square Area by Removing Fences From a Field 1. 解题思路2. 代码实现 题目链接&#xff1a;2975. Maximum Square Area by Removing Fences From a Field 1. 解题思路 这一题思路上是比较直接的&#xff0c;就是直接求出横向和纵向上可能的interva…

判断ip是否合法

//用来判断ip是否合法public boolean checkIp(String tempIp) {String regex "(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)){3}";Pattern p Pattern.compile(regex);Matcher m p.matcher(tempIp);return m.matches();}

Java类类getPackage()方法及示例

类的类getPackage()方法 (Class class getPackage() method) getPackage() method is available in java.lang package. getPackage()方法在java.lang包中可用。 getPackage() method is used to return the package of this class, we find the package of the class by using…

iOS平台快速发布HT for Web拓扑图应用

iOS平台一直是封闭的生态圈&#xff0c;iOS开发者要缴纳年费加入开发者计划才可进行iOS平台的APP开发测试&#xff0c;所开发的APP需要上传到App Store经过苹果审核以后才可对外发布。如果要开发企业内部应用&#xff0c;则要缴纳更高的费用购买企业账户才可以。 对于现在火如荼…

事务注解 @Transactional 失效的3种场景及解决办法

Transactional失效场景第一种 Transactional注解标注方法修饰符为非public时&#xff0c;Transactional注解将会不起作用。例如以下代码&#xff0c;定义一个错误的Transactional标注实现&#xff0c;修饰一个默认访问符的方法&#xff1a;/*** author zhoujy**/ Component pub…

Android的多语言实现

文章转自&#xff1a;http://blog.csdn.net/barryhappy/article/details/23436527 以前就知道Android的多语言实现很简单&#xff0c;可以在不同的语言环境下使用不同的资源什么的&#xff0c;但是一直没有实际使用过。 最近公司的项目要用到多语言于&#xff0c;是就研究了一下…

java 根据类名示例化类_Java即时类| minusNanos()方法与示例

java 根据类名示例化类即时类minusNanos()方法 (Instant Class minusNanos() method) minusNanos() method is available in java.time package. minusNanos()方法在java.time包中可用。 minusNanos() method is used to subtract the given duration in nanoseconds from this…

厉害了,自己手写一个Java热加载!

热加载&#xff1a;在不停止程序运行的情况下&#xff0c;对类&#xff08;对象&#xff09;的动态替换。Java ClassLoader 简述Java中的类从被加载到内存中到卸载出内存为止&#xff0c;一共经历了七个阶段&#xff1a;加载、验证、准备、解析、初始化、使用、卸载。接下来我们…

php相应的扩展的对应链接地址

window下memcached安装&#xff1a;http://code.jellycan.com/memcached/ memcached.exe -m 15 -p 11211开启服务转载于:https://www.cnblogs.com/jakentec/p/4496828.html

duration java_Java Duration类| toMinutes()方法与示例

duration javaDuration Class toMinutes()方法 (Duration Class toMinutes() method) toMinutes() method is available in java.time package. toMinutes()方法在java.time包中可用。 toMinutes() method is used to convert this Duration into the number of minutes. toMin…