登录注册模块的 JWT 认证机制详解
深入浅出地讲解本项目中的双 Token 机制、AuthGuard 路由守卫,以及前端后端的完整认证闭环。
一、认证系统的全景架构
在开始深入细节之前,先用一张俯瞰图理解整个认证系统的数据流向:
bash
┌──────────────────────────────────────────────────────────────────┐
│ 用户浏览器 (Frontend) │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌─────────────────────────┐ │
│ │ Login 页面 │───▶│ Zustand Store │───▶│ localStorage │ │
│ │ │ │ (useUserStore)│ │ key: "user-store" │ │
│ └──────────┘ └──────┬───────┘ └─────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Axios 拦截器 │ │
│ │ 自动附加 Bearer │ │
│ │ Token 到请求头 │ │
│ └───────┬────────┘ │
│ │ │
│ ┌──────────────────────┼──────────────────────────┐ │
│ │ App.tsx │ BottomNav.tsx │ │
│ │ useEffect 路由守卫 │ 点击导航守卫 │ │
│ │ (检查 needsLoginPath)│ (拦截未登录点击) │ │
│ └──────────────────────┴──────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
│ HTTP Request
│ Authorization: Bearer <access_token>
▼
┌──────────────────────────────────────────────────────────────────────┐
│ NestJS 后端 (Backend) │
│ │
│ ┌────────────┐ ┌───────────────┐ ┌────────────────────┐ │
│ │ AuthController│───▶│ AuthService │───▶│ JwtService │ │
│ │ /api/auth/* │ │ login() │ │ signAsync() │ │
│ │ │ │ refreshToken()│ │ verifyAsync() │ │
│ └────────────┘ └───────────────┘ └────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ JwtAuthGuard (路由守卫) │ │
│ │ extends AuthGuard('jwt') │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ JwtStrategy (验证策略) │ │
│ │ - 从 Authorization Header 提取 Token │ │
│ │ - 验证签名 & 过期时间 │ │
│ │ - 将用户信息挂载到 request.user │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌───────────────┐ │
│ │ UsersModule│◀───│ PrismaService │──▶ PostgreSQL │
│ │ register() │ │ (数据库连接) │ │
│ └──────────┘ └───────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
二、注册:一切从"你是谁"开始
注册是用户身份体系的入口。用户在客户端填写用户名和密码,数据穿越网络到达后端,经过层层校验后,最终在数据库中落地------但密码永远不会以明文存储。
2.1 前端发起注册
前端收集用户输入的用户名和密码,以简单的 POST 请求发送到 /api/users/register。请求体结构非常简洁:
json
{
"name": "zhangsan",
"password": "my-secret-123"
}
注意:当前前端工程中尚未实现独立的注册页面,但后端接口已完全就绪,只需一个表单即可打通。
2.2 后端的层层防线
后端收到请求后,首先经过 NestJS 的 ValidationPipe 全局管道:
typescript
// main.ts
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 自动剔除 DTO 未定义的字段
forbidNonWhitelisted: true, // 发现未知字段直接拒绝
transform: true, // 自动转换类型(如字符串转数字)
}));
这个管道就像是门口的安检员,任何不符合 DTO 定义的数据都会被拦在门外。其中 CreateUserDto 定义了它的"准入标准":
typescript
export class CreateUserDto {
@IsNotEmpty()
@IsString()
name: string;
@IsNotEmpty()
@IsString()
@MinLength(6) // 密码至少 6 位
password: string;
}
通过管道校验后,UsersService.register() 执行真正的注册逻辑------这里有两道核心防线:
第一道:查重。 用户名必须全局唯一。通过 Prisma 在数据库中查询同名用户,若已存在则直接抛出 BadRequestException('用户名已存在')。
第二道:加密。 密码绝对不能以明文存入数据库------这是安全红线。使用 bcrypt.hash(password, 10) 进行哈希处理,其中 10 代表盐轮次(salt rounds)。10 轮意味着哈希函数会迭代 2^10 = 1024 次,即使是现代硬件也无法快速暴力破解。
最终存入数据库的是一串看起来毫无意义的密文:
perl
明文: my-secret-123
密文: $2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
│ │ │
│ └── 盐轮次(10) └── 最终的哈希值
└── bcrypt 算法标识
typescript
// users.service.ts --- register 核心逻辑
async register(createUserDto: CreateUserDto) {
// 查重
const existing = await this.prisma.user.findUnique({
where: { name: createUserDto.name }
});
if (existing) throw new BadRequestException('用户名已存在');
// 哈希
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
// 入库(只返回 id 和 name,密码哈希不见天日)
return this.prisma.user.create({
data: { name: createUserDto.name, password: hashedPassword },
select: { id: true, name: true }
});
}
关键细节 :select: { id: true, name: true } 确保了密码哈希永远不会出现在 API 响应中。这是一种最小暴露原则------即使某个地方的代码逻辑出错,查询层面就已经把密码字段剪掉了。
三、登录与双 Token 机制:认证系统的灵魂
如果注册是在户籍系统里建立身份档案,那么登录就是每次进入大楼时签发通行证。而本项目采用的"双 Token 机制",则是通行证体系中的一项精巧设计。
3.1 为什么需要两个 Token?
一个直观的类比:
| 类比物 | 对应 | 有效期 | 用途 |
|---|---|---|---|
| 门禁卡 | access_token | 15 分钟 | 每次进门刷卡,频繁使用 |
| 身份证 | refresh_token | 7 天 | 门禁卡过期后证明"我确实是我",换取新卡 |
设计双 Token 的核心原因是一个安全与体验的平衡:
- 如果只有 短期 Token(比如 15 分钟),用户每隔 15 分钟就要重新登录,体验极差。
- 如果只有 长期 Token(比如 7 天),一旦被窃取,攻击者拥有长达 7 天的操作窗口,风险极高。
双 Token 方案巧妙地将"高频使用"和"长期有效"拆成两张令牌:
- access_token (高频使用,短期有效):挂在每个请求的
Authorization头中。即使被中间人截获,15 分钟后自动失效,攻击窗口极小。 - refresh_token(低频使用,长期有效):只在 access_token 过期时才拿出来用一次,大幅减少在网络中传输的次数,降低了暴露风险。
3.2 登录流程:两张令牌的诞生
用户输入用户名和密码,点击登录。数据流经以下路径:
scss
前端 Login.tsx
│
│ POST /api/auth/login { name, password }
▼
后端 AuthController.login()
│
▼
AuthService.login()
│
├── 1. prisma.user.findUnique({ where: { name } }) ← 查数据库
│
├── 2. bcrypt.compare(password, user.password) ← 验证密码
│ │ 失败 → UnauthorizedException('用户名或密码错误')
│ │ 成功 ↓
│
└── 3. generateTokens(user.id, user.name) ← 签发双 Token
│
├── 生成 payload = { sub: user.id, name: user.name }
│
└── Promise.all([
jwtService.signAsync(payload, { expiresIn: '15m' }), ← access_token
jwtService.signAsync(payload, { expiresIn: '7d' }), ← refresh_token
])
两个 Token 同时签发,共享同一个 payload(用户 ID 和用户名),使用同一个 TOKEN_SECRET 签名,唯一区别就是过期时间不同。后端将其一并返回:
json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": { "id": 1, "name": "zhangsan" }
}
3.3 前端存储:Zustand + localStorage
前端拿到这两个 Token 后,将它们交给 Zustand 的 useUserStore 管理:
typescript
// useUserStore.ts --- login action
login: async (credentials: Credential) => {
const res = await doLogin(credentials); // 调用 POST /auth/login
set({
accessToken: res.access_token, // 存到内存
refreshToken: res.refresh_token, // 存到内存
user: res.user,
isLogin: true, // 设置登录状态
});
}
但 Store 在刷新页面后会丢失数据------这时 zustand/middleware 的 persist 中间件登场了:
typescript
const useUserStore = create<UserState>()(
persist(
(set) => ({ /* state & actions */ }),
{
name: 'user-store', // localStorage 中的 key
partialize: (state) => ({ // 只持久化这些字段
accessToken: state.accessToken,
refreshToken: state.refreshToken,
user: state.user,
isLogin: state.isLogin,
}),
}
)
);
数据流转全景:
scss
登录成功
│
├──▶ Zustand State (内存) ← 组件通过 useUserStore() 实时读取
│
└──▶ localStorage "user-store" ← 持久化,页面刷新后自动恢复
│
▼
下次页面加载时,persist 中间件自动 rehydrate
│
▼
内存和本地存储同步恢复,用户无需重复登录
3.4 每次请求:Token 的自动化装载
用户登录后,每个发往后端的请求都需要携带 access_token。如果让开发者每次手动添加 Token,不仅繁琐还容易遗漏。解决方案是 Axios 请求拦截器:
typescript
// api/config.ts --- 请求拦截器
axios.interceptors.request.use(config => {
const token = useUserStore.getState().accessToken; // 直接从 Store 读取(非响应式)
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
注意这里用的是 useUserStore.getState() 而非 useUserStore()。前者是 Zustand 提供的命令式读取方法,适用于拦截器这种不在 React 组件树中的场景------它直接读取 Store 的当前快照,不触发任何响应式更新。
四、AuthGuard 路由守卫:保护你的后端资源
前端可以随意修改,但后端不行。后端的路由守卫是整个安全体系的最后一道防线,它必须绝对可靠。
4.1 守卫的注册与使用
在 NestJS 中,守卫通过 @UseGuards() 装饰器绑定到控制器方法上。本项目中的 JwtAuthGuard 可以保护任何需要认证的接口:
typescript
@Controller('posts')
export class PostsController {
@Get('mine')
@UseGuards(JwtAuthGuard) // ← 只有携带有效 Token 的请求能进来
async getMyPosts(@Req() req) {
return this.postsService.findByUser(req.user.id);
}
}
4.2 守卫的执行链路
当一个携带 Authorization: Bearer <token> 头的请求到达受保护的接口时,执行链路如下:
css
HTTP Request
│
│ Authorization: Bearer eyJhbGciOi...
▼
┌──────────────────────────────────────────────────────┐
│ JwtAuthGuard │
│ extends AuthGuard('jwt') │
│ │
│ "这是一个 jwt 策略的守卫" │
│ 将请求委托给 → JwtStrategy │
└──────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ JwtStrategy (extends PassportStrategy) │
│ │
│ ① ExtractJwt.fromAuthHeaderAsBearerToken() │
│ 从请求头中提取 "Bearer " 后面的 token 字符串 │
│ │
│ ② 使用 TOKEN_SECRET 验证签名 │
│ - 签名不对?→ 401 Unauthorized │
│ - 签名正确 ↓ │
│ │
│ ③ ignoreExpiration: false (默认) │
│ 检查 Token 是否过期 │
│ - 过期了?→ 401 Unauthorized │
│ - 未过期 ↓ │
│ │
│ ④ validate(payload) 被调用 │
│ return { id: payload.sub, name: payload.name } │
│ ↓ │
│ 返回值自动挂载到 request.user │
└──────────────────┬───────────────────────────────────┘
│
▼
Controller 方法执行
req.user = { id: 1, name: 'zhangsan' }
开发者可以直接用 req.user.id 获取当前用户
4.3 JwtStrategy 源码解析
typescript
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
// 告诉 Passport 从哪里找 Token
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
// false = 严格模式,Token 过期直接拒绝
ignoreExpiration: false,
// 验签密钥
secretOrKey: process.env.TOKEN_SECRET || ""
});
}
async validate(payload: any) {
// 返回值会被注入到 request.user
// 这里做了一层"翻译":把 JWT 内部字段名转为业务字段名
return {
id: payload.sub, // JWT 规范中 subject 字段 → 业务中的 id
name: payload.name,
};
}
}
值得品味的设计细节:
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken():这是passport-jwt提供的内置方法,它会自动解析Authorization: Bearer <token>格式的请求头。不需要自己写正则提取逻辑。ignoreExpiration: false:这是 Passport JWT 的默认行为,显式写出来是为了强调"过期 Token 绝不放过"的安全态度。validate()中的字段映射 :JWT 标准使用sub(subject)字段存储用户标识,但业务代码中使用id更自然。validate()做了这一层语义转换,让后续代码可以写req.user.id而非req.user.sub。
4.4 一个具体的受保护请求示例
当用户在 "我的" 页面查看自己的帖子时:
typescript
// 请求:GET /api/posts/mine
// 请求头:Authorization: Bearer eyJ...access_token...
// guard 通过后,Controller 直接拿到已认证的用户信息
@Get('mine')
@UseGuards(JwtAuthGuard)
async getMyPosts(@Req() req) {
console.log(req.user); // { id: 1, name: 'zhangsan' }
// 无需从请求参数中手动获取用户 ID
// 无需担心用户伪造身份
return this.postsService.findByUser(req.user.id);
}
这就是 AuthGuard 最大的价值:它将"身份验证"和"业务逻辑"彻底解耦 。Controller 不需要知道 Token 是如何验证的,它只需要信任 req.user 已经是真实有效的用户信息。
五、Token 刷新:不打扰用户的续期机制
想象一个场景:你正在填写一个长表单,花了 20 分钟。当你点击提交时,access_token 已经过期(15 分钟),你被无情地踢回登录页,所有填好的数据都丢了。这正是 Token 刷新机制要解决的问题。
5.1 刷新流程
后端的刷新逻辑非常简洁:
typescript
// auth.service.ts
async refreshToken(rt: string) {
try {
// 验证 refresh_token 是否有效
const payload = await this.jwtService.verifyAsync(rt, {
secret: process.env.TOKEN_SECRET,
});
// 有效 → 签发全新的 Token 对
return this.generateTokens(payload.sub, payload.name);
} catch (e) {
// 无效或过期 → 用户需要重新登录
throw new UnauthorizedException('Refresh Token 已失效,请重新登录');
}
}
调用方式:前端在感知到 access_token 过期后(通常是通过 401 响应),取出存储的 refresh_token,调用 POST /api/auth/refresh,然后无缝替换新的 access_token。
bash
用户视角:
"一切正常,我甚至不知道 Token 刷新了。"
后台实际发生的事情:
access_token 过期
→ 前端拦截器检测 401
→ 自动调用 /api/auth/refresh
→ 获得新的 access_token 和 refresh_token
→ 用新 access_token 重试原来的请求
→ 用户毫无感知
注意:当前前端代码中尚未实现这一自动刷新逻辑------Axios 的响应拦截器在收到非 2xx 状态码时只是简单抛出错误。这是后续迭代的重要方向。
5.2 Refresh Token 的滚动更新
注意 refreshToken() 在验证通过后调用的是 generateTokens()------这意味着每次刷新都会同时获得新的 access_token 和新的 refresh_token。这不只是换了一张新门禁卡,连身份证也换了。这个策略的优势在于:
- 缩短攻击窗口:即使某个 refresh_token 被窃取,一旦合法用户进行了刷新操作,旧的 refresh_token 就变成了废纸。
- 无限续期:只要用户在 7 天内使用过一次应用,登录状态就能持续保持。只有连续 7 天不访问,才需要重新输入密码。
六、前端的路由守卫:一道优雅的门禁
后端有 AuthGuard 保护接口,前端也需要对应的守卫保护页面。想象一个未登录用户直接在浏览器地址栏输入 /mine------如果没有前端守卫,他会看到一堆报错的空白态,体验很差。
6.1 核心守卫逻辑
本项目实现了两个层面的前端守卫:
应用级守卫(App.tsx)------监听 URL 变化:
typescript
const needsLoginPath = ['/mine', '/order', '/chat'];
function App() {
const { isLogin } = useUserStore();
const navigate = useNavigate();
const { pathname } = useLocation();
useEffect(() => {
if (!isLogin && needsLoginPath.includes(pathname)) {
navigate('/login'); // 未登录 → 跳转登录页
}
}, [isLogin, navigate, pathname]);
// ... 渲染路由
}
这个 useEffect 像是一个站岗的哨兵 ------每当 URL 路径 (pathname) 或登录状态 (isLogin) 发生变化时,它都会重新检查一次:当前路径是否需要登录?用户是否已登录?如果答案是"需要但未登录",立刻重定向到登录页。
导航级守卫(BottomNav.tsx)------拦截点击行为:
typescript
const handleNav = (path: string) => {
if (path === pathname) return; // 不重复导航到当前页
if (needsLoginPath.includes(path) && !isLogin) {
navigate('/login'); // 拦截未登录点击
return;
}
navigate(path);
};
这层守卫确保用户即便从底部导航栏点击受保护的 tab,也会在进入页面前被拦下。它与应用级守卫形成了双重保障。
6.2 为什么前端路由守卫不能替代后端守卫?
一个常见的误解是:"前端已经做了路由守卫,后端是不是就可以放松了?"
答案是否定的。 因为前端代码运行在用户的浏览器中,用户可以:
- 打开浏览器 DevTools,修改 localStorage 中的
isLogin为true - 直接使用
curl或 Postman 发送请求,绕过浏览器 - 修改前端源代码,注释掉守卫逻辑
因此后端的 JwtAuthGuard 才是真正的安全防线 。前端路由守卫的目的是提升体验------在用户看到报错之前就引导到正确的页面------而非保证安全。
七、完整的认证时序图
下面用一张完整的时序图将注册、登录、Token 刷新和路由守卫串联起来:
bash
用户浏览器 前端 App Zustand Store 后端 NestJS PostgreSQL
│ │ │ │ │
│ │ │ │ │
│ ═══ 注册 ═══ │ │ │ │
│ │ │ │ │
│ 填写注册表单 │ │ │ │
│ ───────────────────────────▶│ │ │ │
│ │ POST /api/users/register │ │
│ │ ───────────────────────────────────────────▶│ │
│ │ │ │ bcrypt.hash(pw,10) │
│ │ │ │ ────────────────────▶│
│ │ │ │ 存入 users 表 │
│ │ │ │ ◀────────────────────│
│ │ 返回 { id, name } │ │ │
│ │ ◀───────────────────────────────────────────│ │
│ │ │ │ │
│ ═══ 登录 ═══ │ │ │ │
│ │ │ │ │
│ 填写用户名密码 │ │ │ │
│ ───────────────────────────▶│ │ │ │
│ │ POST /api/auth/login │ │ bcrypt.compare() │
│ │ ───────────────────────────────────────────▶│ ────────────────────▶│
│ │ │ │ │
│ │ access_token (15min) + refresh_token (7d) │ │
│ │ ◀───────────────────────────────────────────│ │
│ │ │ │ │
│ │ login() 更新状态 │ │ │
│ │ ─────────────────────▶│ │ │
│ │ │ persist 到 localStorage │
│ │ 导航到首页 │ │ │
│ ◀───────────────────────────│ │ │ │
│ │ │ │ │
│ ═══ 访问受保护页面 ═══ │ │ │ │
│ │ │ │ │
│ 点击 "我的" tab │ │ │ │
│ ───────────────────────────▶│ │ │ │
│ │ BottomNav 守卫检查 │ │ │
│ │ isLogin === true ✓ │ │ │
│ │ 放行 │ │ │
│ │ │ │ │
│ 页面发起数据请求 │ │ │ │
│ ───────────────────────────▶│ │ │ │
│ │ Axios 拦截器 │ │ │
│ │ 自动附加 Bearer Token │ │ │
│ │ │ │ │
│ │ GET /api/posts/mine │ │ │
│ │ Authorization: Bearer <access_token> │ │
│ │ ───────────────────────────────────────────▶│ │
│ │ │ JwtAuthGuard │ │
│ │ │ → JwtStrategy │ │
│ │ │ 验证签名 ✓ │ │
│ │ │ 未过期 ✓ │ │
│ │ │ validate() │ │
│ │ │ → req.user │ │
│ │ │ │ Controller 执行业务 │
│ │ │ │ ◀────────────────────│
│ │ 返回用户数据 │ │ │
│ │ ◀───────────────────────────────────────────│ │
│ 看到自己的内容 │ │ │ │
│ ◀───────────────────────────│ │ │ │
│ │ │ │ │
│ ═══ Token 刷新 ═══ │ │ │ │
│ (access_token 已过期 15min+)│ │ │ │
│ │ │ │ │
│ │ 请求返回 401 │ │ │
│ │ ◀───────────────────────────────────────────│ │
│ │ │ │ │
│ │ POST /api/auth/refresh │ │
│ │ { refresh_token } │ │
│ │ ───────────────────────────────────────────▶│ │
│ │ │ verify refresh │ │
│ │ 新的 access_token │ 签发新双 Token │ │
│ │ + refresh_token │ │ │
│ │ ◀───────────────────────────────────────────│ │
│ │ │ │ │
│ │ 更新 Store │ │ │
│ │ ─────────────────────▶│ │ │
│ │ │ persist 新 Token │ │
│ │ │ │ │
│ │ 重试原来的请求 ✓ │ │ │
│ │ │ │ │
八、安全设计要点
8.1 密码安全
- bcrypt 哈希(盐轮次 = 10):即使数据库被拖库,攻击者也难以从哈希值逆向出明文密码。
- 最小暴露 :数据库查询层面使用
select限定返回字段,密码哈希永远不会随 API 响应泄露。 - 管道白名单 :
whitelist: true+forbidNonWhitelisted: true,防止客户端注入未预期的字段。
8.2 Token 安全
| 机制 | 说明 |
|---|---|
| 短期 access_token (15min) | 即使被窃取,攻击窗口仅 15 分钟 |
| 长期 refresh_token (7d) | 低频传输,降低暴露概率 |
| HS256 签名 | 服务端保管密钥,任何人无法伪造 Token |
| 滚动刷新 | 每次刷新同时更新两个 Token,旧 Token 立即失效 |
| 过期检查 | ignoreExpiration: false,过期 Token 直接拒绝 |
8.3 当前系统的改进空间
作为一个正在演进中的项目,当前的认证系统还有以下可以优化的方向:
- 前端 Token 刷新拦截器:目前响应拦截器在收到 401 时没有自动重试,这是待实现的关键特性。
- 登出功能 :Zustand Store 中缺少
logoutaction,Token 无法主动失效。 - refresh_token 持久化校验:可以考虑在数据库中存储 refresh_token 的哈希,实现服务端的主动撤销能力。
- CORS 细粒度配置 :当前
cors: true允许所有来源,生产环境应限制为具体域名。 - 速率限制 :登录和注册接口应添加限流(如
@nestjs/throttler),防止暴力破解。 - 注册页面前端实现:后端注册接口已就绪,前端注册页面尚未实现。
九、技术栈一览
| 层级 | 技术 | 角色 |
|---|---|---|
| 前端状态管理 | Zustand + persist | 管理 Token 和用户信息,持久化到 localStorage |
| 前端 HTTP | Axios + 拦截器 | 自动附加 Bearer Token,统一错误处理 |
| 前端路由守卫 | React Router v6 + useEffect | 检测登录状态,拦截未授权页面访问 |
| 后端框架 | NestJS | 模块化架构,依赖注入 |
| 身份验证 | Passport + passport-jwt | JWT 策略,Token 提取和验证 |
| 加密 | bcrypt (10 轮) | 密码哈希存储 |
| JWT | @nestjs/jwt (jsonwebtoken) | Token 签发和验证,HS256 签名 |
| 数据校验 | class-validator + class-transformer | DTO 验证,类型转换 |
| 数据库 | Prisma ORM + PostgreSQL | 用户数据持久化 |
十、总结
本项目的登录注册模块实现了一个结构清晰、安全可用的 JWT 认证体系。它遵循了以下核心设计原则:
- 短 Token 高高频,长 Token 低低频:access_token 只有 15 分钟的生命周期,最大限度地缩小了 Token 失窃的风险窗口;refresh_token 用 7 天的有效期换来了流畅的用户体验。
- 守卫分层,各司其职 :后端的
JwtAuthGuard提供不可绕过安全屏障,前端的路由守卫提供无缝的用户体验,两者互补而非替代。 - Token 携带自动化:Axios 拦截器让开发者几乎忘记 Token 的存在------它被静默地注入每个请求,就像刷卡门禁在你靠近时自动感应。
- 密码不留痕:bcrypt 哈希 + 查询层裁剪,确保密码的明文和密文都不会出现在任何 API 响应中。
一个成熟的认证系统,不在于它有多少花哨的特性,而在于每一个设计决策背后都有清晰的"为什么"。15 分钟和 7 天的选择不是随意的数字,而是经过对安全性、用户体验和工程复杂度的综合权衡。理解这些"为什么",比记住具体的代码实现更为重要。