首页 / NestJS 入门教程 / Passport 认证

NestJS 入门教程

Passport 认证

本教程共 47 篇 · 第 20 篇 · 更新于 2026-08-09 · 约 15 分钟阅读

NestJSPassport认证本地策略JWTAuthGuardPassportStrategy

本节目标:搞懂 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/passportpassport 这两个包是必须的。然后再根据你用的策略安装对应的包(比如 passport-localpassport-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() 方法的参数由策略类型决定。本地策略固定接收 usernamepassword 两个参数。

Tip

如果你的登录表单用的是 email 而不是 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') 做了两件事:

  1. 从请求体中提取 usernamepassword
  2. 调用 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 {}
Warning

JWT 的 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 一致
Note

JWT 策略的 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 中的核心套路就三步:

  1. 定义策略类(继承 PassportStrategy,实现 validate()
  2. 在模块中注册策略
  3. AuthGuard('策略名') 保护路由

关键知识点回顾:

  • 本地策略用于用户名密码登录
  • JWT 策略用于 Token 验证
  • validate() 的返回值会被挂到 req.user
  • JwtModule.register() 配置 Token 签发参数
  • 全局守卫 + @Public() 装饰器是推荐的路由保护模式
  • 密码必须用 bcrypt 加密存储

下一章深入 JWT 认证,聊聊 Token 刷新、黑名单这些生产环境必须面对的问题。