拒绝“明文”裸奔!NestJS + Bcrypt 打造企业级用户注册与异常防御体系

3 阅读4分钟

拒绝“明文”裸奔!NestJS + Bcrypt 打造企业级用户注册与异常防御体系

在后端开发的江湖里,用户注册看似是“Hello World”级别的功能,实则暗藏杀机。很多新手(甚至老手)容易犯的一个致命错误就是:把用户密码明文存入数据库

这就像是把你家大门的钥匙直接贴在门上,还挂个牌子写着“欢迎来拿”。一旦数据库泄露(删库跑路的前同事、黑客攻击等),所有用户的账号都将瞬间沦陷。

今天,我们就基于 NestJS 和 Prisma,聊聊如何给密码穿上“防弹衣”,以及如何优雅地处理那些“不听话”的请求。

为什么密码不能存明文?Bcrypt 是什么鬼?

首先,我们要确立一个铁律:永远、永远不要存储用户的明文密码!

那存什么?存哈希值

这里我们请出今天的男一号:bcrypt

  • 单向加密(哈希) :你可以把它想象成“榨汁机”。把水果(密码)放进去,榨成汁(哈希值)。但你绝对不可能把果汁还原成原来的水果。这意味着,即使是拥有数据库最高权限的 DBA,或者不小心“删库跑路”的你,也无法得知用户的原始密码是多少。
  • 加盐:为了防止黑客使用“彩虹表”进行暴力破解,bcrypt 会在 hashing 之前自动给密码加点“佐料”(Salt)。哪怕两个用户都用了 123456 这种弱智密码,加密后的结果也是天差地别。

代码实战:

import * as bcrypt from 'bcrypt'

// 10 代表加密强度( rounds),数字越大越安全但也越慢,10 是个不错的平衡点
const hashedPassword = await bcrypt.hash(password, 10);

看,只需要一行代码,你的密码就从“裸奔”状态变成了“全副武装”。

核心战场:UsersService 里的注册逻辑

让我们深入 UsersServiceregister 方法,看看一场标准的注册流程是如何发生的。

第一步:查重——“这个名字被占用了!”

在创建用户之前,我们必须先问问数据库:“哥们,这个用户名有人用了吗?”

const existingUser = await this.prisma.user.findUnique({
  where: { name }
})
if (existingUser) {
  // 如果查到了,说明名字重复
  throw new BadRequestException("用户名已存在")
}

这里我们用到了 NestJS 强大的异常类 BadRequestException。一旦触发 throw,请求就会立刻中断,不会傻傻地继续往下执行。

第二步:加密与入库

如果名字没被占用,我们就可以放心地处理密码了。

// 再次强调:不要把 password 字段暴露给前端!
const user = await this.prisma.user.create({
  data: {
    name,
    password: hashedPassword // 存入的是密文!密文!
  },
  select: {
    id: true,
    name: true
    // 注意:这里故意没有 select password,防止敏感信息泄露
  }
})

异常处理:别让服务器“原地爆炸”

你可能会问:“如果在 findUnique 的时候数据库挂了怎么办?或者 bcrypt 抽风了怎么办?我的服务会崩吗?”

这就是 NestJS 迷人的地方。

JavaScript 是单线程的,如果不妥善处理错误,一个未捕获的异常可能会导致整个进程崩溃(Crash)。但在 NestJS 中,我们利用 try-catch 的思想(虽然这里是通过框架层面的过滤器实现),配合 BadRequestException 这样的标准异常类,可以将错误转化为标准的 HTTP 响应。

当我们在 Service 层抛出 new BadRequestException("用户名已存在") 时:

  1. Controller 层不需要写一堆 if (error) 的判断。
  2. NestJS 的内置异常过滤器会捕获它。
  3. 客户端会收到一个清晰的 JSON 响应:{ "statusCode": 400, "message": "用户名已存在", "error": "Bad Request" }

这就叫企业级容错。即使后端逻辑出错,返回给前端的也是一个优雅的 400 或 500 状态码,而不是一堆让前端小哥抓狂的 HTML 报错页面。

完整代码赏析

最后,让我们把刚才讨论的知识点串起来,看看这段丝滑的代码:

import { Injectable, BadRequestException } from '@nestjs/common'
import { PrismaService } from '../prisma/prisma.service'
import { CreateUserDto } from './dto/create-user.dto'
import * as bcrypt from 'bcrypt'

@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}

  async register(createUserDto: CreateUserDto) {
    const { name, password } = createUserDto;

    // 1. 检查用户名是否存在
    const existingUser = await this.prisma.user.findUnique({
      where: { name }
    })

    if (existingUser) {
      // 2. 遇到错误,直接抛出 NestJS 标准异常
      // 这里的 throw 非常关键,它阻断了后续逻辑,并通知框架返回 400
      throw new BadRequestException("用户名已存在")
    }

    // 3. 密码加密:单向哈希,防君子也防小人
    // 哪怕你是程序员,也没法反推出用户的密码
    const hashedPassword = await bcrypt.hash(password, 10);

    // 4. 入库
    const user = await this.prisma.user.create({
      data: {
        name,
        password: hashedPassword // 存入密文
      },
      select: {
        id: true,
        name: true
        // 返回数据时,坚决不带 password 字段,安全第一
      }
    })

    return user
  }
}

总结

写后端不仅仅是 CRUD,更是关于安全健壮性的艺术。

  • 使用 bcrypt 确保密码即使泄露也无法被还原。
  • 使用 Prismaselect 选项控制数据返回范围,避免敏感字段外泄。
  • 使用 BadRequestException 统一处理业务异常,给用户明确的反馈,而不是冷冰冰的服务器崩溃。

好了,现在你的用户注册功能已经具备了初步的企业级素养。接下来,是不是该考虑怎么让他们登录了?