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

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

这篇关于附彩蛋|Spring Security 竟然故意延长登录时间?知道真相的我惊呆了!的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/698075

相关文章

Java反转字符串的五种方法总结

《Java反转字符串的五种方法总结》:本文主要介绍五种在Java中反转字符串的方法,包括使用StringBuilder的reverse()方法、字符数组、自定义StringBuilder方法、直接... 目录前言方法一:使用StringBuilder的reverse()方法方法二:使用字符数组方法三:使用自

JAVA封装多线程实现的方式及原理

《JAVA封装多线程实现的方式及原理》:本文主要介绍Java中封装多线程的原理和常见方式,通过封装可以简化多线程的使用,提高安全性,并增强代码的可维护性和可扩展性,需要的朋友可以参考下... 目录前言一、封装的目标二、常见的封装方式及原理总结前言在 Java 中,封装多线程的原理主要围绕着将多线程相关的操

Java进阶学习之如何开启远程调式

《Java进阶学习之如何开启远程调式》Java开发中的远程调试是一项至关重要的技能,特别是在处理生产环境的问题或者协作开发时,:本文主要介绍Java进阶学习之如何开启远程调式的相关资料,需要的朋友... 目录概述Java远程调试的开启与底层原理开启Java远程调试底层原理JVM参数总结&nbsMbKKXJx

Spring Cloud之注册中心Nacos的使用详解

《SpringCloud之注册中心Nacos的使用详解》本文介绍SpringCloudAlibaba中的Nacos组件,对比了Nacos与Eureka的区别,展示了如何在项目中引入SpringClo... 目录Naacos服务注册/服务发现引⼊Spring Cloud Alibaba依赖引入Naco编程s依

java导出pdf文件的详细实现方法

《java导出pdf文件的详细实现方法》:本文主要介绍java导出pdf文件的详细实现方法,包括制作模板、获取中文字体文件、实现后端服务以及前端发起请求并生成下载链接,需要的朋友可以参考下... 目录使用注意点包含内容1、制作pdf模板2、获取pdf导出中文需要的文件3、实现4、前端发起请求并生成下载链接使

Java springBoot初步使用websocket的代码示例

《JavaspringBoot初步使用websocket的代码示例》:本文主要介绍JavaspringBoot初步使用websocket的相关资料,WebSocket是一种实现实时双向通信的协... 目录一、什么是websocket二、依赖坐标地址1.springBoot父级依赖2.springBoot依赖

如何用java对接微信小程序下单后的发货接口

《如何用java对接微信小程序下单后的发货接口》:本文主要介绍在微信小程序后台实现发货通知的步骤,包括获取Access_token、使用RestTemplate调用发货接口、处理AccessTok... 目录配置参数 调用代码获取Access_token调用发货的接口类注意点总结配置参数 首先需要获取Ac

Java逻辑运算符之&&、|| 与&、 |的区别及应用

《Java逻辑运算符之&&、||与&、|的区别及应用》:本文主要介绍Java逻辑运算符之&&、||与&、|的区别及应用的相关资料,分别是&&、||与&、|,并探讨了它们在不同应用场景中... 目录前言一、基本概念与运算符介绍二、短路与与非短路与:&& 与 & 的区别1. &&:短路与(AND)2. &:非短

Java的volatile和sychronized底层实现原理解析

《Java的volatile和sychronized底层实现原理解析》文章详细介绍了Java中的synchronized和volatile关键字的底层实现原理,包括字节码层面、JVM层面的实现细节,以... 目录1. 概览2. Synchronized2.1 字节码层面2.2 JVM层面2.2.1 ente

什么是 Java 的 CyclicBarrier(代码示例)

《什么是Java的CyclicBarrier(代码示例)》CyclicBarrier是多线程协同的利器,适合需要多次同步的场景,本文通过代码示例讲解什么是Java的CyclicBarrier,感... 你的回答(口语化,面试场景)面试官:什么是 Java 的 CyclicBarrier?你:好的,我来举个例