NestJS 中实现完整的 JWT 认证系统
2025-08-16 2102字
在现代 Web 开发中,JWT(JSON Web Token)认证因其简单、无状态和可扩展的特性,已成为构建安全 API 的首选方案。特别是在分布式系统和微服务架构中,JWT 的优势更加明显。本文将带你一步步在 NestJS 框架中实现一个完整的 JWT 认证系统。
环境准备与依赖安装
首先,我们需要安装必要的依赖包。NestJS 提供了官方的 JWT 模块 @nestjs/jwt,它封装了常用的 JWT 操作,让我们的开发更加便捷。
npm install @nestjs/jwt
提示:在实际项目中,通常会配合
@nestjs/passport来实现更复杂的认证策略。但为了让大家更好地理解 JWT 认证的原理,本文将采用手动实现的方式。
配置 JWT 模块
安装完成后,我们需要在应用中注册 JWT 服务。NestJS 的 JwtModule 提供了两种注册方式:同步注册和异步注册。
同步注册(适用于开发环境)
在开发阶段,我们可以使用同步注册方式快速配置:
// app.module.ts
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
JwtModule.register({
secret: 'your-secret-key',
signOptions: { expiresIn: '60s' },
global: true, // 设置为全局模块
}),
],
})
export class AppModule {}
这种方式简单直接,但存在安全隐患——密钥硬编码在代码中。
异步注册(推荐用于生产环境)
在生产环境中,我们应该使用异步注册方式,从环境变量中读取配置:
// app.module.ts
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET', 'jwt_secret_key'),
signOptions: {
expiresIn: configService.get('JWT_EXPIRES_IN', '1d')
},
}),
global: true,
}),
],
})
export class AppModule {}
通过 ConfigService 从环境变量中读取配置,不仅提高了安全性,还让配置管理更加灵活。
生成 JWT Token
配置好 JWT 模块后,我们就可以在业务逻辑中生成 Token 了。通常在用户登录成功后,我们需要生成一个包含用户信息的 JWT Token。
// user.service.ts
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class UserService {
constructor(private readonly jwtService: JwtService) {}
async login(user: User) {
// 构建 Token 负载(payload)
const payload = {
userId: user.id,
username: user.username,
email: user.email
};
// 生成 JWT Token
const token = this.jwtService.sign(payload);
return {
token,
user: {
id: user.id,
username: user.username,
email: user.email
}
};
}
}
关键点说明:
payload中可以包含任何需要的信息,但要避免存储敏感数据sign方法会自动处理签名和过期时间- 建议同时返回用户基本信息,避免前端需要额外请求
Token 认证流程与前端集成
先看下完整的 JWT 认证流程:
- 用户登录:前端发送用户名密码到登录接口
- 生成 Token:服务器验证成功后生成 JWT Token
- 返回 Token:服务器将 Token 返回给前端
- 存储 Token:前端将 Token 存储在 localStorage 或 cookie 中
- 携带 Token:后续请求在 Authorization 头中携带 Token
- 验证 Token:服务器验证 Token 有效性
前端如何携带 Token?
在前端应用中,我们需要在每次请求的 Authorization 头中携带 Token:
import axios from 'axios';
// 从存储中获取 Token
const token = localStorage.getItem('jwt_token');
// 发送请求时携带 Token
axios.get('/api/protected-endpoint', {
headers: {
Authorization: `Bearer ${token}`,
},
});
注意事项:
Bearer是认证方案标识,后面必须跟一个空格- Token 应该安全存储,避免 XSS 攻击
- 建议设置请求拦截器自动添加 Token
实现 Token 校验与路由守卫
现在我们来解决最关键的问题:如何在服务器端验证 Token 的有效性。如果为每个需要认证的接口都编写校验代码,显然是不现实的。NestJS 提供了优雅的解决方案——路由守卫(Guard)。
什么是路由守卫?
路由守卫是 NestJS AOP(面向切面编程)思想的重要体现。它可以在请求到达控制器之前进行拦截,执行一些预处理逻辑,比如权限验证、Token 校验等。
创建认证守卫
让我们创建一个 AuthGuard 来实现 JWT Token 的校验:
// auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import type { Request } from 'express';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractTokenFromHeader(request);
// 检查 Token 是否存在
if (!token) {
throw new UnauthorizedException('Token 不存在');
}
try {
// 验证 Token 有效性
const payload = await this.jwtService.verifyAsync(token);
// 将用户信息挂载到请求对象上,供后续使用
request['user'] = payload;
} catch (error) {
throw new UnauthorizedException('Token 无效或已过期');
}
return true;
}
/**
* 从请求头中提取 Token
*/
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
守卫的核心功能:
- 提取 Token:从
Authorization头中提取 Bearer Token - 验证有效性:使用
JwtService.verifyAsync()验证 Token - 挂载用户信息:验证成功后,将用户信息挂载到请求对象
- 异常处理:Token 无效或过期时抛出相应异常
使用认证守卫
守卫的使用非常简单,只需要在需要保护的接口或控制器上添加 @UseGuards() 装饰器:
// user.controller.ts
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
// 保护单个接口
@UseGuards(AuthGuard)
@Get('profile')
async getProfile(@Req() request: Request) {
// 可以直接从请求对象中获取用户信息
const user = request['user'];
return await this.userService.getProfile(user.userId);
}
}
控制器级别的守卫
如果整个控制器的接口都需要保护,可以在控制器级别应用守卫:
// user.controller.ts
@Controller('user')
@UseGuards(AuthGuard) // 应用到整个控制器
export class UserController {
constructor(private readonly userService: UserService) {}
@Get('profile')
async getProfile(@Req() request: Request) {
const user = request['user'];
return await this.userService.getProfile(user.userId);
}
@Get('settings')
async getSettings(@Req() request: Request) {
const user = request['user'];
return await this.userService.getSettings(user.userId);
}
}
控制器级别的守卫虽然方便,但会保护所有接口,包括注册、登录等公开接口。为了解决这个问题,我们需要实现一个机制来标记公开接口。
实现公开接口标记机制
为了解决控制器级别守卫的局限性,我们需要实现一个机制来标记公开接口。NestJS 提供了元数据(Metadata)机制,可以让我们在接口上添加自定义标记。
创建公开接口装饰器
首先创建一个自定义装饰器来标记公开接口:
// public.decorator.ts
import { SetMetadata } from '@nestjs/common';
// 定义元数据键名
export const IS_PUBLIC_KEY = 'isPublic';
// 创建公开接口装饰器
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
这个装饰器的作用是在方法或类上设置一个元数据标记,表示该接口不需要 Token 认证。
使用公开接口装饰器
在控制器中使用 @Public() 装饰器标记公开接口:
// user.controller.ts
@Controller('user')
@UseGuards(AuthGuard) // 整个控制器默认需要认证
export class UserController {
constructor(private readonly userService: UserService) {}
@Public() // 标记为公开接口
@Post('register')
async register(@Body() userRegisterDto: UserRegisterDto) {
return await this.userService.create(userRegisterDto);
}
@Public() // 标记为公开接口
@Post('login')
async login(@Body() userLoginDto: UserLoginDto) {
return await this.userService.login(userLoginDto);
}
// 需要认证的接口
@Get('profile')
async getProfile(@Req() request: Request) {
const user = request['user'];
return await this.userService.getProfile(user.userId);
}
}
更新认证守卫
现在我们需要修改认证守卫,让它能够识别公开接口标记:
// auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import type { Request } from 'express';
import { JwtService } from '@nestjs/jwt';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly reflector: Reflector, // 注入 Reflector 服务
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// 检查是否为公开接口
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(), // 方法级别
context.getClass(), // 类级别
]);
// 如果是公开接口,直接放行
if (isPublic) {
return true;
}
// 非公开接口,进行 Token 校验
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('请提供有效的 Token');
}
try {
const payload = await this.jwtService.verifyAsync(token);
request['user'] = payload;
} catch (error) {
throw new UnauthorizedException('Token 无效或已过期');
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
关键改进:
- 注入
Reflector服务来读取元数据 - 使用
getAllAndOverride方法检查接口是否被标记为公开 - 优先检查方法级别,然后检查类级别的标记
- 公开接口直接放行,非公开接口进行 Token 校验
总结
通过本文的实现,我们成功构建了一个基于 NestJS 的 JWT 认证系统。核心在于利用路由守卫进行 Token 验证,配合自定义装饰器实现灵活的权限控制。这种方案既保证了安全性,又保持了代码的简洁性。