目录:
1.SHA-256和Salt
1.1.什么是SHA-256
SHA-256是一种信息摘要算法,也是一种密码散列函数。对于任意长度的消息,SHA256都会产生一个256bit长的散列值(哈希值),用于确保信息传输完整一致,称作消息摘要。这个摘要相当于是个长度为32个字节的数组,通常用一个长度为64的十六进制字符串来表示。
SHA-256的具备以下几个关键特点:
- 固定长度输出:无论输入数据的大小,SHA-256都会产生一个256位(32字节)的固定长度散列值。
- 不可逆性:SHA-256的设计使得从生成的散列值无法还原原始输入数据。这种不可逆性在安全性上是非常重要的。
- 抗碰撞性:找到两个不同的输入数据具有相同的散列值(碰撞)是极其困难的。虽然理论上碰撞可能发生,但SHA-256被设计得非常抗碰撞。
除了SHA-256之外,还有一个密码散列函数MD5,过去也常被用于密码加密,但MD5在安全性上低于SHA-256,现在已经很少用于密码加密了,本文不做考虑。
SHA-256 和 MD5 的比较:
特性 | SHA-256 | MD5 |
---|---|---|
输出长度 | 256 位(64 个十六进制字符) | 128 位(32 个十六进制字符) |
安全性 | 高 | 低 |
计算速度 | 较慢 | 快 |
抗碰撞能力 | 强 | 弱 |
应用场景 | 数据完整性校验、数字签名、密码存储、区块链 | 曾用于文件校验、密码存储 |
推荐使用 | 是 | 否 |
1.2.什么是随机盐值
盐值(salt) 是一种在密码学和安全计算中常用的随机数据,用于增强密码散列的安全性。
随机盐值(random salt)是一种用于增强密码散列安全性的技术。它是一个随机生成的数据块,在将密码输入散列函数之前,将盐值与密码组合。通过引入随机盐值,可以有效地防止彩虹表攻击和相同密码散列值重复的问题。
盐值的作用:
- 防止彩虹表攻击: 彩虹表是一个预计算的哈希值数据库,用于快速查找常见密码的哈希值。通过在密码哈希之前加入随机盐值,即使密码相同,其最终的哈希值也会不同,从而使彩虹表无效。
- 避免散列值重复: 如果两个用户使用相同的密码,在没有盐值的情况下,他们的哈希值会相同。加入盐值后,即使密码相同,生成的哈希值也会不同,这有助于防止攻击者通过观察哈希值来推测用户是否使用了相同的密码。
- 增加攻击难度: 盐值增加了密码哈希的复杂性。即使攻击者获取了存储的哈希值和盐值,他们仍需对每个盐值进行单独的暴力破解,显著增加了破解的时间和计算成本。
1.3.如何进行加密操作
本文采用的加密方式是在前端采用md加密防止明文传输,后端对密码二次加密后再进行随机盐值的混入。
2.前端实现
前端先进行一次SHA256加密,防止明文传输密码。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Password SHA256 Encryption Example</title>
</head>
<body>
<h1>Password SHA256 Encryption</h1>
<input type="password" id="passwordInput" placeholder="Enter your password">
<button onclick="encryptPassword()">Encrypt Password</button>
<p>Encrypted Password: <span id="encryptedPassword"></span></p>
<script>
function encryptPassword() {
const password = document.getElementById('passwordInput').value;
if (password) {
window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(password))
.then(hashBuffer => {
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
document.getElementById('encryptedPassword').textContent = hashHex;
})
.catch(err => {
console.error('Encryption failed:', err);
document.getElementById('encryptedPassword').textContent = 'Error: Encryption failed';
});
} else {
document.getElementById('encryptedPassword').textContent = 'Please enter a password';
}
}
</script>
</body>
</html>
3.后端实现
3.1.导入Maven依赖
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
3.2.密码加密
3.2.1.密码加盐
首先使用Apache的RandomStringUtils
工具类,生成16位的盐值。然后将盐拼接到明文后面,进行SHA256加密。
这个加密后的SHA256是个固定64长度的字符串。
// 生成一个16位的随机数,也就是盐
String salt = RandomStringUtils.randomAlphanumeric(16);
// 将盐拼接到明文后,并生成新的sha256码
String sha256Hex = DigestUtils.sha256Hex(password + salt);
3.2.2.随机盐值混合
加盐后的SHA256码长度为80位,这里我们采用的盐值混合规则:将SHA-256散列值的每四个字符中间插入一个盐值字符,依次交替排列。
// 将盐混到新生成的SHA-256码中,之所以这样做是为了后期解密,校验密码
StringBuilder sb = new StringBuilder(80); // SHA-256是64个字符,加16个字符的盐,总共80个字符
for (int i = 0; i < 16; i++) {
sb.append(sha256Hex.charAt(i * 4));
sb.append(salt.charAt(i));
sb.append(sha256Hex.charAt(i * 4 + 1));
sb.append(sha256Hex.charAt(i * 4 + 2));
sb.append(sha256Hex.charAt(i * 4 + 3));
}
return sb.toString();
这样就完成了加密的操作:密码加盐 + 盐值混合。
3.3.密码解密
3.3.1.提取盐值和加盐密码
按照加密时采用的规则:将SHA-256散列值的每四个字符中间插入一个盐值字符,依次交替排列。
我们可以将盐值和加盐后的SHA-256码
// 提取盐值和加盐后的SHA-256码
StringBuilder sb1 = new StringBuilder(64);
StringBuilder sb2 = new StringBuilder(16);
for (int i = 0; i < 16; i++) {
sb1.append(encrypted.charAt(i * 5));
sb1.append(encrypted.charAt(i * 5 + 2));
sb1.append(encrypted.charAt(i * 5 + 3));
sb1.append(encrypted.charAt(i * 5 + 4));
sb2.append(encrypted.charAt(i * 5 + 1));
}
String sha256Hex = sb1.toString();
String salt = sb2.toString();
3.3.2.比较密码
最后,将取出的盐值与原始密码再次加盐,再次得到加盐密码,与sha256Hex比较即可判断密码是否相同。
// 比较二者是否相同
return DigestUtils.sha256Hex(password + salt).equals(sha256Hex);
3.4.完整工具类
public class SHA256Util {
/**
* 加密
* 生成盐和加盐后的SHA-256码,并将盐混入到SHA-256码中,对SHA-256密码进行加强
**/
public static String encryptPassword(String password) {
// 生成一个16位的随机数,也就是盐
String salt = RandomStringUtils.randomAlphanumeric(16);
// 将盐拼接到明文后,并生成新的sha256码
String sha256Hex = DigestUtils.sha256Hex(password + salt);
// 将盐混到新生成的SHA-256码中,之所以这样做是为了后期解密,校验密码
StringBuilder sb = new StringBuilder(80); // SHA-256是64个字符,加16个字符的盐,总共80个字符
for (int i = 0; i < 16; i++) {
sb.append(sha256Hex.charAt(i * 4));
sb.append(salt.charAt(i));
sb.append(sha256Hex.charAt(i * 4 + 1));
sb.append(sha256Hex.charAt(i * 4 + 2));
sb.append(sha256Hex.charAt(i * 4 + 3));
}
return sb.toString();
}
/**
* 解密
* 从混入盐的SHA-256码中提取盐值和加盐后的SHA-256码
**/
public static boolean verifyPassword(String password, String encrypted) {
// 提取盐值和加盐后的SHA-256码
StringBuilder sb1 = new StringBuilder(64);
StringBuilder sb2 = new StringBuilder(16);
for (int i = 0; i < 16; i++) {
sb1.append(encrypted.charAt(i * 5));
sb1.append(encrypted.charAt(i * 5 + 2));
sb1.append(encrypted.charAt(i * 5 + 3));
sb1.append(encrypted.charAt(i * 5 + 4));
sb2.append(encrypted.charAt(i * 5 + 1));
}
String sha256Hex = sb1.toString();
String salt = sb2.toString();
// 比较二者是否相同
return DigestUtils.sha256Hex(password + salt).equals(sha256Hex);
}
}