在现代后端开发中,用户认证是核心功能之一。NestJS 作为一款高效的 Node.js 框架,提供了内置的 JWT(JSON Web Token)模块,使得实现 token-based 认证变得简单可靠。相比传统的单 token 机制,双 token 机制(access_token 和 refresh_token)能更好地应对安全挑战,如 token 被截获的风险。本文将基于一个典型的 Auth 模块场景,详细讲解 JWT 双token 的原理、实现步骤,以及在 NestJS 中的代码实践。我们会使用 Prisma 作为 ORM 处理用户数据,结合 bcrypt 进行密码哈希验证。同时,前端部分使用 Zustand 状态管理库存储 token。
JWT 认证基础
JWT 是一种开放标准(RFC 7519),用于在各方之间安全传输声明。它由三部分组成:Header(头部)、Payload(负载)和 Signature(签名)。在 NestJS 中,我们通过 @nestjs/jwt 模块集成 JWT 服务。这个模块需要单独安装(npm install @nestjs/jwt),但它是 NestJS 生态的一部分,提供 sign 和 verify 方法。
单 token 机制的痛点:一个 token 包含用户身份信息,如果被中间人攻击(如 MITM),攻击者可无限使用直到过期。笔记中提到 mockjs 使用 jsonwebtoken 的单 token sign/decode,这在简单原型中可行,但生产环境不安全。为此,双 token 机制应运而生:
- access_token:短效(通常分钟级,如 15m),用于 API 请求。过期快,降低泄露风险。
- refresh_token:长效(天级,如 7d),用于刷新 access_token。不直接用于 API,而是发送到后端换取新 token 对。
- 流程:用户登录获 token 对;API 请求用 access_token;过期时,用 refresh_token 刷新获新对;refresh_token 过期需重登录。
这种机制增强安全性:即使 access_token 泄露,影响有限;refresh_token 只用于特定端点,且可服务器端黑名单管理。生成 token 时,使用 JwtService.signAsync 方法,传入 payload(如用户 ID 和 name)。
在性能上,双 token 生成可用 Promise.all 并发执行,减少延迟。如在 posts 查询中,并发 count 和 list;类似地,token 生成也耗时,Promise.all 优化明显。
Auth 模块配置
Auth 模块是认证的核心。我们在 AuthModule 中导入 JwtModule,注册 secret(从环境变量获取)。示例代码:
TypeScript
import {
Module,
} from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
console.log(process.env.TOKEN_SECRET, "-=-=-=-=-=-=-=");
// 设计模式 面向对象企业级别开发 经验总结
// 23种 工厂模式 单例模式 装饰器模式(类添加属性和方法)
// 观察者模式(IntersectionObserver) 代理模式(Proxy)
// 订阅发布者模式(addEventListener)
@Module({
imports: [JwtModule.register({
secret: process.env.TOKEN_SECRET
})],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {
}
这里,JwtModule.register 配置 secret,用于签名和验证。笔记中注释提到设计模式,如装饰器模式(NestJS 大量使用 @Module 等装饰器添加元数据)、代理模式(Proxy 在 JWT 验证中类似)。console.log(process.env.TOKEN_SECRET) 用于调试,但这是一个安全隐患:在生产环境中,日志可能泄露 secret。建议移除或用环境检查工具替换。
模块导入后,JwtService 可在服务中注入。注意,secret 应从 .env 文件加载,避免硬编码。
Auth 控制器:处理登录和刷新
控制器定义 RESTful 端点:POST /auth/login 和 /auth/refresh。使用 DTO 验证输入,指定 HTTP 状态码。代码如下:
TypeScript
import {
Controller,
Post,
Body,
HttpCode,
HttpStatus,
} from '@nestjs/common'
import { LoginDto } from './dto/login.dto'
import { AuthService } from './auth.service'
// restful
// method + URL(名词 有可读性且直指资源)
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('login')
@HttpCode(HttpStatus.OK) // 指定状态码为200
async login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto);
}
@Post('refresh')
@HttpCode(HttpStatus.OK)
async refresh(@Body('refresh_token') refresh_token: string) {
return this.authService.refreshToken(refresh_token);
}
}
注释强调 RESTful 原则:URL 用名词(如 'auth'),方法表示动作。@HttpCode(HttpStatus.OK) 覆盖默认 201,确保响应 200。refresh 端点只取 refresh_token,无需完整 DTO。
Auth 服务:核心逻辑实现
服务处理业务:用户验证、token 生成和刷新。注入 PrismaService 和 JwtService ,使用 bcrypt 比对密码。完整代码:
TypeScript
import {
Injectable,
UnauthorizedException
} from '@nestjs/common';
import {
PrismaService
} from '../prisma/prisma.service';
import { LoginDto } from './dto/login.dto';
import * as bcrypt from 'bcrypt';
// nestjs 内置了jwt 模块
// 需要安装的 性能比较好
// @nestjs 插件式 企业级同时保持小巧
// 注入的方式注入Auth模块
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService
) {}
async login(loginDto: LoginDto) {
const { name, password } = loginDto;
// name 查询
const user = await this.prisma.user.findUnique({
where: {
name
}
})
if(!user || !(await bcrypt.compare(password, user.password))) {
throw new UnauthorizedException('用户名或密码错误')
}
console.log(user);
// hased password 比对?
// 颁发token
// 模块化分离, 业务专注
const tokens = await this.generateTokens(user.id.toString(), user.name);
return {
...tokens,
user:{
id: user.id.toString(),
name: user.name
}
}
}
async refreshToken(rt: string) {
try {
const payload = await this.jwtService.verifyAsync(rt, {
secret:process.env.TOKEN_SECRET
});
// console.log(payload, "--------()()()")
return this.generateTokens(payload.sub, payload.name);
} catch(err) {
throw new UnauthorizedException("Refresh Token 已失效,请重新登录");
}
}
// OOP private 方法 复杂度剥离
private async generateTokens(id: string, name: string) {
// 用户信息关键 JSON Object
// 马上用于签发token, 发令枪先装填弹药(payload),生成token,先要准备用户对象一样
const payload = {
sub: id, // subject 主题 JWT 中规定的 关键字端
name
};
const [at, rt] = await Promise.all([
// 颁发了两个token access_token
this.jwtService.signAsync(payload, {
expiresIn: '15m', // 有效期 15分钟 更安全 被中间人攻击
secret: process.env.TOKEN_SECRET
}),
// refresh_token 刷新
// 7d 服务器接受我们,用于refresh
// 服务器再次生成两个token 给我们
// 依然使用 15m token 请求
this.jwtService.signAsync(payload, {
expiresIn: '7d',
secret: process.env.TOKEN_SECRET
}),
])
return {
access_token: at,
refresh_token: rt
}
}
}
剖析:
- login:查询用户,bcrypt.compare 验证密码(需预存哈希密码)。生成 token 对,返回带用户info。
- refreshToken:verifyAsync 检查 refresh_token,提取 payload,生成新 token 对。
- generateTokens:私有方法,使用 Promise.all 并发 signAsync。payload 用 sub (ID) 和 name。expiresIn 设置过期。
前端 token 管理:Zustand 与持久化
前端使用 Zustand 存储 token,用 persist 中间件持久到 localStorage。API 调用如 doLogin 处理登录。代码:
TypeScript
// localstorage
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import {
doLogin
} from '@/api/user';
import type { User } from '@/types/index';
import type { Credential } from '@/types/index'
interface UserStore {
accessToken: string | null;
refreshToken: string | null;
user: User | null;
isLogin: boolean;
login: (credentials: { name: string, password: string }) => Promise<void>;
}
// 高阶函数 柯里化
export const useUserStore = create<UserStore>() (
persist((set) => ({ // state 对象
accessToken: null,
refreshToken: null,
user: null,
isLogin: false,
login: async ({name, password}) => {
const res = await doLogin({name, password});
console.log(res, '??????');
const { access_token, refresh_token, user } = res;
// console.log(access_token, refresh_token, user);
set({
user: user,
accessToken: access_token,
refreshToken: refresh_token,
isLogin: true,
})
}
}), {
name: 'user-store',
partialize: (state) => ({
accessToken: state.accessToken,
refreshToken: state.refreshToken,
user: state.user,
isLogin: state.isLogin,
})
})
)
create 高阶函数(柯里化)包装 persist。login 调用 doLogin(假设 axios POST /auth/login),更新 state。partialize 选择持久字段。
双 Token 机制的完整流程
- 用户登录:前端发 name/password,后端验证,生成 token 对返回。
- API 请求:axios 拦截加 Authorization: Bearer access_token。
- 过期:后端返回 401,前端用 refresh_token POST /auth/refresh,获新对,更新 store,重试请求。
- refresh 过期:重定向登录。
优势:安全(短效 token)、无缝(自动刷新)。笔记中 Promise.all 面试例:posts count 和 list 并发;token 生成类似,减少延迟。
总结
通过 NestJS 的 JWT 双 token 机制,我们构建了安全的 Auth 系统。从模块配置到服务实现,再到前端存储,每步都注重模块化和性能。