拒绝"明文"裸奔!NestJS + Bcrypt 打造企业级用户注册与异常防御体系
在后端开发的江湖里,用户注册 看似是"Hello World"级别的功能,实则暗藏杀机。很多新手(甚至老手)容易犯的一个致命错误就是:把用户密码明文存入数据库。
这就像是把你家大门的钥匙直接贴在门上,还挂个牌子写着"欢迎来拿"。一旦数据库泄露(删库跑路的前同事、黑客攻击等),所有用户的账号都将瞬间沦陷。
今天,我们就基于 NestJS 和 Prisma,聊聊如何给密码穿上"防弹衣",以及如何优雅地处理那些"不听话"的请求。
为什么密码不能存明文?Bcrypt 是什么鬼?
首先,我们要确立一个铁律:永远、永远不要存储用户的明文密码!
那存什么?存哈希值。
这里我们请出今天的男一号:bcrypt。
- 单向加密(哈希) :你可以把它想象成"榨汁机"。把水果(密码)放进去,榨成汁(哈希值)。但你绝对不可能把果汁还原成原来的水果。这意味着,即使是拥有数据库最高权限的 DBA,或者不小心"删库跑路"的你,也无法得知用户的原始密码是多少。
- 加盐 :为了防止黑客使用"彩虹表"进行暴力破解,
bcrypt会在 hashing 之前自动给密码加点"佐料"(Salt)。哪怕两个用户都用了123456这种弱智密码,加密后的结果也是天差地别。
代码实战:
javascript
import * as bcrypt from 'bcrypt'
// 10 代表加密强度( rounds),数字越大越安全但也越慢,10 是个不错的平衡点
const hashedPassword = await bcrypt.hash(password, 10);
看,只需要一行代码,你的密码就从"裸奔"状态变成了"全副武装"。
核心战场:UsersService 里的注册逻辑
让我们深入 UsersService 的 register 方法,看看一场标准的注册流程是如何发生的。
第一步:查重------"这个名字被占用了!"
在创建用户之前,我们必须先问问数据库:"哥们,这个用户名有人用了吗?"
csharp
const existingUser = await this.prisma.user.findUnique({
where: { name }
})
if (existingUser) {
// 如果查到了,说明名字重复
throw new BadRequestException("用户名已存在")
}
这里我们用到了 NestJS 强大的异常类 BadRequestException。一旦触发 throw,请求就会立刻中断,不会傻傻地继续往下执行。
第二步:加密与入库
如果名字没被占用,我们就可以放心地处理密码了。
csharp
// 再次强调:不要把 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("用户名已存在") 时:
- Controller 层不需要写一堆
if (error)的判断。 - NestJS 的内置异常过滤器会捕获它。
- 客户端会收到一个清晰的 JSON 响应:
{ "statusCode": 400, "message": "用户名已存在", "error": "Bad Request" }。
这就叫企业级容错。即使后端逻辑出错,返回给前端的也是一个优雅的 400 或 500 状态码,而不是一堆让前端小哥抓狂的 HTML 报错页面。
完整代码赏析
最后,让我们把刚才讨论的知识点串起来,看看这段丝滑的代码:
typescript
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确保密码即使泄露也无法被还原。 - 使用
Prisma的select选项控制数据返回范围,避免敏感字段外泄。 - 使用
BadRequestException统一处理业务异常,给用户明确的反馈,而不是冷冰冰的服务器崩溃。
好了,现在你的用户注册功能已经具备了初步的企业级素养。接下来,是不是该考虑怎么让他们登录了?