先直接看一段创建用户的代码:
async create({
username,
password,
...data
}: UserDto): Promise<void> {
const exists = await this.userRepository.findOneBy({
username,
})
if (!isEmpty(exists))
throw new BusinessException(ErrorEnum.SYSTEM_USER_EXISTS)
await this.entityManager.transaction(async (manager) => { // 事务
const salt = randomValue(32)
if (!password) {
const initPassword = await this.paramConfigService.findValueByKey(
SYS_USER_INITPASSWORD,
)
password = md5(`${initPassword ?? '123456'}${salt}`)
}
else {
password = md5(`${password ?? '123456'}${salt}`)
}
const u = manager.create(UserEntity, {
username,
password,
...data,
psalt: salt,
})
const result = await manager.save(u)
return result
})
}
事务:
首先需要理解数据库操作中,什么叫事务:
事务最核心的思想是保证数据的一致性和完整性。
- 它确保一系列相关的操作,要么全部成功执行
- 要么在出现任何问题时全部撤销,让系统恢复到事务开始前的状态。
一个最经典的例子就是银行转账: 假设用户A要向用户B转账100元,这个过程至少包含两个步骤:
- 从A的账户中扣除100元。
- 向B的账户中增加100元。
这两个操作必须被放在一个事务中。如果第一步成功了,但第二步因为某种原因(比如系统故障)失败了,那么事务机制会保证第一步的操作也被撤销(即“回滚”),A的100元会回到他的账户里,从而避免出现“钱扣了但对方没收到”的数据不一致问题。
密码加密:
加密方式采用:
let pd = md5(`${password ?? '123456'}${salt}`)
md5
MD5 不是 一种加密算法,而是一种哈希(Hash)算法,也常被称为“摘要算法”
- 不可逆:md5后的随机字符串是 不可逆的;
所以服务端是不知道用户的密码的 - 同样的输入,永远得到同样的输出
加盐
“加盐”(Salting)是一种在密码哈希过程中,人为地加入一段随机字符串(即“盐”)的安全技术。
它的核心作用是:让即使是完全相同的密码,在经过哈希运算后,也能得到完全不同的结果。
为什么要“加盐”?
这主要是为了对抗一种叫做“彩虹表”的攻击。
-
不加盐的风险
- MD5 算法有一个特性:同样的输入,永远得到同样的输出。
- 例如,密码
123456的 MD5 值永远是e10adc3949ba59abbe56e057f20f883e。 - 黑客会预先计算出海量常见密码(如
123456,password等)的 MD5 值,并整理成一张巨大的“彩虹表”。 - 一旦网站数据库泄露,黑客只需将泄露的 MD5 值与彩虹表进行比对,就能瞬间反查出原始密码。
-
加盐如何解决问题
- 系统会为每个用户在注册时生成一个独一无二的随机“盐”。
- 在计算密码的 MD5 值前,系统会先将这个“盐”和密码拼接在一起。
举个例子:
假设用户 A 和用户 B 的密码都是 123456。
-
不加盐时:
- A 的密码哈希值:
MD5("123456")=e10adc39... - B 的密码哈希值:
MD5("123456")=e10adc39... - 结果:两个哈希值完全相同,黑客一看就知道他们的密码一样,破解一个就等于破解了两个。
- A 的密码哈希值:
-
加盐后:
- 系统为用户 A 生成随机盐
abc,计算MD5("123456abc")=x9f2k... - 系统为用户 B 生成随机盐
xyz,计算MD5("123456xyz")=m3n7p... - 结果:虽然原始密码相同,但最终存储的哈希值却天差地别。黑客无法使用彩虹表进行批量破解,破解难度呈指数级上升。
- 系统为用户 A 生成随机盐
服务端如何验证密码的正确性
-
用户注册时,会生成一个
随机的saltlet salt = 一个随机数; let pd = md5(`${password ?? '123456'}${salt}`)然后数据password,salt 存入数据库
-
用户登录时:根据用户名,从数据库中取出为你存储的专属
盐(salt)和哈希值(password)。 -
将你本次输入的密码和取出的“盐”进行同样的拼接。
-
对拼接后的字符串进行 MD5 运算。
-
将运算结果与数据库中存储的哈希值进行比对。如果一致,则密码正确。
最后看一段校验密码的代码:
async checkPassword(username: string, password: string) {
const user = await this.userService.findUserByUserName(username)
const comparePassword = md5(`${password}${user.psalt}`)
if (user.password !== comparePassword)
throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD)
// 后面逻辑则表示密码正确,密码不一致,在上面代码报错 跳出方法;
// 。。。。。
}