Spring Security系列之PasswordEncoder

概述

任何一个登录系统的密码不能明文存储,万一发生数据库泄漏事故(不管是内部人员导出数据库数据还是被黑客攻击破解数据库实例节点拿到数据库数据等,又或者是其他情况造成的),将产生巨大的损失。因此明文密码在存储到数据库之前需要加密处理。

加密算法有很多,大致有如下分类:

  • 哈希函数算法:包括消息摘要算法(MD4,MD5等),消息摘要算法是一种特殊类型的哈希函数算法,用于将任意长度的数据映射为固定长度的哈希值或摘要。摘要值通常用于验证数据的完整性、数字签名、身份验证等用途。算法包括:
    • MD5(Message Digest Algorithm 5):已经不推荐使用,存在碰撞攻击漏洞
    • SHA-1(Secure Hash Algorithm 1):也存在碰撞攻击漏洞,逐渐被淘汰
    • SHA-256、SHA-384、SHA-512:SHA-2系列,目前被广泛应用,提供更高的安全性
  • 对称加密算法使用相同的密钥进行加密和解密。常见的对称加密算法包括:
    • DES(Data Encryption Standard):已经不推荐使用,因为密钥长度较短易受到攻击
    • 3DES(Triple DES):DES增强版,使用三个密钥提高安全性
    • AES(Advanced Encryption Standard):目前广泛应用的对称加密算法,具有较高的安全性和性能
  • 非对称加密算法(公钥加密算法):
    非对称加密算法使用一对密钥,分别是公钥和私钥,公钥用于加密,私钥用于解密。常见的非对称加密算法包括:
  • RSA(Rivest-Shamir-Adleman):基于大数分解难题,被广泛用于数字签名和密钥交换
  • ECC(Elliptic Curve Cryptography):利用椭圆曲线上的离散对数问题,相比RSA,提供相同安全级别下更短的密钥长度和更高的性能

反查表、彩虹表

上文提到一些已经不推荐使用、逐渐被淘汰的算法,如MD5、SHA-1。因为不管是MD5还是SHA-1算法,对于给定的某个字符串(密码),经过哈希函数计算之后得到的结果都是固定的。比如admin经过MD5计算(有16位和32位之分,这里用的是16位)结果始终是7a57a5a743894a0eroot经过SHA-1计算后结果始终是dc76e9f0c0006e8f919e0c515c66dbba3982f785

那黑客们就可以维护一个数据库,其字段包括加密后的密文、加密算法、明文密码,意味着可以根据密文反查明文密码。这就是反查表。

基于反查表,黑客们后来发明更高级的彩虹表。

在Java Web开发中,我们会遇到各种各样的安全问题。作为最基本的,数据库密码的安全性如何得到保证呢?此时Spring Security隆重登场,可以帮助我们解决这个问题。

Spring Security

实例

加密密码的配置类(代码片段):

import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;@Configuration
@EnableWebSecurity
@EnableMethodSecurity()
public class WebSecurityConfig implements SecurityFilterChain {@Resourceprivate UserDetailsService userDetailsService;@Autowiredpublic void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {authenticationManagerBuilder.userDetailsService(this.userDetailsService).passwordEncoder(passwordEncoder());}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}

PasswordEncoder

PasswordEncoder接口定义如下:

public interface PasswordEncoder {// 用来对明文密码进行加密String encode(CharSequence rawPassword);// 用来进行密码比对boolean matches(CharSequence rawPassword, String encodedPassword);// 用来判断当前密码是否需要升级,默认返回false表示不需要升级default boolean upgradeEncoding(String encodedPassword) {return false;}
}

尚未废弃的实现类,都是自适应单向函数(Adaptive One-way Functions)来处理密码问题,这种函数在进行密码匹配时,会有意占用大量系统资源(例如CPU、内存等),可以增加恶意用户攻击系统的难度。包括:bcrypt、PBKDF2、scrypt以及argon2。

因此实现类包括:

  • BCryptPasswordEncoder:使用bcrypt强散列算法对密码进行加密,为提高密码的安全性,bcrypt算法故意降低运行速度,以增强密码破解的难度。BCryptPasswordEncoder自带salt加盐机制,即使相同的明文每次生成的加密字符串都不相同。默认强度为10(参考源码里的strength字段),开发者可以根据自己的服务器性能进行调整,以确保密码验证时间约为1秒钟(官方建议密码验证时间为1秒钟,既可以提高系统安全性,又不会过多影响系统运行性能)
  • Argon2PasswordEncoder:使用Argon2算法对密码进行加密,Argon2曾在Password Hashing Competition竞赛中获胜。为了解决在定制硬件上密码容易被破解的问题,Argon2也是故意降低运算速度,同时需要大量内存
  • Pbkdf2PasswordEncoder:使用PBKDF2算法对密码进行加密,可用于FIPS(Federal Information Processing Standard,美国联邦信息处理标准)认证
  • SCryptPasswordEncoder:使用scrypt算法对密码进行加密

几个已经被废弃的基于消息摘要算法的实现类:

  • NoOpPasswordEncoder:密码明文存储,不可用于生产环境
  • Md4PasswordEncoder:使用Md4算法加密密码
  • LdapShaPasswordEncoder:使用SHA算法
  • StandardPasswordEncoder:使用SHA-256算法
  • MessageDigestPasswordEncoder:使用MD5算法

BCryptPasswordEncoder

public String encode(CharSequence rawPassword) {if (rawPassword == null) {throw new IllegalArgumentException("rawPassword cannot be null");}String salt = getSalt();return BCrypt.hashpw(rawPassword.toString(), salt);
}private String getSalt() {if (this.random != null) {return BCrypt.gensalt(this.version.getVersion(), this.strength, this.random);}return BCrypt.gensalt(this.version.getVersion(), this.strength);
}

使用Spring Security提供的BCrypt工具类生成盐(salt);然后,根据盐和明文密码生成最终的密文。所谓加盐,就是在初始化明文数据时,由系统自动向该明文里添加一些附加数据,然后散列。引入加盐机制的目的是进一步提高加密数据的安全性,单向散列加密及加盐思想广泛应用于系统登录过程中的密码生成和校验。

构造方法:

public BCryptPasswordEncoder(BCryptVersion version, int strength, SecureRandom random) {if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) {throw new IllegalArgumentException("Bad strength");}this.version = version;this.strength = (strength == -1) ? 10 : strength;this.random = random;
}

从构造函数可知,strength长度默认为10,最小值为BCrypt.MIN_LOG_ROUNDS=4,最大值为BCrypt.MAX_LOG_ROUNDS=31。显而易见,长度越长,加密算法越复杂,被恶意破解攻击的难度越大,但是也会增加系统负载,增加加密计算时长和存储空间。因此需要取得权衡,默认情况下使用Spring Security建议的长度10即可。

PasswordEncoderFactories

浏览一下spring-security-crypto-6.2.3源码结构:
在这里插入图片描述
不难发现PasswordEncoderFactories这个类,采用工厂方法模式,源码:

public static PasswordEncoder createDelegatingPasswordEncoder() {String encodingId = "bcrypt";Map<String, PasswordEncoder> 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", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));encoders.put("sha256", new StandardPasswordEncoder());encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());return new DelegatingPasswordEncoder(encodingId, encoders);
}

静态方法createDelegatingPasswordEncoder,encoders中存储每一种密码加密方案的id和所对应的加密类,如bcrypt对应BcryptPassword。最后,返回代理类DelegatingPasswordEncoder实例,并且默认使用的加密方案是BCryptPasswordEncoder。

DelegatingPasswordEncoder

DelegatingPasswordEncoder,采用代理模式,Spring Security 5.0版本后默认的密码加密方案,主要考虑如下三方面的因素:

  • 兼容性:使用DelegatingPasswordEncoder可以帮助许多使用旧密码加密方式的系统顺利迁移到Spring Security中,它允许在同一个系统中同时存在多种不同的密码加密方案
  • 便捷性:密码存储的最佳方案不可能一直不变,使用DelegatingPasswordEncoder作为默认的密码加密方案,当需要修改加密方案时,只需要修改很小一部分代码即可实现
  • 稳定性:作为一个框架,Spring Security不能经常进行重大更改,使用Delegating PasswordEncoder可以方便地对密码进行升级(自动从一个加密方案升级到另外一个加密方案)

属性如下:

// 默认的前缀和后缀,用于包裹将来生成的加密方案的id
private static final String DEFAULT_ID_PREFIX = "{";
private static final String DEFAULT_ID_SUFFIX = "}";
// 构造方法里支持传入用户自定义的前缀和后缀
private final String idPrefix;
private final String idSuffix;
// 默认的加密方案id
private final String idForEncode;
// 根据idForEncode从idToPasswordEncoder map中提取出来的
private final PasswordEncoder passwordEncoderForEncode;
// 保存id和加密方案之间的映射
private final Map<String, PasswordEncoder> idToPasswordEncoder;
// 默认的密码比对器,当根据密码加密方案的id无法找到对应的加密方案时,就会使用默认的密码比对器。默认类型是UnmappedIdPasswordEncoder
private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();

UnmappedIdPasswordEncoder是一个内部私有类:

private class UnmappedIdPasswordEncoder implements PasswordEncoder {@Overridepublic String encode(CharSequence rawPassword) {// 直接抛出异常throw new UnsupportedOperationException("encode is not supported");}@Overridepublic boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {// 并不会做任何密码比对操作,直接抛出异常String id = extractId(prefixEncodedPassword);throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");}
}

核心方法encode

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

作为一个代理类,不负责具体的加密工作,由加密类来完成,最后加上类似于{bcrypt}这样的前缀,不同的前缀表示使用不同的加密算法,即不同的PasswordEncoder实现类,当然也包括自定义的加密类。

核心方法matches

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);
}

extractId方法用于从加密字符串中提取出具体的加密方案id,也就是前缀和后缀包裹的字符串,如bcrypt,此方法就不贴出来了。根据加密方案id从map集合查找对应的加密算法实现类,查找失败则使用默认的加密类,即UnmappedIdPasswordEncoder,然后就会抛出异常。

核心方法upgradeEncoding

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);}
}

如果当前加密字符串所采用的加密方案不是默认的BcryptPasswordEncoder ,就会自动进行密码升级,否则就调用默认加密方案的upgradeEncoding方法判断密码是否需要升级。

自定义加密方案

业务开发中,如果Spring Security自带的几个加密类都不能满足需求,或者业务场景比较复杂,需要兼容数据库历史未加密字段或加密算法不够好的字段,则可能需要自定义加密类。

具体来说,实现PasswordEncoder接口类,并重写3个方法。比如自定义一个使用SHA-512加密算法的加密类:

public class Sha512PasswordEncoder implements PasswordEncoder {@Overridepublic String encode(CharSequence rawPassword) {return hashWithSha512(rawPassword.toString());}@Overridepublic boolean matches(CharSequence rawPassword, String encodedPassword) {String hashedPassword = encode(rawPassword);return encodedPassword.equals(hashedPassword);}@Overridepublic boolean upgradeEncoding(String prefixEncodedPassword) {// 不需要升级return false;}private String hashWithSha512(String input) {StringBuilder result = new StringBuilder();try {MessageDigest md = MessageDigest.getInstance("SHA-512");byte [] digested = md.digest(input.getBytes());for (int i = 0; i < digested.length; i++) {result.append(Integer.toHexString(0xFF & digested[i]));}} catch (NoSuchAlgorithmException e) {throw new RuntimeException("Bad algorithm");}return result.toString();}
}

最后需要配置一下使用此自定义类,使其生效。

参考

  • 深入浅出Spring Security

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

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

相关文章

DOS 操作系统

DOS 介绍 DOS&#xff1a;disk operating system&#xff0c;磁盘操作系统。 中国DOS联盟下载 MS-DOS 7.10完整安装版&#xff08;含图形安装程序&#xff09; DOS 环境下的操作 输入部分内容后按下 Tab 可以快速自动补全。 按住 Ctrl 键可以用鼠标滚轮改变字号大小。 DO…

【数据结构初阶】--- 顺序表

顺序表&#xff0c;好像学C语言时从来没听过&#xff0c;实际上就是给数组穿了层衣服&#xff0c;本质是一模一样的。 这里的顺序表实际是定义了一个结构体&#xff0c;设计各种函数来实现它的功能&#xff0c;比如说数组中的增删改查插入&#xff0c;这些基本操作其实平时就会…

Log4j日志级别介绍

Log4j 是一个广泛使用的 Java 日志记录框架&#xff0c;提供了多种日志级别&#xff0c;用于控制日志输出的详细程度。每个日志级别代表一种特定的重要性和紧急程度。 以下是 Log4j 的常见日志级别及其解读&#xff1a; FATAL&#xff08;致命&#xff09; 解释&#xff1a;表…

【报错解决】深度学习模型训练时cuda内存足够但测试时反而报错cuda out of memory

报错描述 报错的代码如下&#xff1a; model reader(configargs, encoderencoder)#初始化模型 model.to(cuda)#把模型放到gpu上 model.load_state_dict(torch.load(join(args.checkpoint_path, best_ckpt_model1.pkl)))#加载模型参数 model torch.nn.DataParallel(model)#并…

c++调用动态库LNK2019无法解析的外部符号LNK1120无法解析的外部命令

严重性 代码 说明 项目 文件 行 禁止显示状态 错误 LNK1120 6 个无法解析的外部命令 ConsoleApplication1 D:\vs_qt_project\ConsoleApplication1\x64\Debug\ConsoleApplication1.exe 1 严重性 代码 说明 项目 文件 行 …

聊聊App在安卓设备中所使用的内存

文章用来记录及分享本人对安卓程序中内存使用的个人见解&#xff0c;如有不同见解 不吝赐教。 先简单浏览下面代码&#xff0c;看看对它们是否熟悉&#x1f447; val activityManager getSystemService(ACTIVITY_SERVICE) as ActivityManager// Java 堆内存信息val runtime R…

adb 脚本化Android系统截图和录屏

1、首先我们了解截图和录屏最基础的命令 手机录屏 adb shell screenrecord sdcard/Pictures/Screenshots/Record_xxx.mp4导出手机录屏 adb pull sdcard/Pictures/Screenshots/Record_xxx.mp4手机截屏 adb shell screencap /sdcard/Screenshots_xxx.png导出手机截屏 adb pu…

应用层——HTTP协议(自己实现一个http协议)——客户端(浏览器)的请求做反序列化和请求分析,然后创建http向响应结构

应用层&#xff1a;之前我们写的创建套接字&#xff0c;发送数据&#xff0c;序列化反序列化这些都是在写应用层 我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层 之前的网络计算机是我们自定义的协议&#xff1a;传输的数据最终是什么样的结…

Gnu/Linux 系统编程 - 如何获取帮助及一个演示

Gnu/Linux 系统编程 - 如何获取帮助及一个演示 今天开始写 Gnu/Linux 环境下的系统编程&#xff0c;主要的用的语言是 C&#xff0c;主要是为了学习 C 语言&#xff0c;边学边写&#xff0c;这样的学习速度是比较快的。 今天就先介绍下如何在手头上没有任何资料的情况下&…

windows11搭建 stable-diffusion-webui

2024年5月22日23:46:57 建议电脑配置 电脑配置&#xff1a; Intel Core™ Ultra 5 125H 1.20 GHz 32.0 GB (31.6 GB 可用) 系统&#xff1a;windows11 注意&#xff1a;最好挂上外网&#xff0c;或者设置好访问github的dns&#xff0c;不然很可能失败 1&#xff0c;安装 An…

高级文件操作

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 Python内置的os模块除了可以对目录进行操作&#xff0c;还可以对文件进行一些高级操作&#xff0c;具体函数如表4所示。 表4 os模块提供的与文件相…

vue3 项目目录使用文件夹和index.vue,与直接用直接用名字.vue的区别

在 Vue 3 项目中&#xff0c;使用文件夹加 index.vue 文件与直接使用组件名字命名的 .vue 文件都是常见的组织方式。两者各有优缺点&#xff0c;适用于不同的场景和需求。以下是对这两种方式的详细比较&#xff1a; 使用文件夹和 index.vue 文件 结构示例 src/ ├── compo…

【AI基础】第三步:纯天然保姆喂饭级-安装并运行chatglm2-6b

chatglm2构建时使用了RUST&#xff0c;所以在安装chatglm2之前&#xff0c;先安装RUST。 此系列文章列表&#xff1a; 【AI基础】第一步&#xff1a;安装python开发环境-windows篇_下载安装ai环境python-CSDN博客 【AI基础】第一步&#xff1a;安装python开发环境-conda篇_mini…

知识图谱的应用---智慧司法

文章目录 智慧司法典型应用 智慧司法 智慧司法是综合运用人工智能、大数据、互联网、物联网、云计算等信息技术手段&#xff0c;遵循司法公开、公平、公正的原则&#xff0c;与司法领域业务知识经验深度融合&#xff0c;使司法机关在审判、检查、侦查、监管职能各方面得到全面的…

Android14 WMS-Power键短按流程

目录 1. PhoneWindowManager#interceptKeyBeforeQueueing 2. PhoneWindowManager#handleKeyGesture 3. PhoneWindowManager#handleCameraGesture 4. SingleKeyGestureDetector#interceptKey 5. SingleKeyGestureDetector#interceptKeyUp 6. SingleKeyGestureDetector#Ke…

【数据结构】图论入门

引入 数据的逻辑结构&#xff1a; 集合&#xff1a;数据元素间除“同属于一个集合”外&#xff0c;无其他关系线性结构&#xff1a;一个对多个&#xff0c;例如&#xff1a;线性表、栈、队列树形结构&#xff1a;一个对多个&#xff0c;例如&#xff1a;树图形结构&#xff1…

「前端+鸿蒙」鸿蒙应用开发-搭建开发环境创建项目

快速体验鸿蒙应用开发&#xff0c;首先需要搭建开发环境和创建项目。 搭建开发环境 下载DevEco Studio&#xff1a; 访问华为开发者联盟官方网站下载DevEco Studio。DevEco Studio下载链接 安装DevEco Studio&#xff1a; 运行下载的安装程序&#xff0c;按照安装向导指引完成…

SpringBoot中的WebMvcConfigurationSupport和WebMvcConfigurer

在SpringBoot中可以通过以下两种方式来完成自定义WebMvc的配置&#xff1a; &#xff08;1&#xff09;继承WebMvcConfigurationSupport类 &#xff08;2&#xff09;实现WebMvcConfigurer接口 通过这两种方式完成的WebMvc配置存在差异&#xff0c;本文将对此作简单说明与区…

【Vue】单页应用程序介绍

通常基于Vue去开发一整个网站&#xff0c;开发出来的这整个网站应用&#xff0c;我们都会叫做单页应用程序 概念 单页应用程序&#xff1a;SPA【Single Page Application】是指所有的功能都在一个html页面上实现 我们可以将页面共用的部分封装成组件&#xff0c;底下要切换的也…

【copy_dwg_files.bat】

参考这段代码,实现copy_dwg_files的功能 好的,以下是参考你提供的代码来实现 copy_dwg_files.bat 的功能。这段代码将在当前目录及其所有子目录中查找 施工图 文件夹,并将该文件夹下的所有 .dwg 文件复制到同一目录下的 Calculate 文件夹中。如果 Calculate 文件夹不存在,…