前言

密码认证是身份认证中最常用的方法。密码作为证明身份的凭证,一但发生泄露,会对用户的信息安全造成极大的威胁。作为开发人员,在开发用户系统时要充分考虑,明文存储、明文传输是非常不明智的行为,极易造成密码泄露。

这里记录一下自己使用过的一种密码加密和存储的方法。

后端

后端直接管理密码的存储,需要保证密码存储的安全。给密码进行哈希加密是一种常见的做法,这样即使数据库发生泄露,已被哈希加密的密码也无法被直接使用。而为了防止用户设置简单的密码被暴力破解,哈希加密前往往还会向原密码中加入盐值。

这里使用SHA256进行哈希加密,随机生成32~64位的盐值,随加密后的密码一起保存到数据库中。

编写一个加密工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

/**
* 生成盐值和对字符串使用SHA256加密的工具类
*/
public class EncryptUtil {
/**
* 使用SHA256进行加密
*
* @param password 密码
* @param salt 盐值
* @return 加密后长为64的字符串
*/
public static String Encrypt(String password, String salt){
String str = salt + password;
MessageDigest messageDigest;
String encodeStr = "";
try {
messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(str.getBytes(StandardCharsets.UTF_8));
encodeStr = byteToHex(messageDigest.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return encodeStr;
}

/**
* 生成随机32~64位的盐值
*
* @return 生成的盐值
*/
public static String getSalt(){
SecureRandom random = new SecureRandom();

int randomInt = random.nextInt(16) + 16;
byte[] bytes = new byte[randomInt];
random.nextBytes(bytes);

//转换为16进制字符串并返回
return byteToHex(bytes);
}

/**
* 将byte转为16进制表示的字符串
*
* @param bytes 字节码
* @return 16进制表示的字符串
*/
private static String byteToHex(byte[] bytes) {
StringBuilder stringBuilder = new StringBuilder();
String temp;
for (byte aByte : bytes) {
temp = Integer.toHexString(aByte & 0xFF);
if (temp.length() == 1) {
//1得到一位的进行补0操作
stringBuilder.append("0");
}
stringBuilder.append(temp);
}
return stringBuilder.toString();
}
}

由此,只需使用String salt = EncryptUtil.getSalt()即可生成随机盐值,再使用String encryptedPassword = EncryptUtil.Encrypt(password, salt)即可得到加密后的密码。

将新用户存入数据库

1
2
3
4
5
6
7
8
9
10
//密码加盐加密
String salt = EncryptUtil.getSalt();
String encryptedPassword = EncryptUtil.Encrypt(password,salt);
//存入数据库中
User newUser = new User();
newUser.setUserName(userName);
newUser.setUserPassword(encryptedPassword);
newUser.setSalt(salt);
boolean saveResult = userService.save(newUser);
...

用户登录校验

结合Mybatis查询数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//按用户名查询用户
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_name", userName);
User user = userMapper.selectOne(queryWrapper);
if(user == null){
//未查询到用户,登录失败
...
}

//加盐加密后与数据库中的密码比较
String encryptedPassword = EncryptUtil.Encrypt(password, user.getSalt());
if(!encryptedPassword.equals(user.getUserPassword())){
//用户名与密码不对应,登录失败
...
}
//登录成功
...

前端

前端同样对密码进行一次哈希加密,使用crypto-js包,以网站域名或是用户名或是其他自定义的字符串作为盐值,进行SHA256哈希加密。

1
2
3
4
5
6
7
import CryptoJS from 'crypto-js';

const handleSubmit = async (values) => {
...
values.password = CryptoJS.SHA256(values.userName! + values.password).toString();
...
}

前端加密的目的是为了防止请求被破解后原密码泄露,这样用户即使在其他地方也使用了相同的密码,也无法使用泄露的密码登录。前端加盐的目的则是为了让哈希加密更难以破解,即使原密码非常简单,加盐后也使得破解难度大大增加。

值得注意的是,如果请求真的被破解了,那么前端加密后的密码实际上已经相当于用户的登录凭证,可以直接使用加密后的密码发起请求进行登录,所以注册、登录以及各种数据交互的安全性归根到底还是要由https来保证的。