Passport 认证
本教程共 47 篇 · 第 20 篇 · 更新于 2026-08-09 · 约 15 分钟阅读
本节目标:搞懂 Passport 在 NestJS 中的工作方式,学会用本地策略做用户名密码认证,用 JWT 策略做 Token 保护路由。
Passport 是什么
Passport 是 Node.js 生态里最老牌的认证库。它的设计思路很巧妙——把认证过程抽象成”策略”模式。
打个比方:你去酒店入住,前台可以用身份证、护照、驾驶证来验证你的身份。每种证件就是一个”策略”,酒店前台就是 Passport——它不关心你用什么证件,只关心验证流程是否通过。
NestJS 通过 @nestjs/passport 模块把 Passport 无缝集成进了框架。
安装依赖
npm install @nestjs/passport passport passport-local
npm install -D @types/passport-local
不管用什么策略,@nestjs/passport 和 passport 这两个包是必须的。然后再根据你用的策略安装对应的包(比如 passport-local、passport-jwt)。
核心概念:策略
Passport 的所有认证逻辑都封装在”策略”里。在 NestJS 中,你通过继承 PassportStrategy 类来定义策略:
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
// 你的验证逻辑
}
每个策略都需要实现一个 validate() 方法。Passport 在认证时会自动调用它,你只需要在里面写验证逻辑:
- 验证通过 → 返回用户对象
- 验证失败 → 抛出异常或返回
null
Note
PassportStrategy(Strategy)的写法看起来有点绕。Strategy是从 passport 策略包里导入的类,PassportStrategy是 NestJS 提供的包装函数。它的作用就是把原生 Passport 策略适配成 NestJS 的依赖注入体系。
本地策略(用户名密码认证)
本地策略是最基础的认证方式——用户提交用户名和密码,服务端验证。
第一步:创建 UsersService
先写一个用户服务,提供”根据用户名查找用户”的能力:
import { Injectable } from '@nestjs/common';
export type User = {
userId: number;
username: string;
password: string;
};
@Injectable()
export class UsersService {
private readonly users: User[] = [
{ userId: 1, username: 'john', password: 'changeme' },
{ userId: 2, username: 'maria', password: 'guess' },
];
async findOne(username: string): Promise<User | undefined> {
return this.users.find(user => user.username === username);
}
}
Warning这里密码是明文存储的,仅用于演示。实际项目中必须用 bcrypt 之类的哈希算法存储密码。
第二步:创建 AuthService
AuthService 负责验证用户凭证:
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
async validateUser(username: string, password: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user && user.password === password) {
const { password, ...result } = user;
return result; // 返回用户对象,但不包含密码
}
return null;
}
}
注意这里用解构把 password 字段剔除了。返回给调用方的用户对象不应该包含密码。
第三步:定义本地策略
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super(); // 默认用 username 和 password 字段
// 如果前端传的字段名不同,可以配置:
// super({ usernameField: 'email' });
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException('用户名或密码错误');
}
return user;
}
}
validate() 方法的参数由策略类型决定。本地策略固定接收 username 和 password 两个参数。
Tip如果你的登录表单用的是
username,在super()里传{ usernameField: 'email' }就行。
第四步:配置模块
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { UsersModule } from '../users/users.module';
@Module({
imports: [UsersModule, PassportModule],
providers: [AuthService, LocalStrategy],
})
export class AuthModule {}
第五步:在控制器中使用
@nestjs/passport 自动为每个策略生成一个 AuthGuard。本地策略的默认名称是 'local':
import { Controller, Post, UseGuards, Request } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller('auth')
export class AuthController {
@UseGuards(AuthGuard('local'))
@Post('login')
async login(@Request() req) {
// 认证通过后,Passport 会把用户对象挂到 req.user 上
return req.user;
}
}
AuthGuard('local') 做了两件事:
- 从请求体中提取
username和password - 调用
LocalStrategy.validate()验证
验证通过后,req.user 就是 validate() 返回的用户对象。验证失败则直接抛 401 Unauthorized。
Tip建议把
AuthGuard('local')封装成具名守卫,避免在代码里到处写魔法字符串:@Injectable() export class LocalAuthGuard extends AuthGuard('local') {}
JWT 策略
本地策略用于”登录”这一步——验证用户名密码。但登录之后呢?总不能每次请求都传密码。
JWT(JSON Web Token)就是解决这个问题的。登录成功后,服务端签发一个 Token 给客户端。后续请求带上这个 Token,服务端验证 Token 就知道你是谁。
安装依赖
npm install @nestjs/jwt passport-jwt
npm install -D @types/passport-jwt
配置 JwtModule
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: 'your-secret-key-dont-expose-it', // 生产环境用环境变量
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
})
export class AuthModule {}
WarningJWT 的 secret 绝对不能硬编码在代码里。生产环境应该用环境变量或配置服务来管理。这里为了演示方便直接写了字符串。
登录时签发 JWT
修改 AuthService,登录成功后签发 Token:
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async validateUser(username: string, password: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user && user.password === password) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: any) {
const payload = { username: user.username, sub: user.userId };
return {
access_token: this.jwtService.sign(payload),
};
}
}
jwtService.sign(payload) 会把 payload 签名成一个 JWT 字符串。sub 是 JWT 标准里表示”主题”(通常是用户 ID)的字段。
定义 JWT 策略
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'your-secret-key-dont-expose-it',
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
三个配置项的含义:
jwtFromRequest:告诉 Passport 从哪里提取 Token。fromAuthHeaderAsBearerToken()表示从Authorization: Bearer <token>请求头中提取ignoreExpiration:设为false表示 Passport 会自动检查 Token 是否过期secretOrKey:用于验证 Token 签名的密钥,必须和签发时用的 secret 一致
NoteJWT 策略的
validate()方法接收的是 Token 解码后的 payload。因为 Passport 已经验证过签名了,所以到这里你拿到的一定是合法数据。validate()的返回值会被挂到req.user上。
保护路由
创建 JWT 守卫:
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
在控制器中使用:
import { Controller, Get, Post, UseGuards, Request, Body } from '@nestjs/common';
import { LocalAuthGuard } from './local-auth.guard';
import { JwtAuthGuard } from './jwt-auth.guard';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
// 登录:用本地策略验证用户名密码,返回 JWT
@UseGuards(LocalAuthGuard)
@Post('login')
async login(@Request() req) {
return this.authService.login(req.user);
}
// 获取个人信息:用 JWT 策略验证 Token
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}
测试流程
# 1. 登录,拿到 Token
curl -X POST http://localhost:3000/auth/login \
-d '{"username": "john", "password": "changeme"}' \
-H "Content-Type: application/json"
# 返回: {"access_token":"eyJhbGciOiJIUzI1NiIs..."}
# 2. 不带 Token 访问受保护路由 → 401
curl http://localhost:3000/auth/profile
# 返回: {"statusCode":401,"message":"Unauthorized"}
# 3. 带上 Token 访问 → 成功
curl http://localhost:3000/auth/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
# 返回: {"userId":1,"username":"john"}
完整的认证流程
把本地策略和 JWT 策略串起来看,整个认证流程是这样的:
1. 客户端 POST /auth/login(用户名 + 密码)
↓
2. LocalAuthGuard 触发 LocalStrategy
↓
3. LocalStrategy.validate() 验证用户名密码
↓
4. 验证通过 → req.user = 用户对象
↓
5. 控制器调用 authService.login(req.user)
↓
6. AuthService 用 JwtService 签发 Token
↓
7. 客户端拿到 Token,后续请求带上
↓
8. 客户端 GET /auth/profile(Authorization: Bearer <token>)
↓
9. JwtAuthGuard 触发 JwtStrategy
↓
10. JwtStrategy 验证 Token 签名和有效期
↓
11. validate() 解析 payload → req.user = { userId, username }
↓
12. 控制器处理请求
扩展守卫
@nestjs/passport 提供的 AuthGuard 可以直接用,但有时候你需要自定义逻辑。比如处理公开路由:
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
// 检查是否是公开路由
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
handleRequest(err, user, info) {
if (err || !user) {
throw err || new UnauthorizedException('认证失败');
}
return user;
}
}
handleRequest 方法让你自定义”认证失败时怎么处理”。默认行为是抛 401,你可以在这里改成返回自定义错误格式。
全局认证守卫
如果你的大部分路由都需要认证,可以注册全局守卫,然后用 @Public() 标记少数公开路由:
// auth.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './jwt-auth.guard';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
})
export class AuthModule {}
// public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// 使用
@Controller('auth')
export class AuthController {
@Post('login')
@Public() // 登录接口不需要认证
login() {}
@Get('profile')
getProfile() {} // 其他接口自动受 JwtAuthGuard 保护
}
命名策略
如果你的项目里同一种类型的策略有多个实例(比如同时支持两种不同的 JWT 签发方式),可以给策略起名字:
@Injectable()
export class AdminJwtStrategy extends PassportStrategy(Strategy, 'admin-jwt') {
// ...
}
// 使用时指定名字
@UseGuards(AuthGuard('admin-jwt'))
Tip大多数项目用默认名字就够了(
'local'、'jwt')。只有在需要多个同类型策略时才需要自定义名字。
密码加密
虽然不完全是 Passport 的事,但认证系统离不开密码加密。用 bcrypt 来处理密码:
npm install bcrypt
npm install -D @types/bcrypt
import * as bcrypt from 'bcrypt';
@Injectable()
export class UsersService {
// 注册时加密密码
async create(username: string, password: string) {
const hashedPassword = await bcrypt.hash(password, 10);
// 存储 hashedPassword 到数据库
return { username, password: hashedPassword };
}
// 登录时验证密码
async validatePassword(plainPassword: string, hashedPassword: string) {
return bcrypt.compare(plainPassword, hashedPassword);
}
}
然后在 AuthService 的 validateUser 里用 validatePassword 代替直接比较明文:
async validateUser(username: string, password: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user && await bcrypt.compare(password, user.password)) {
const { password, ...result } = user;
return result;
}
return null;
}
Note
bcrypt.hash(data, saltRounds)的第二个参数10是加盐轮数。轮数越大越安全,但计算越慢。10 是目前推荐的默认值。
小结
Passport 在 NestJS 中的核心套路就三步:
- 定义策略类(继承
PassportStrategy,实现validate()) - 在模块中注册策略
- 用
AuthGuard('策略名')保护路由
关键知识点回顾:
- 本地策略用于用户名密码登录
- JWT 策略用于 Token 验证
validate()的返回值会被挂到req.user上JwtModule.register()配置 Token 签发参数- 全局守卫 +
@Public()装饰器是推荐的路由保护模式 - 密码必须用 bcrypt 加密存储
下一章深入 JWT 认证,聊聊 Token 刷新、黑名单这些生产环境必须面对的问题。