加密与安全_优雅存储用户密码的最佳实践
文章目录
- Pre
- 概述
- 最佳实践
- 避免使用MD5、SHA1等快速哈希算法
- 加盐哈希 (不推荐)
- 使用BCrypt、Argon2等慢哈希算法 (推荐)
- BCrypt Code
- 1. 自动生成和嵌入盐
- 2. 哈希结果的格式
- 3. 代价因子
- BCrypt特点
- 防止暴力破解
- 1. 登录失败锁定
- 2. 双因素认证(2FA)
- 3. 图形验证码或短信验证
- 4. 异常检测与应对
- 5. 登录通知
- 总结

Pre
MD5破解网站:https://www.cmd5.com/

计划
- 讨论密码保存的关键原则和最佳实践。
- 解析不应使用MD5单独保存密码的原因。
- 探讨加盐的必要性及注意事项。
- 推荐使用更安全的哈希算法,例如BCrypt,并解释其优点。
- 总结防止暴力破解和进一步保护用户账号的措施。
概述
在保存用户密码时,最重要的原则是不要以任何形式存储原始密码,而是存储经过哈希处理后的密码哈希值。传统的哈希算法如MD5虽然不可逆,但由于它们的速度快且容易构建彩虹表,这使得仅用它们来保存密码是不安全的。
最佳实践
避免使用MD5、SHA1等快速哈希算法
快速哈希算法,如MD5、SHA1等,因计算速度过快,容易被利用来构建彩虹表,导致密码被破解。因此,不建议使用这些算法来存储用户密码。
import org.apache.commons.codec.digest.DigestUtils;@GetMapping("wrong1")public UserData wrong1(@RequestParam(value = "name", defaultValue = "artisan") String name, @RequestParam(value = "password", defaultValue = "Abcd1234") String password) {UserData userData = new UserData();userData.setId(1L);userData.setName(name);userData.setPassword(DigestUtils.md5Hex(password));return userRepository.save(userData);}
String password = "Abcd1234";
String s = DigestUtils.md5Hex(password);
System.out.println(s);

多次 MD5 依然不安全
// 多次 MD5String s1 = DigestUtils.md5Hex(DigestUtils.md5Hex(password));System.out.println(s1);

加盐哈希 (不推荐)
加盐(Salt)是一种防御措施,通过在密码前后加入一个随机的字符串(盐值)来增加哈希的复杂度。正确的加盐方法如下:
-
每个密码都应有一个唯一的盐值。
-
盐值应足够长和复杂,通常建议长度超过20位。
// 不能在代码中写死盐,且盐需要有一定的长度 ,如下是个错误的示例String s2 = DigestUtils.md5Hex("salt" + password);System.out.println(s2);
对于这样一串 MD5,虽然破解网站上找不到原始密码,但是黑客可以自己注册一个账号,
使用一个简单的密码,比如 1 ,得到55f312f84e7785aa1efa552acbf251db

然后,再去破解网站试一下这个 MD5,就可以得到原始密码是 salt,也就知道了盐值是salt -
盐值不应直接与用户的其他信息(如用户名)相关。
@GetMapping("wrong3")public UserData wrong3(@RequestParam(value = "name", defaultValue = "朱晔") String name, @RequestParam(value = "password", defaultValue = "Abcd1234") String password) {UserData userData = new UserData();userData.setId(1L);userData.setName(name);userData.setPassword(DigestUtils.md5Hex(name + password));return userRepository.save(userData);}如果世界上所有的系统都是按照这个方案来保存密码,那么 root、admin 这样的用户使再复杂的密码也总有一天会被破解,因为黑客们完全可以针对这些常用用户名来做彩虹表。
所以,盐最好是随机的值,并且是全球唯一的,意味着全球不可能有现成的彩虹表给你用
-
盐值和哈希后的密码应一同存储,盐值无需加密。
正确的做法是,使用全球唯一的、和用户无关的、足够长的随机值作为盐。比如,可以使用UUID 作为盐,把盐一起保存到数据库中
@GetMapping("right")public UserData right(@RequestParam(value = "name", defaultValue = "朱晔") String name, @RequestParam(value = "password", defaultValue = "Abcd1234") String password) {UserData userData = new UserData();userData.setId(1L);userData.setName(name);userData.setSalt(UUID.randomUUID().toString());userData.setPassword(DigestUtils.md5Hex(userData.getSalt() + password));return userRepository.save(userData);}并且每次用户修改密码的时候都重新计算盐,重新保存新的密码。在我看来, 盐没有必要加密保存。盐的作用是,防止通过彩虹表快速实现密码“解密”,如果用户的盐都是唯一的,那么生成一次彩虹表只可能拿到一个用户的密码,这样黑客的动力会小很多。
使用BCrypt、Argon2等慢哈希算法 (推荐)
Spring Security 已经废弃了
MessageDigestPasswordEncoder,推荐使用BCryptPasswordEncoder
BCrypt和Argon2是为密码保存设计的慢哈希算法。这些算法通过引入计算成本(例如BCrypt的代价因子)使得暴力破解变得非常困难。
BCrypt 是为保存密码设计的算法,相比 MD5 要慢很多.
测试一下 MD5,以及使用不同代价因子的 BCrypt,看看哈希一次密码的耗时
import org.springframework.security.crypto.bcrypt.BCrypt;@GetMapping("performance")public void performance() {StopWatch stopWatch = new StopWatch();String password = "Abcd1234";stopWatch.start("MD5");DigestUtils.md5Hex(password);stopWatch.stop();stopWatch.start("BCrypt(10)");String hash1 = BCrypt.gensalt(10);BCrypt.hashpw(password, hash1);System.out.println(hash1);stopWatch.stop();stopWatch.start("BCrypt(12)");String hash2 = BCrypt.gensalt(12);BCrypt.hashpw(password, hash2);System.out.println(hash2);stopWatch.stop();stopWatch.start("BCrypt(14)");String hash3 = BCrypt.gensalt(14);BCrypt.hashpw(password, hash3);System.out.println(hash3);stopWatch.stop();log.info("{}", stopWatch.prettyPrint());}

MD5 的计算速度非常快(约 0.8 毫秒),这使得它容易被用于构建彩虹表或暴力破解密码。正因为它的速度快,攻击者可以在短时间内生成并查找大量的 MD5 哈希值,从而快速破解密码。
BCrypt 设计用于密码存储,考虑了安全性。它的计算速度相对较慢,特别是在设置较高代价因子(cost factor)的情况下。代价因子越高,BCrypt 的计算时间越长,这意味着攻击者需要花费更长时间才能生成彩虹表或进行暴力破解。例如:
- 代价因子为 10 时,BCrypt 哈希耗时 82 毫秒。
- 代价因子为 12 时,耗时增加到 312 毫秒。
- 代价因子为 14 时,耗时更是达到了 1.2 秒。
这意味着,即使攻击者想要创建一个针对 BCrypt 的彩虹表,所需的时间和计算资源也大大增加。例如,若生成一个针对 8 位密码的 MD5 彩虹表需要 5 个月的时间,那么对 BCrypt 而言,这个时间可能会延长到几十年甚至更久。由于需要大量的计算资源和时间,大部分攻击者可能会选择放弃,转而寻找更容易的目标。
要估算生成一个针对 8 位密码的 MD5 彩虹表所需的时间,我们需要考虑以下因素:
- 密码空间大小:8 位密码的可能组合数。
- MD5 哈希的计算速度:每秒可以计算多少次 MD5 哈希。
- 总计算时间:生成彩虹表所需的总时间。
- 计算密码空间大小
假设密码由 62 个字符组成(大写字母、小写字母和数字,共 26 + 26 + 10 = 62 个字符),那么8位密码的可能组合数为:[ 62^8 ]
计算这个值:
[ 62^8 = 218,340,105,584,896 \text{ (约为 218 万亿次计算)} ]- 计算MD5哈希的速度
根据数据,MD5 哈希的计算速度是 0.8 毫秒每次。那么,每秒的哈希计算次数为:
[ \frac{1}{0.8 \text{ 毫秒}} = \frac{1}{0.0008 \text{ 秒}} = 1,250 \text{ 次/秒} ]- 计算生成彩虹表的总时间
生成彩虹表的总时间 = 密码空间大小 ÷ 每秒哈希计算次数:
BCrypt Code
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;private static BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();@GetMapping("better")public UserData better(@RequestParam(value = "name", defaultValue = "artisan") String name, @RequestParam(value = "password", defaultValue = "Abcd1234") String password) {UserData userData = new UserData();userData.setId(1L);userData.setName(name);userData.setPassword(passwordEncoder.encode(password));userRepository.save(userData);log.info("match ? {}", passwordEncoder.matches(password, userData.getPassword()));return userData;}
1. 自动生成和嵌入盐
BCrypt 的 encode 方法会自动生成一个随机盐,然后将这个盐与密码一起进行哈希计算。这个盐会作为哈希结果的一部分存储,因此在对密码进行校验时,BCrypt 可以直接从哈希结果中提取盐,重新计算哈希值并进行比对。这样做的好处是,你不需要手动生成和存储盐,也不需要在密码校验时额外传入盐。
2. 哈希结果的格式
BCrypt 的哈希结果格式是非常标准化的,结构如下:
$<ver>$<cost>$<salt><digest>
$:字段分隔符。<ver>:算法版本,例如2a代表算法的版本。<cost>:代价因子,控制哈希计算的复杂度。值越大,计算时间越长。<salt>:生成的盐,通常为 22 个字符。<digest>:最终的哈希值,也就是加盐后的密码哈希结果。
例如,$2a$10$wPWdQwfQO2lMxqSIb6iCROXv7lKnQq5XdMO96iCYCj7boK9pk6QPC 可以被解析为:
- 版本:
2a - 代价因子:
10 - 盐:
wPWdQwfQO2lMxqSIb6iCRO - 哈希:
Xv7lKnQq5XdMO96iCYCj7boK9pk6QPC
由于盐是嵌入在哈希结果中的,因此无需单独存储盐,也不需要在校验时提供它。
3. 代价因子
代价因子(cost factor)控制了 BCrypt 哈希函数的计算复杂度。具体而言,代价因子的值越高,哈希计算所需的时间越长。这是 BCrypt 的一个重要特性,能够有效地防止暴力破解攻击。
- 低代价因子:计算速度快,适用于对性能有要求的系统,但安全性相对较低。
- 高代价因子:计算速度慢,提高了攻击者进行暴力破解的成本,适用于需要高安全性的场景。
实践建议:
- 默认值:通常设置为 10 是一个平衡的选择,既能保证足够的安全性,又不会影响用户体验。
- 动态调整:随着硬件性能的提升,定期评估并增加代价因子值,以应对潜在的暴力破解威胁。
BCrypt特点
- 内置加盐机制: BCrypt 把盐作为了算法的一部分,强制我们遵循安全保存密码的最佳实践。
- 可调节的计算成本: 通过调整代价因子,可以增加哈希计算的时间,进一步增强密码的安全性。
BCrypt 通过引入计算时间和复杂的加盐机制,使得暴力破解和彩虹表攻击变得极其困难。相比之下,MD5 的速度和结构决定了它更容易被破解,因此不推荐用于密码存储。选择合适的代价因子(如 12 或 14)能在安全性和性能之间取得平衡,是保护用户密码的有效方式。
防止暴力破解
在密码存储和验证之外,配套的安全防御机制对于保护用户账户至关重要。以下是几种常见的安全防御措施及其作用:
1. 登录失败锁定
当用户多次输入错误的密码时,暂时锁定账号可以有效防止暴力破解攻击。通常的实现方式有:
- 逐步延迟:在多次错误尝试后,每次登录尝试之间引入逐步增加的延迟时间。
- 账号锁定:在一定次数的错误尝试后,暂时锁定账号一段时间(例如 5 分钟),或要求用户通过额外验证解锁(例如通过邮件或短信)。
这种机制可以大大增加攻击者暴力破解的难度,同时又不至于对用户造成过多的使用不便。
2. 双因素认证(2FA)
双因素认证要求用户在登录时,除了输入密码外,还需要提供另一个独立的认证信息(通常是一次性验证码),以进一步验证用户身份。常见的 2FA 方法包括:
- 短信验证码:用户在输入密码后,会收到一条带有一次性验证码的短信,用户需在登录界面输入该验证码完成认证。
- 认证应用:例如 Google Authenticator、Authy 等应用,会生成基于时间的动态验证码,用户需输入该验证码才能完成登录。
- 硬件令牌:如 U2F 安全密钥,在登录时需要插入或接触物理设备以完成认证。
2FA 有效地增加了账户安全性,因为即使密码被泄露,攻击者仍需获得第二个认证因素才能登录。
3. 图形验证码或短信验证
图形验证码和短信验证可以防止自动化攻击,例如暴力破解工具或脚本批量尝试登录:
- 图形验证码:在用户登录时,要求用户输入图片上显示的字符。这类验证码通过生成随机图像、扭曲字符等方式,阻止自动化脚本识别并输入正确的字符。
- 短信验证:在检测到异常登录行为(例如来自不同地域的登录尝试)时,要求用户输入短信验证码以完成登录。
这种机制可以有效防止自动化攻击,提高系统的安全性。
4. 异常检测与应对
现代系统还可以通过分析用户行为,检测异常活动并采取措施:
- 地理位置分析:如果用户在短时间内从两个远离的地点尝试登录,系统可以标记为异常行为,并要求额外的验证步骤。
- 设备识别:如果用户从未使用过的设备或浏览器登录,系统可以要求进行额外验证,或者通知用户以防止潜在的账户劫持。
5. 登录通知
向用户发送登录通知(通过邮件或短信),告知他们账号的每次登录。这种做法可以让用户及时发现并报告未经授权的登录行为,从而迅速采取防御措施。
总结
除了使用安全的哈希算法外,配套的安全防御机制(如登录失败锁定、双因素认证、图形验证码、异常行为检测等)是构建健壮的用户认证系统的关键。这些机制共同作用,能显著降低账户被攻击的风险,保护用户数据的安全。

相关文章:
加密与安全_优雅存储用户密码的最佳实践
文章目录 Pre概述最佳实践避免使用MD5、SHA1等快速哈希算法加盐哈希 (不推荐)使用BCrypt、Argon2等慢哈希算法 (推荐)BCrypt Code1. 自动生成和嵌入盐2. 哈希结果的格式3. 代价因子 BCrypt特点 防止暴力破解1. 登录失败锁定2. 双因素认证(2FA…...
【多线程】深入剖析线程池的应用
💐个人主页:初晴~ 📚相关专栏:多线程 / javaEE初阶 还记得我们一开始引入线程的概念,就是因为进程太“重”了,频繁创建销毁进程的开销是非常大的。而随着计算机的发展,业务上对性能的要求越来越…...
『功能项目』切换职业面板【48】
我们打开上一篇47技能冷却蒙版的项目, 本章要做的事情是切换职业UI面板的功能 首先双击打开Canvas预制体在左上主角面板信息中新建一个button按钮 重命名(父物体是按钮Button,子物体Image即可) 创建一个Image 设计一下布局 复制三…...
【EasyExcel】@ColumnWidth(value = 20) EasyExcel设置列宽不生效
经过测试发现,只有XLS,ColumnWidth注解才会生效,选择CSV和XLSX都不会生效 //对应的导出实体类 EasyExcel.write(outputStream, Result.class)//excel文件类型,包括CSV、XLS、XLSX.excelType(ExcelTypeEnum.XLS)...
CPU 和 GPU:为什么GPU更适合深度学习?
目录 什么是 CPU ? 什么是 GPU ? GPU vs CPU 差异性对比分析 GPU 是如何工作的 ? GPU 与 CPU 是如何协同工作的 ? GPU vs CPU 类型解析 GPU 应用于深度学习 什么是 CPU ? CPU(中央处理器)…...
【机器学习】:解锁数据背后的智慧宝藏——深度探索与未来展望
欢迎来到 破晓的历程的 博客 ⛺️不负时光,不负己✈️ 文章目录 引言一、深入机器学习的内在机制二、最新进展与趋势三、对未来社会的深远影响结语 引言 在上一篇博客中,我们初步探讨了机器学习如何成为解锁数据背后智慧的关键工具。现在,让…...
【Kubernetes】常见面试题汇总(十八)
目录 55.简述 Kubernetes 共享存储的作用? 56.简述 Kubernetes 数据持久化的方式有哪些? 57.简述 Kubernetes PV 和 PVC ? 58.简述 Kubernetes PV 生命周期内的阶段? 55.简述 Kubernetes 共享存储的作用? Kubernet…...
无限边界:现代整合安全如何保护云
尽管云计算和远程工作得到广泛采用,零信任网络也稳步推广,但边界远未消失。相反,它已被重新定义。就像数学分形的边界一样,现代网络边界现在无限延伸到任何地方。 不幸的是,传统工具在现代无限边界中效果不佳。现代边…...
HTML贪吃蛇游戏
文章目录 贪吃蛇游戏 运行效果代码 贪吃蛇游戏 贪吃蛇是一款经典的休闲益智游戏。本文将通过HTML5和JavaScript详细解析如何实现一个简易版的贪吃蛇游戏。游戏的主要逻辑包括蛇的移动、碰撞检测、食物生成等功能。以下是游戏的完整代码及注释解析。(纯属好玩&#…...
HTML 揭秘:HTML 编码快速入门
HTML 揭秘:HTML 编码快速入门 一 . 前端知识介绍二 . HTML 介绍三 . HTML 快速入门四 . HTML 编辑器 - VSCode4.1 插件安装4.2 修改主题配色4.3 修改快捷键4.4 设置自动保存4.5 创建 HTML 文件4.5 书写 HTML 代码4.6 常见快捷键 五 . 基础标签5.1 字体标签5.1.1 col…...
Ubuntu22.04系统安装opencv步骤简述及问题解决方法
前言 opencv是一个功能强大、开源且跨平台的计算机视觉库,适用于多种编程语言和操作系统,能够帮助开发者构建各种视觉项目。其模块众多,提供了诸多功能,能够进行图像处理、视频处理等等。比如:Highgui模块提供图像用户…...
移情别恋c++ ദ്ദി˶ー̀֊ー́ ) ——13.mapset
1. 关联式容器 在初阶阶段,我们已经接触过STL中的部分容器,比如:vector、list、deque、 forward_list(C11)等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面 存储的是元素本身。那什么是关…...
【webpack4系列】webpack基础用法(二)
文章目录 entryoutputloaderpluginmode前端构建基础配置关联HTML插件html-webpack-plugin构建 CSS 解析 ES6和React JSX解析 ES6解析 React JSX 解析CSS、Less和Sass解析CSS解析Less解析sass 解析图片和字体资源解析:解析图片资源解析:解析字体资源解析&…...
Python Pyvis库创建交互式网络图 高级功能详解
文章目录 动态网络图图布局调整扩展到大规模网络动态网络图 Pyvis支持创建动态网络图,通过时间轴展示网络图的演化过程。 需要使用set_options函数,参数必须为json格式。动态网络图支持添加点和边。 下面是一个简单的动态网络图示例: # 动态网络图示例 from pyvis.networ…...
Linux服务器上安装git lfs命令
有时候,需要批量下载数据集时要用到git lfs命令 首先,使用pip install git-lfs安装,会发现使用时仍然提示:git: lfs is not a git command. See git --help. 这就意味着安装不成功。 因此,需要通过如下途径手动安装&a…...
S100A9:鸡支原体感染中的免疫调控“双面间谍”【AbMole】
在生物学研究的广阔天地里,总有一些分子扮演着令人意想不到的角色。今天,我们要探索的主角是S100A9蛋白,一种在鸡支原体感染过程中展现出惊人双重功能的分子。这项来自华中农业大学动物科技学院与兽医学院的最新研究成果,揭示了S1…...
黑神话悟空黑风山攻略
在黑神话悟空中,玩家一开始来到黑风山的地图就会站在前山土地庙。 下面小编将会根据黑风山地图的地标来进行路线攻略推荐, 玩家可以一边查看游戏地图一边了解这些路线的动线是怎样的。 在苍狼林前山捡到药材老山参,跟随金色光线找到附近的土…...
Android 11 FileProvider的使用和限制
概述: 从Android 7开始,将不允许在app之间,使用file uri,即file://的方式,传递一个file,否则会抛出异常:FileUriExposedException ,其解决方案,就是使用FileProvider,用c…...
闭包+面试真题
对闭包的理解 闭包是内层函数使用外层变量 (子级可以访问父级的变量,但是父级不可以访问子级的) 闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部…...
Java企业面试题3
1. break和continue的作用(智*图) break:用于完全退出一个循环(如 for, while)或一个 switch 语句。当在循环体内遇到 break 语句时,程序会立即跳出当前循环体,继续执行循环之后的代码。continue:用于跳过…...
多场景 OkHttpClient 管理器 - Android 网络通信解决方案
下面是一个完整的 Android 实现,展示如何创建和管理多个 OkHttpClient 实例,分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...
《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》
在注意力分散、内容高度同质化的时代,情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现,消费者对内容的“有感”程度,正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中࿰…...
Java - Mysql数据类型对应
Mysql数据类型java数据类型备注整型INT/INTEGERint / java.lang.Integer–BIGINTlong/java.lang.Long–––浮点型FLOATfloat/java.lang.FloatDOUBLEdouble/java.lang.Double–DECIMAL/NUMERICjava.math.BigDecimal字符串型CHARjava.lang.String固定长度字符串VARCHARjava.lang…...
在四层代理中还原真实客户端ngx_stream_realip_module
一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡(如 HAProxy、AWS NLB、阿里 SLB)发起上游连接时,将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后,ngx_stream_realip_module 从中提取原始信息…...
Frozen-Flask :将 Flask 应用“冻结”为静态文件
Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是:将一个 Flask Web 应用生成成纯静态 HTML 文件,从而可以部署到静态网站托管服务上,如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...
srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...
Java线上CPU飙高问题排查全指南
一、引言 在Java应用的线上运行环境中,CPU飙高是一个常见且棘手的性能问题。当系统出现CPU飙高时,通常会导致应用响应缓慢,甚至服务不可用,严重影响用户体验和业务运行。因此,掌握一套科学有效的CPU飙高问题排查方法&…...
HarmonyOS运动开发:如何用mpchart绘制运动配速图表
##鸿蒙核心技术##运动开发##Sensor Service Kit(传感器服务)# 前言 在运动类应用中,运动数据的可视化是提升用户体验的重要环节。通过直观的图表展示运动过程中的关键数据,如配速、距离、卡路里消耗等,用户可以更清晰…...
并发编程 - go版
1.并发编程基础概念 进程和线程 A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中…...
认识CMake并使用CMake构建自己的第一个项目
1.CMake的作用和优势 跨平台支持:CMake支持多种操作系统和编译器,使用同一份构建配置可以在不同的环境中使用 简化配置:通过CMakeLists.txt文件,用户可以定义项目结构、依赖项、编译选项等,无需手动编写复杂的构建脚本…...

![> [ \frac{218,340,105,584,896}{1,250} = 174,672,084,467 \text{ 秒} ]](https://i-blog.csdnimg.cn/direct/dfcaf7c9893348e8a0f9e1953cefab64.png)
