Passport.js 集成原理
一、Passport 是什么
Passport 是 Node.js 最流行的认证中间件,采用策略(Strategy)模式将认证逻辑解耦。支持 500+ 种策略:
| 策略 | 包名 | 场景 |
|---|---|---|
| JWT | passport-jwt | API 无状态认证(最常用) |
| Local | passport-local | 用户名/密码登录 |
| Google OAuth2 | passport-google-oauth20 | 社交登录 |
| GitHub | passport-github2 | 开发者平台登录 |
| SAML | passport-saml | 企业 SSO |
NestJS 通过 @nestjs/passport 包将 Passport 包装为守卫(Guard),无缝融入 IoC 体系。
二、核心工作流程
HTTP 请求
│
▼
@UseGuards(AuthGuard('jwt')) ← NestJS 守卫触发
│
▼
PassportModule 查找策略 ← 根据策略名称找到 JwtStrategy
│
▼
Strategy.validate(token) ← Passport 提取并验证凭证
│ ├── 验证失败 → 401 Unauthorized(抛出 UnauthorizedException)
│ └── 验证成功 ↓
▼
req.user = validate() 的返回值 ← 注入到请求对象
│
▼
Controller Handler 执行 ← @CurrentUser() 获取用户信息关键点:validate() 返回什么,req.user 就是什么。这是整个 Passport 集成的核心约定。
三、安装与基础配置
bash
npm install @nestjs/passport passport passport-jwt passport-local
npm install -D @types/passport-jwt @types/passport-localtypescript
// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }), // 设置默认策略
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '15m' },
}),
}),
],
providers: [AuthService, JwtStrategy, LocalStrategy],
exports: [AuthService, JwtModule],
})
export class AuthModule {}四、JWT 策略(最常用)
typescript
// auth/strategies/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '../../users/users.service';
export interface JwtPayload {
sub: number; // subject:用户 ID(JWT 标准字段)
email: string;
role: string;
iat?: number; // issued at(自动注入)
exp?: number; // expires at(自动注入)
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(
private configService: ConfigService,
private usersService: UsersService,
) {
super({
// 从 Authorization: Bearer <token> 提取
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false, // false = 过期 token 会被拒绝
secretOrKey: configService.getOrThrow<string>('JWT_SECRET'),
});
}
// Passport 自动验证签名和过期时间,这里只做业务验证
async validate(payload: JwtPayload) {
// 可选:查数据库确认用户仍然存在(防止已删除用户继续访问)
const user = await this.usersService.findOne(payload.sub);
if (!user) throw new UnauthorizedException('用户不存在');
// 返回值成为 req.user
return { id: payload.sub, email: payload.email, role: payload.role };
}
}从 Cookie 提取 Token(前后端同域时):
typescript
import { Request } from 'express';
super({
jwtFromRequest: (req: Request) => {
return req.cookies?.['access_token'] ?? null;
},
// ...
});五、Local 策略(用户名密码登录)
Local 策略专用于登录接口——验证邮箱+密码,成功后签发 JWT。
typescript
// auth/strategies/local.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy, 'local') {
constructor(private authService: AuthService) {
super({
usernameField: 'email', // 默认是 'username',改为 'email'
passwordField: 'password', // 默认即 'password'
});
}
async validate(email: string, password: string) {
const user = await this.authService.validateUser(email, password);
if (!user) {
// 统一错误信息,不暴露"邮箱不存在"还是"密码错误"
throw new UnauthorizedException('邮箱或密码错误');
}
return user; // 成为 req.user
}
}typescript
// 登录控制器:使用 local 守卫
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@UseGuards(AuthGuard('local')) // ← 触发 LocalStrategy.validate()
@Post('login')
login(@CurrentUser() user: User) {
// 到这里说明邮箱密码正确,req.user = validate 的返回值
return this.authService.generateTokens(user);
}
}六、Refresh Token 策略
typescript
// auth/strategies/jwt-refresh.strategy.ts
@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
constructor(configService: ConfigService) {
super({
// Refresh Token 从请求体取(也可从 Cookie)
jwtFromRequest: ExtractJwt.fromBodyField('refresh_token'),
secretOrKey: configService.getOrThrow('JWT_REFRESH_SECRET'),
passReqToCallback: true, // validate 可以接收原始 req
});
}
async validate(req: Request, payload: JwtPayload) {
// 可在此验证 refresh_token 是否在黑名单/数据库中
const refreshToken = req.body['refresh_token'];
const isValid = await this.authService.validateRefreshToken(
payload.sub,
refreshToken,
);
if (!isValid) throw new UnauthorizedException('Refresh Token 已失效');
return payload;
}
}七、策略的 PassportStrategy 继承机制
typescript
// PassportStrategy 是个工厂函数,它:
// 1. 接收 Passport 原生 Strategy 类
// 2. 返回可被 @Injectable() 装饰的 NestJS 类
// 3. NestJS IoC 容器自动注入依赖
// 多个策略同时注册(策略名作为唯一标识)
@Module({
providers: [
JwtStrategy, // 'jwt'
LocalStrategy, // 'local'
JwtRefreshStrategy, // 'jwt-refresh'
GoogleStrategy, // 'google'
],
})
export class AuthModule {}
// 使用时通过名称指定
@UseGuards(AuthGuard('jwt'))
@UseGuards(AuthGuard('local'))
@UseGuards(AuthGuard('jwt-refresh'))八、自定义 AuthGuard(扩展默认行为)
继承 AuthGuard 可以自定义失败处理逻辑:
typescript
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
// 检查是否标记为 @Public(),是则跳过认证
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
return super.canActivate(context);
}
// 自定义认证失败的错误信息
handleRequest(err: any, user: any, info: any) {
if (err || !user) {
if (info?.name === 'TokenExpiredError') {
throw new UnauthorizedException('Token 已过期,请重新登录');
}
if (info?.name === 'JsonWebTokenError') {
throw new UnauthorizedException('无效的 Token');
}
throw err || new UnauthorizedException('请先登录');
}
return user;
}
}九、完整模块结构
auth/
├── auth.module.ts
├── auth.controller.ts
├── auth.service.ts
├── strategies/
│ ├── jwt.strategy.ts
│ ├── local.strategy.ts
│ └── jwt-refresh.strategy.ts
└── guards/
├── jwt-auth.guard.ts ← 扩展 AuthGuard('jwt')
└── local-auth.guard.ts ← 扩展 AuthGuard('local')typescript
// 全局注册 JWT 守卫(所有路由默认需要认证)
// app.module.ts
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
}
// 公开接口用 @Public() 跳过
@Public()
@Post('register')
register(@Body() dto: RegisterDto) {}可运行 Demo:
practice/04-auth-system— JWT + Passport + RBAC 可运行 Demo,npm start→ http://localhost:3001/api
常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
passport.initialize() 未调用 | 手动集成时漏掉初始化 | NestJS 的 PassportModule 自动处理,不要手动调用 |
| 多策略共存时路由用了错误策略 | Guard 绑定的策略名错误 | AuthGuard('jwt') 与 Strategy 的 super('jwt') 名字必须一致 |
req.user 为 undefined | Strategy 的 validate() 返回了 null 或抛出异常被吞掉 | 在 validate 中加日志,确认返回值非空 |
| Refresh Token 策略无法拿到 body | 默认只读 header | new PassportStrategy(Strategy, 'jwt-refresh') + super({ passReqToCallback: true }) |