大家好,我是大布布将军。
在你的 BFF 服务中,你不可能让所有人都能调用 /orders/list 接口。我们需要一个机制来验证用户的身份和权限,这个机制在现代 Web 应用中,几乎都基于 JWT (JSON Web Token) 。
本篇,我们将深入 JWT 的原理,并学习如何在 Node.js 中实现一个安全的注册和登录流程。
1. 什么是 JWT?------无状态的身份证明
JWT 是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在双方之间安全地传输信息。
核心关键词:无状态 (Stateless)。
传统的 Session/Cookie 模式需要在服务器端存储用户的会话信息(有状态)。而 JWT 的所有信息都编码在 Token 中,服务器无需存储 Session,极大提高了 Node.js 服务的可扩展性 和集群化能力。
一个 JWT 通常由三部分组成,用点号(.)分隔:
Header.Payload.Signature\text{Header} . \text{Payload} . \text{Signature}Header.Payload.Signature
| 部分 | 内容 | 作用 |
|---|---|---|
| Header (头部) | 包含 Token 类型(JWT)和所使用的加密算法(如 HMAC SHA256)。 | 告诉接收方如何解析和验证 Token。 |
| Payload (载荷) | 存放实际的声明信息(Claims),如用户 ID (userId)、角色 (role)、Token 过期时间 (exp)。 |
存放用户的身份信息。 |
| Signature (签名) | 使用 Header 和 Payload 结合一个只有服务器知道的密钥 (Secret Key) 进行加密计算得出的字符串。 | 防止数据被篡改。 如果有人修改了 Payload,签名就会失效。 |
2. 实践项目:注册、登录与 Token 校验
我们使用 Node.js 中常用的 jsonwebtoken 库来实现这套流程。
步骤一:用户注册与密码安全
在用户注册时,我们绝对不能 明文存储密码。我们需要使用 哈希算法 (如 bcrypt)对密码进行单向加密。
TypeScript
// 📁 src/auth/auth.service.ts
import * as bcrypt from 'bcrypt';
import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service'; // 假设我们有用户服务操作数据库
@Injectable()
export class AuthService {
// 密码加密存储
async register(username: string, password_plain: string): Promise<User> {
// 1. 生成盐值 (Salt)
const salt = await bcrypt.genSalt();
// 2. 将密码和盐值进行哈希
const hashedPassword = await bcrypt.hash(password_plain, salt);
// 3. 将用户名和哈希后的密码存储到数据库
return this.userService.createUser(username, hashedPassword);
}
}
步骤二:用户登录与 JWT 生成
用户登录成功后,我们需要为他生成一个 JWT Token,并返回给前端。
TypeScript
// 📁 src/auth/auth.service.ts (续)
import * as jwt from 'jsonwebtoken';
// 假设你的密钥存储在环境变量中
const JWT_SECRET = process.env.JWT_SECRET || 'YOUR_UNSAFE_SECRET_KEY';
const JWT_EXPIRES_IN = '1d'; // Token 1天后过期
async login(username: string, password_plain: string): Promise<string> {
const user = await this.userService.findUserByUsername(username);
if (!user) throw new Error('用户不存在');
// 1. 验证密码:将用户输入的明文密码与数据库中的哈希值进行比对
const isMatch = await bcrypt.compare(password_plain, user.hashedPassword);
if (!isMatch) throw new Error('密码错误');
// 2. 构造 Payload (载荷)
const payload = {
userId: user.id,
role: user.role
};
// 3. 生成 JWT Token 并签名
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
// 4. 返回 Token(前端会将其存储在 LocalStorage 或 Cookie 中)
return token;
}
步骤三:身份验证中间件/守卫(Token 校验)
在 BFF 层,我们需要在每一个受保护的 API 接口前,校验前端请求头中携带的 Token 是否有效。
TypeScript
// 📁 src/middleware/auth.middleware.ts (NestJS Guard 或 Express Middleware)
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
// ... 其他依赖
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('请求未携带 Token 或格式错误');
}
const token = authHeader.substring(7); // 提取 Token(去掉 "Bearer ")
try {
// 1. 验证签名和过期时间
const payload = jwt.verify(token, JWT_SECRET) as { userId: number, role: string };
// 2. 将用户信息附加到请求对象上,供 Controller/Service 使用
request.user = payload;
return true; // 验证通过,允许访问 Controller
} catch (error) {
// 签名错误或 Token 过期
throw new UnauthorizedException('Token 无效或已过期');
}
}
}
3. 安全性考量:JWT 的缺点
虽然 JWT 很强大,但它并非完美:
- 无法主动失效: 一旦 Token 被签发,除非过期,否则服务器无法强制使其失效(除非引入 Redis 黑名单机制)。
- Payload 不加密: Payload 只是被 Base64 编码,任何人都可以解析 。因此,Payload 中绝不能存储敏感信息,如密码或密钥。
总结
掌握 JWT 是成为一名合格全栈工程师的必备技能。它让你能以可扩展、无状态 的方式管理用户身份。通过 bcrypt 保证密码安全,通过 jsonwebtoken 签发和校验 Token,你的 BFF 服务就有了坚固的安全门。

下一篇,我们将进入工程化领域,学习 Docker 容器化。这是将我们写好的 Node.js 服务部署到生产环境的通行证。