【NestJS】19 用户认证系统设计与实践总结

3185 字
16 分钟
【NestJS】19 用户认证系统设计与实践总结

作为前端开发,第一次从零构建完整的后端认证系统,记录一些思考和收获。

字数:约 1,900 字 | 阅读时间:5 分钟


前言 ✨#

经过前面的学习和实践,我终于完成了一个完整的用户认证系统。从注册、登录到权限管理,从配额系统到用户信息管理,每个接口都已跑通并通过测试。

作为一个前端开发,以前只是调用后端提供的接口,从来没想过这些接口背后的设计逻辑。这次亲手实现了一遍,才真正理解了很多之前”理所当然”的东西。

这篇文章不讲具体代码实现,而是想梳理一下思路,总结一些经验,记录一些思考。重在复盘,而非实战。

让我们开始吧!


一、用户系统的核心架构 🏗️#

1.1 数据模型设计#

User Schema 是整个系统的基础。一开始我以为用户表很简单,不就是用户名、邮箱、密码吗?但实际设计时才发现要考虑的东西很多:

User 表结构:
├── 基础信息(username、email、password)
├── 配额管理(credits、freeQuota、paidQuota)
├── 状态标识(isActive、isVip)
├── 第三方登录(wechatId)
└── 时间戳(createdAt、updatedAt)

我学到的设计原则

  • ✅ 唯一索引:email、username 必须唯一(这个很重要,不然会有重复注册的问题)
  • ✅ 默认值:新用户赠送初始配额(产品思维,让用户先体验)
  • ✅ 扩展性:预留第三方登录字段(虽然现在用不到,但以后可能要接微信登录)

💭 我的思考:以前做前端,表单验证只是为了”不让用户乱填”。现在做后端才明白,数据库层面的约束才是真正的防线。前端验证可以绕过,但数据库的唯一索引绕不过。


1.2 认证流程设计#

JWT vs Session:为什么选择 JWT?

学习之前,我只知道”登录后会有个 Token”,但不知道为什么要用 Token。现在理解了:

特性SessionJWT
存储位置服务器客户端
扩展性差(需要共享 Session)好(无状态)
性能需要查询存储直接验证签名
适用场景传统应用现代应用、微服务

JWT 的三部分结构

Header(头部):算法和类型
Payload(负载):用户信息
Signature(签名):验证完整性

我的理解

  • JWT 是无状态的,服务器不需要存储 Token(这点很关键,意味着可以水平扩展)
  • Token 包含用户信息,减少数据库查询(以前不理解为什么每次请求都要带 Token,现在明白了)
  • 通过签名验证,确保 Token 未被篡改(这是安全的核心)

💭 我的疑问:一开始我不理解,为什么 Token 可以放在客户端,不怕被篡改吗?后来才明白,Token 虽然可以被解码看到内容,但因为有签名,任何修改都会导致验证失败。就像一封有蜡封的信,你可以看,但一旦拆开就能发现。


二、安全性:这是我最大的收获 🔒#

2.1 密码安全#

永远不要明文存储密码!

这个道理我以前就知道,但不知道具体怎么做。现在学会了 bcrypt:

用户输入:password123
bcrypt 加密(加盐 + 哈希)
存储:$2a$10$N9qo8uLO...

bcrypt 的三个优势

  1. 加盐:每次加密结果不同,防止彩虹表攻击
  2. 慢速:故意设计得慢,防止暴力破解
  3. 自适应:可以调整计算复杂度

我学到的最佳实践

  • ✅ 使用 bcrypt 加密密码
  • ✅ 验证时使用 comparePassword()
  • ✅ 永远不返回密码给客户端
  • ❌ 不使用弱加密算法(MD5、SHA1)

💭 我的感悟:以前做前端表单验证,总觉得”密码至少 6 位”这种规则很烦。现在做后端才明白,这些规则都是为了安全。而且后端的验证更重要,因为前端验证可以被绕过。


2.2 Token 安全#

Token 设计的四个原则(这是我踩过坑后总结的):

1. 设置过期时间

expiresIn: '7d' // 7 天后失效
  • 一开始我设置了 30 天,后来发现这样不安全
  • 现在改成 7 天,平衡用户体验和安全性

2. 从 Authorization 头提取

Authorization: Bearer <token>
  • 不要在 URL 中传递 Token(会被记录到日志里)
  • 这个是我看别人的代码学到的

3. 验证签名

JwtService.verify(token) // 验证签名是否有效
  • 确保 Token 未被篡改
  • 使用强密钥(secret),不要用 ‘123456’ 这种

4. 不在 Token 中放敏感信息

// ✅ 可以放
{ userId: '123', username: 'alice' }
// ❌ 不要放
{ userId: '123', password: '...' }

💭 我的教训:一开始我把用户的所有信息都放 Token 里了,包括邮箱、手机号。后来才知道 Token 是可以被解码的(虽然不能篡改)。现在我只放必要的信息:userId 和 username。


2.3 用户隐私保护#

三个核心原则(这是我最容易忽略的):

1. 从 req.user 获取当前用户

// ✅ 正确
const userId = req.user.userId;
// ❌ 错误:允许用户访问任意用户数据
const userId = req.params.userId;

这个坑我踩过!一开始我写了个”修改用户信息”的接口,直接从 URL 参数拿 userId。结果任何人都可以修改别人的信息。后来才明白,要从 req.user 拿当前登录用户的 ID。

2. 不返回敏感字段

// 使用 select 排除密码
.select('-password')
// 或在 Schema 中设置
@Prop({ select: false })
password: string;

3. 检查权限

// 更新用户信息时,确保是本人
if (req.user.userId !== targetUserId) {
throw new ForbiddenException('无权操作');
}

💭 我的反思:以前做前端,总觉得”用户只能看到自己的数据”是理所当然的。现在做后端才知道,这些都需要代码来保证。前端的权限控制只是 UI 层面的,真正的权限控制在后端。


三、配额系统:这个把我难住了 ⚡#

3.1 为什么需要原子操作?#

这是我遇到的最难理解的概念。一开始我写的代码是这样的:

// ❌ 我最初的错误写法
const user = await this.userModel.findById(userId);
if (user.quota > 0) {
user.quota--;
await user.save();
}

看起来没问题对吧?但测试的时候发现,如果两个请求同时来,配额会变成负数!

问题场景

用户剩余配额:1 次
两个请求同时到达:
请求 A:检查配额 → 1 > 0 → 通过 ✅
请求 B:检查配额 → 1 > 0 → 通过 ✅
请求 A:扣减配额 → 0
请求 B:扣减配额 → -1 ❌(超额了!)

后来学到了原子操作:

// ✅ 正确的写法
await this.userModel.findByIdAndUpdate(
userId,
{ $inc: { quota: -1 } },
{ new: true }
);

我的理解

  • MongoDB 的 $inc 操作是原子的
  • 数据库层面保证不会超额
  • 高并发场景下必须使用

💭 我的感悟:这个问题让我意识到,后端开发和前端开发的思维方式真的不一样。前端很少考虑并发问题,但后端必须考虑。这也是为什么后端要写测试用例,模拟并发请求。


3.2 配额扣减的完整流程#

经过几次迭代,我总结出了这个流程:

1. 检查配额是否充足
2. 调用 AI 服务
3. AI 调用成功?
├─ 是 → 扣减配额 + 记录消费
└─ 否 → 不扣减,返回错误

我学到的最佳实践

  • ✅ 先检查后扣减(不要扣了才发现不够)
  • ✅ 只有成功才扣减(AI 调用失败不能扣钱)
  • ✅ 记录每次消费历史(方便用户查询,也方便排查问题)
  • ✅ 使用原子操作防止竞态(这个太重要了)

四、认证流程:串起来才理解 🔄#

4.1 完整的认证链路#

一开始学的时候,每个概念都是独立的。现在把它们串起来,才真正理解了整个流程:

1. 用户注册
POST /user/register
├── DTO 验证数据格式
├── 检查邮箱是否重复
├── bcrypt 加密密码
└── 保存到数据库
2. 用户登录
POST /user/login
├── 查找用户
├── bcrypt 比对密码
├── 生成 JWT Token
└── 返回 Token
3. 访问受保护接口
GET /user/info
Authorization: Bearer <token>
├── JwtAuthGuard 拦截
├── 提取 Token
├── JwtStrategy 验证
├── 提取用户信息到 req.user
└── 业务逻辑处理

4.2 各组件的职责#

这是我花了很长时间才理解的:

DTO → 定义数据结构,验证格式
ValidationPipe → 自动验证 DTO
Controller → 接收请求,调用 Service
Service → 业务逻辑,调用数据库
JwtService → 生成和验证 Token
JwtStrategy → Passport 策略,提取用户信息
JwtAuthGuard → 守卫,保护需要认证的接口
ResponseInterceptor → 统一响应格式

我的理解

  • 每一层只做自己的事(关注点分离)
  • Controller 不写业务逻辑(只做转发)
  • Service 不关心 HTTP(只关心业务)

💭 我的感悟:这种分层的思想,其实前端也有(比如 React 的容器组件和展示组件)。但后端的分层更严格,每一层的职责更明确。这样做的好处是,改一个地方不会影响其他地方。


五、最佳实践总结 🎯#

5.1 代码组织#

这是我参考优秀项目总结的结构:

user/
├── dto/ # 数据传输对象
│ ├── register.dto.ts
│ ├── login.dto.ts
│ └── update-user.dto.ts
├── user.schema.ts # 数据模型
├── user.service.ts # 业务逻辑
├── user.controller.ts # HTTP 接口
└── user.module.ts # 模块配置
auth/
├── jwt.strategy.ts # JWT 策略
├── jwt-auth.guard.ts # JWT 守卫
└── public.decorator.ts # 公开接口装饰器

5.2 命名规范#

这是我自己总结的,方便以后查找:

方法命名

  • get*:查询方法
  • create*:创建方法
  • update*:更新方法
  • check*:检查方法
  • *Quota:配额相关方法

参数命名

  • userId:用户 ID
  • dto:数据传输对象
  • req:请求对象

5.3 安全检查清单#

这是我每次写完代码都会检查的:

密码安全

  • 使用 bcrypt 加密
  • 不返回密码给客户端
  • 使用 comparePassword() 验证

Token 安全

  • 设置过期时间
  • 从 Authorization 头提取
  • 验证签名
  • 不在 Token 中放敏感信息

用户隐私

  • 从 req.user 获取当前用户
  • 不返回敏感字段
  • 检查操作权限

配额安全

  • 使用原子操作
  • 先检查后扣减
  • 记录消费历史
  • 只在成功时扣减

六、一些思考 💭#

Q1: 用户忘记密码怎么办?#

这个功能我还没实现,但已经有思路了:

  1. 用户输入邮箱
  2. 后端发送重置链接(带 Token)
  3. 用户点击链接,输入新密码
  4. 验证 Token,重置密码

关键是这个 Token 要设置短期过期(比如 15 分钟),而且只能用一次。


Q2: Token 被盗了怎么办?#

这个问题我想了很久,目前的防护方案:

  • 使用 HTTPS 加密传输(这个是必须的)
  • 设置短期 Token(7 天)
  • 可选:IP 绑定、设备绑定(但会影响用户体验)

说实话,如果 Token 真的被盗了,很难完全防止。只能尽量降低风险。


Q3: 如何实现登出?#

这个我纠结了很久:

简单方案:客户端删除 Token(我现在用的) 复杂方案:维护 Token 黑名单(但这样就失去了 JWT 无状态的优势)

最后我选择了简单方案,设置短期 Token,不需要登出机制。


七、我的收获 🎉#

经过这段时间的学习,我最大的收获是:

架构思维

  • ✅ 理解了用户系统的完整架构
  • ✅ 学会了从全局角度思考问题
  • ✅ 明白了各组件的职责划分

安全意识

  • ✅ 密码加密不是可选项,是必选项
  • ✅ Token 设计要考虑安全性
  • ✅ 并发问题必须重视(原子操作)

工程化思维

  • ✅ 代码组织和命名规范很重要
  • ✅ 关注点分离让代码更易维护
  • ✅ 安全检查清单帮我避免遗漏

结语 🚀#

用户认证系统是我学习后端的第一个完整项目。作为一个前端开发,这次经历让我对后端有了全新的认识。

我最大的感悟

  1. 安全第一:密码加密、Token 验证、权限检查,每一步都不能省
  2. 原子操作:并发问题比想象中复杂,必须在数据库层面保证
  3. 职责分离:每一层只做自己的事,这样代码才能长期维护

现在,我不仅会调用后端接口,还能理解接口背后的设计逻辑。这种感觉真好。

下一步:进入 AI 的世界,学习大语言模型和 Prompt 工程!

继续加油!💪

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

【NestJS】19 用户认证系统设计与实践总结
https://blog.fridolph.top/posts/2026-02-05__nest19-user/
作者
Fridolph
发布于
2026-02-05
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Fridolph
热爱 Coding、音乐和羽毛球的 90 后全栈工程师
公告
欢迎访问我的小站 ^_^ 我是昇哥,热爱Coding,喜爱音乐、羽毛球和摄影的 90后全栈工程师
分类
标签

文章目录