Skip to content

Passport.js 集成原理

一、Passport 是什么

Passport 是 Node.js 最流行的认证中间件,采用策略(Strategy)模式将认证逻辑解耦。支持 500+ 种策略:

策略包名场景
JWTpassport-jwtAPI 无状态认证(最常用)
Localpassport-local用户名/密码登录
Google OAuth2passport-google-oauth20社交登录
GitHubpassport-github2开发者平台登录
SAMLpassport-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-local
typescript
// 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 starthttp://localhost:3001/api


常见错误

错误原因解决
passport.initialize() 未调用手动集成时漏掉初始化NestJS 的 PassportModule 自动处理,不要手动调用
多策略共存时路由用了错误策略Guard 绑定的策略名错误AuthGuard('jwt') 与 Strategy 的 super('jwt') 名字必须一致
req.userundefinedStrategy 的 validate() 返回了 null 或抛出异常被吞掉在 validate 中加日志,确认返回值非空
Refresh Token 策略无法拿到 body默认只读 headernew PassportStrategy(Strategy, 'jwt-refresh') + super({ passReqToCallback: true })

NestJS 深度学习体系