Spring Security系列之PasswordEncoder

news2024/11/18 17:38:01

概述

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

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

  • 哈希函数算法:包括消息摘要算法(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 {
	@Resource
	private UserDetailsService userDetailsService;
	
	@Autowired
	public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
		authenticationManagerBuilder
			.userDetailsService(this.userDetailsService)
			.passwordEncoder(passwordEncoder());
	}
	
	@Bean
	public 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 {
	@Override
	public String encode(CharSequence rawPassword) {
		// 直接抛出异常
		throw new UnsupportedOperationException("encode is not supported");
	}

	@Override
	public 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 {
	@Override
	public String encode(CharSequence rawPassword) {
		return hashWithSha512(rawPassword.toString());
	}
	
	@Override
	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		String hashedPassword = encode(rawPassword);
		return encodedPassword.equals(hashedPassword);
	}

	@Override
	public 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.coloradmin.cn/o/1799424.html

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

相关文章

Scanpy(4)用与数据整合和批次处理

Scanpy包,用与数据整合和批次处理,包含批次效应的BBKNN算法和用于对比的ingest基础算法比较,及其原理简介。 1. 依赖: (1)数据集(全部需要挂VPN): PBMC:pbmc3k_processed()(需要下载);pbmc68k_reduced()(scanpy自带)Pancreas(需要下载)(2)Python包:Scanp…

后端进阶-分库分表

文章目录 为什么需要分库为什么需要分表 什么时候需要分库分表只需要分库只需要分表 分库分表解决方案垂直分库水平分库垂直分表水平分表 分库分表常用算法范围算法hash分片查表分片 分库分表模式客户端模式代理模式 今天跟着训练营学习了分库分表&#xff0c;整理了学习笔记。…

Skins

本主题解释如何将DevExpress主题/皮肤应用到应用程序中&#xff0c;如何允许用户在运行时在主题之间切换&#xff0c;如何自定义现有皮肤或创建自己的皮肤&#xff0c;等等。 WinForms订阅包括许多基本控件&#xff1a;按钮、复选框、表单、消息框、对话框、对话框等。 我们实现…

CodeMirror 创建标签计算编辑器

在日常开发中对于一些数据计算场景可能会遇到标签计算的需求&#xff0c;下面关于如何使用CodeMirror实现标签计算编辑功能。 1&#xff0c;结果图 2&#xff0c;主体代码逻辑 大家只需要复制粘贴主要codeMirror使用逻辑即可 <template><el-dialogref"dialogRe…

【电路笔记】-分贝

分贝 分贝是以 10 为底的对数比,用于表示电路中功率、电压或电流的增加或减少。 1、概述 一般来说,分贝是响度的度量。 在设计或使用放大器和滤波器电路时,计算中使用的一些数字可能非常大或非常小。 例如,如果我们将两个放大器级级联在一起,功率或电压增益分别为 20 和…

qmt量化交易策略小白学习笔记第18期【qmt编程之获取对应周期的北向南向数据--方式2:原生python】

qmt编程之获取对应周期的北向南向数据 qmt更加详细的教程方法&#xff0c;会持续慢慢梳理。 也可找寻博主的历史文章&#xff0c;搜索关键词查看解决方案 &#xff01; 获取对应周期的北向南向数据 提示 该数据通过get_market_data_ex接口获取获取历史数据前需要先用downl…

力扣303. 区域和检索 - 数组不可变

Problem: 303. 区域和检索 - 数组不可变 文章目录 题目描述思路复杂度Code 题目描述 思路 创建前缀和数组preSum&#xff0c;其中preSum[i]处元素值为nums[0] - nums[i - 1]处元素值得和&#xff0c;当调用sumRange函数时直接返回preSum[right 1] - preSum[left] 复杂度 函数…

通过U盘将第三方软件安装到各大品牌电视的方法

在本教程中&#xff0c;小武给大家整理了通过U盘的方式安装第三方软件到电视盒子上&#xff0c;可直接使用通用U盘的方式来进行安装。 如果您相应电视品牌按通用方式无法完成需求&#xff0c;下面为您也贴心整理了20款主流智能电视和电视盒子的U盘安装指南。这些步骤适用于小米…

MSP430单片机控制流水灯,Proteus仿真

作品功能 本项目利用MSP430单片机控制一个简单的流水灯&#xff0c;通过按键切换流水灯的模式。用户可以通过按键控制LED灯的方向&#xff0c;从左向右或从右向左依次点亮。 作品的硬件材料 MSP430单片机 具体型号&#xff1a;MSP430G2553 LED灯 数量&#xff1a;8个类型&…

一篇文章讲透排序算法之归并排序

0.前言 本篇文章将详细解释归并排序的原理&#xff0c;以及递归和非递归的代码原理。 一.概念 归并排序是建立在归并操作上的一种有效的排序算法&#xff0c;该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并&#xff0c;得到完全有序的序列&#xff1b;即先使…

[消息队列 Kafka] Kafka 架构组件及其特性(二)Producer原理

这边整理下Kafka三大主要组件Producer原理。 目录 一、Producer发送消息源码流程 二、ACK应答机制和ISR机制 1&#xff09;ACK应答机制 2&#xff09;ISR机制 三、消息的幂等性 四、Kafka生产者事务 一、Producer发送消息源码流程 Producer发送消息流程如上图。主要是用…

【Python】使用Gradio作为机器学习web服务器

在机器学习领域&#xff0c;模型的展示和验证是一个重要的环节。传统的模型展示方式往往需要复杂的Web开发知识&#xff0c;这对于许多机器学习研究者或数据科学家来说可能是一个挑战。然而&#xff0c;Gradio的出现为我们提供了一个简单而强大的解决方案&#xff0c;让我们能够…

ffmpeg视频编码原理和实战-(2)视频帧的创建和编码packet压缩

源文件&#xff1a; #include <iostream> using namespace std; extern "C" { //指定函数是c语言函数&#xff0c;函数名不包含重载标注 //引用ffmpeg头文件 #include <libavcodec/avcodec.h> } //预处理指令导入库 #pragma comment(lib,"avcodec.…

【Week-R2】使用LSTM实现火灾预测(tf版本)

【Week-R2】使用LSTM实现火灾预测&#xff08;tf版本&#xff09; 一、 前期准备1.1 设置GPU1.2 导入数据1.3 数据可视化 二、数据预处理(构建数据集)2.1 设置x、y2.2 归一化2.3 划分数据集 三、模型创建、编译、训练、得到训练结果3.1 构建模型3.2 编译模型3.3 训练模型3.4 模…

虚拟机Ubuntu 22.04上搭建GitLab操作步骤

GitLab是仓库管理系统&#xff0c;使用Git作为代码管理工具。GitLab提供了多个版本&#xff0c;包括社区版(Community Edition)和企业版(Enterprise Edition)。实际应用场景中要求CPU最小4核、内存最小8GB&#xff0c;非虚拟环境。 以下是在虚拟机中安装社区版步骤&#xff1a;…

C++青少年简明教程:C++函数

C青少年简明教程&#xff1a;C函数 C函数是一段可重复使用的代码&#xff0c;用于执行特定的任务&#xff0c;可以提高代码的可读性和可维护性。函数可以接受参数&#xff08;输入&#xff09;并返回一个值&#xff08;输出&#xff09;&#xff0c;也可以没有参数和返回值。 …

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

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

Redis缓存(笔记二:Redis常用五大数据类型)

目录 1、Redis中String字符串 1.1 常用命令解释&#xff1a; 1.2 原子性 1.3 具有原子性的常用命令 1.4 String数据结构 1、Redis中String字符串 概念 String 是 Redis 最基本的类型&#xff0c;可以理解成与 Memcached 一模一样的类型&#xff0c;一个 key对应一个 value…

Go微服务: 基于使用场景理解分布式之二阶段提交

概述 二阶段提交&#xff08;Two-Phase Commit&#xff0c;2PC&#xff09;是一种分布式事务协议&#xff0c;用于在分布式系统中确保多个参与者的操作具有原子性即所有参与者要么全部提交事务&#xff0c;要么全部回滚事务&#xff0c;以维持数据的一致性它分为两个阶段进行&…

php反序列化中的pop链

目录 一、什么是POP 二、成员属性赋值对象 例题&#xff1a; 方法一 方法二 三、魔术方法的触发规则 例题&#xff1a; 四、POC的编写 例题1&#xff1a; 例题2 [NISACTF 2022]babyserialize 今日总结&#xff1a; 一、什么是POP 在反序列化中&#xff0c;我们…