附彩蛋|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;只有单列的称之为列向量。 相等 维数和元素都相等 数乘(与标量相乘) 每一个元素与…

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

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

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

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

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

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

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热加载!

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

公司新来的小可爱,竟然把内存搞崩了!

ThreadLocal使用不规范&#xff0c;师傅两行泪组内来了一个实习生&#xff0c;看这小伙子春光满面、精神抖擞、头发微少&#xff0c;我心头一喜&#xff1a;绝对是个潜力股。于是我找经理申请亲自来带他&#xff0c;为了帮助小伙子快速成长&#xff0c;我给他分了一个需求&…

理解Node.js的event loop

为什么80%的码农都做不了架构师&#xff1f;>>> 关于Node.js的第一个基本概念是I/O操作开销是巨大的&#xff1a; 所以&#xff0c;当前变成技术中最大的浪费来自于等待I/O操作的完成。有几种方法可以解决性能的影响&#xff1a; 同步方式&#xff1a;按次序一个…

硬核|定时任务的10种实现方案,满足你的不同需求!

最近有几个读者私信给我&#xff0c;问我他们的业务场景&#xff0c;要用什么样的定时任务。确实&#xff0c;在不用的业务场景下要用不同的定时任务&#xff0c;其实我们的选择还是挺多的。我今天给大家总结10种非常实用的定时任务&#xff0c;总有一种是适合你的。一. linux自…

Semaphore自白:限流器用我就对了!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;大家好&#xff0c;我是 Semaphore&#xff0c;我的中文名字叫“信号量”&#xff0c;我来自 JUC 家族&#xff08;java.uti…

Android Activity和Intent机制学习笔记

文章转自&#xff1a;http://www.cnblogs.com/feisky/archive/2010/01/16/1649081.html Activity Android中&#xff0c;Activity是所有程序的根本&#xff0c;所有程序的流程都运行在Activity之中&#xff0c;Activity具有自己的生命周期&#xff08;见http://www.cnblogs.com…

线程的故事:我的3位母亲成就了优秀的我!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;声明&#xff1a;本故事纯属虚构&#xff0c;如果雷同那就是真事了&#xff01;大家好&#xff0c;我是线程&#xff0c;我的…

7种内存泄露场景和13种解决方案!

前言Java通过垃圾回收机制&#xff0c;可以自动的管理内存&#xff0c;这对开发人员来说是多么美好的事啊。但垃圾回收器并不是万能的&#xff0c;它能够处理大部分场景下的内存清理、内存泄露以及内存优化。但它也并不是万能的。不然&#xff0c;我们在实践的过程中也不会出现…

Android Sqlite

2019独角兽企业重金招聘Python工程师标准>>> 一、http://lansuiyun.iteye.com/blog/1246430 good! 二、增加新表时&#xff0c;需要更新版本号。 三、Android 使用自定义cursorAdapter&#xff1a; http://blog.csdn.net/buaalei/article/details/6064792 good! 四…

小黑小波比.git clone报错解决方案

2019独角兽企业重金招聘Python工程师标准>>> zmzpzmzp1:~/data$ git clone git192.168.199.199:zmw/s910.git 正克隆到 s910... ssh: connect to host 192.168.199.199 port 22: Connection refused fatal: Could not read from remote repository.Please make sure…

一个sql注入直接把我们服务搞挂了

前言最近我在整理安全漏洞相关问题&#xff0c;准备在公司做一次分享。恰好&#xff0c;这段时间团队发现了一个sql注入漏洞&#xff1a;在一个公共的分页功能中&#xff0c;排序字段作为入参&#xff0c;前端页面可以自定义。在分页sql的mybatis mapper.xml中&#xff0c;orde…

reinterpret_cast和static_cast的总结

主要参考&#xff1a;http://blog.csdn.net/querw/article/details/7387594 http://www.cnblogs.com/jerry19880126/archive/2012/08/14/2638192.html http://www.cnblogs.com/ider/archive/2011/07/30/cpp_cast_operator_part3.htmlhttp://bbs.csdn.net/topics/390249118 关键…

Java双刃剑之Unsafe类详解

前一段时间在研究juc源码的时候&#xff0c;发现在很多工具类中都调用了一个Unsafe类中的方法&#xff0c;出于好奇就想要研究一下这个类到底有什么作用&#xff0c;于是先查阅了一些资料&#xff0c;一查不要紧&#xff0c;很多资料中对Unsafe的态度都是这样的画风&#xff1a…