nestjs实战-登录、鉴权(三-密码加密)

3 阅读4分钟

先直接看一段创建用户的代码:

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元,这个过程至少包含两个步骤:

  1. 从A的账户中扣除100元。
  2. 向B的账户中增加100元。

这两个操作必须被放在一个事务中。如果第一步成功了,但第二步因为某种原因(比如系统故障)失败了,那么事务机制会保证第一步的操作也被撤销(即“回滚”),A的100元会回到他的账户里,从而避免出现“钱扣了但对方没收到”的数据不一致问题。

密码加密:

加密方式采用:

let pd = md5(`${password ?? '123456'}${salt}`)

md5

MD5 不是 一种加密算法,而是一种哈希(Hash)算法,也常被称为“摘要算法”

  • 不可逆:md5后的随机字符串是 不可逆的;所以服务端是不知道用户的密码的
  • 同样的输入,永远得到同样的输出

加盐

“加盐”(Salting)是一种在密码哈希过程中,人为地加入一段随机字符串(即“盐”)的安全技术。

它的核心作用是:让即使是完全相同的密码,在经过哈希运算后,也能得到完全不同的结果。

为什么要“加盐”?

这主要是为了对抗一种叫做“彩虹表”的攻击。

  1. 不加盐的风险

    • MD5 算法有一个特性:同样的输入,永远得到同样的输出。
    • 例如,密码 123456 的 MD5 值永远是 e10adc3949ba59abbe56e057f20f883e
    • 黑客会预先计算出海量常见密码(如 123456, password 等)的 MD5 值,并整理成一张巨大的“彩虹表”。
    • 一旦网站数据库泄露,黑客只需将泄露的 MD5 值与彩虹表进行比对,就能瞬间反查出原始密码。
  2. 加盐如何解决问题

    • 系统会为每个用户在注册时生成一个独一无二的随机“盐”。
    • 在计算密码的 MD5 值前,系统会先将这个“盐”和密码拼接在一起。

举个例子:

假设用户 A 和用户 B 的密码都是 123456

  • 不加盐时:

    • A 的密码哈希值:MD5("123456") = e10adc39...
    • B 的密码哈希值:MD5("123456") = e10adc39...
    • 结果:两个哈希值完全相同,黑客一看就知道他们的密码一样,破解一个就等于破解了两个。
  • 加盐后:

    • 系统为用户 A 生成随机盐 abc,计算 MD5("123456abc") = x9f2k...
    • 系统为用户 B 生成随机盐 xyz,计算 MD5("123456xyz") = m3n7p...
    • 结果:虽然原始密码相同,但最终存储的哈希值却天差地别。黑客无法使用彩虹表进行批量破解,破解难度呈指数级上升。

服务端如何验证密码的正确性

  1. 用户注册时,会生成一个随机的salt

    let salt = 一个随机数;
    let pd = md5(`${password ?? '123456'}${salt}`)
    

    然后数据password,salt 存入数据库

  2. 用户登录时:根据用户名,从数据库中取出为你存储的专属 盐(salt)哈希值(password)

  3. 将你本次输入的密码和取出的“盐”进行同样的拼接。

  4. 对拼接后的字符串进行 MD5 运算。

  5. 将运算结果与数据库中存储的哈希值进行比对。如果一致,则密码正确。

最后看一段校验密码的代码:

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)
  
  // 后面逻辑则表示密码正确,密码不一致,在上面代码报错 跳出方法;
  // 。。。。。
}