在 Nest 中使用 Passport:从用户名密码登录到 JWT 路由保护
原文:NestJS 官方文档贡献者,Passport (authentication)。中文翻译与技术整理:未完纪。核对日期:2026-10-08。对照 Nest 官方文档仓库的 Passport 原始 Markdown;该 URL 指向可变 master 分支,未固定 commit。为避免重复,统一保留 TypeScript 分支,省去等价的 JavaScript 切换示例。来源仓库根许可证为 MIT,全文随稿附上。
Passport 将认证过程归纳为三件事:核验用户名密码、JWT 或身份提供商令牌;通过令牌或会话管理认证状态;把认证后的用户信息附加到请求对象。它有丰富的策略生态,@nestjs/passport 将策略包装成 Nest 熟悉的依赖注入、模块和守卫。本文串起完整的 REST API 认证过程,并说明如何扩展这套结构。

先明确认证流程
客户端先把用户名和密码发给登录端点。验证成功,服务器签发 JWT;后续请求在 Authorization 头中以 Bearer 方式携带它。受保护路由只处理带有效 JWT 的请求。先实现本地策略,再加令牌签发,最后接上保护路由。
$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-local
无论选择哪一种 Passport 策略,都需要 @nestjs/passport 和 passport;另外还要安装实现该策略的包,以及需要的 TypeScript 类型声明。原生 Passport 接收策略选项和 verify 回调。Nest 中的写法是继承 PassportStrategy,在构造函数中通过 super(options) 传配置,并用 validate() 实现校验回调。成功时返回用户;用户不存在或凭据不符时返回失败或抛出异常。
建立用户服务与认证服务
$ nest g module auth
$ nest g service auth
$ nest g module users
$ nest g service users
UsersService 封装用户读取。原文为了让例子自包含,把两个用户放在内存数组中;实际项目应在这一层接入数据库和用户实体。下面两组账户和密码都是公开演示数据,不能作为真实账户配置。
import { Injectable } from '@nestjs/common';
// This should be a real class/interface representing a user entity
export type User = any;
@Injectable()
export class UsersService {
private readonly users = [
{
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);
}
}
在 users/users.module.ts 中导出 UsersService,AuthModule 才能注入它。
import { Module } from '@nestjs/common';
import { UsersService } from './users.service.js';
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
AuthService 的 validateUser() 查询用户并核对密码。验证成功后,用对象剩余语法移除 password,避免它跟随认证结果进入 req.user 或响应。
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service.js';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
}
原文安全边界:这里把密码以明文保存,并使用 === 比较,只为解释控制流程。真实项目应保存带盐单向密码哈希,使用 bcrypt 等专门库的验证接口核对输入,不能把“对输入重新哈希后比较字符串”当作所有密码哈希格式通用的验证方法。还应按自身环境补充登录频率限制、输入校验和安全传输。本文没有实现这些生产控制,也没有执行示例服务。
接着让 AuthModule 导入 UsersModule。
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service.js';
import { UsersModule } from '../users/users.module.js';
@Module({
imports: [UsersModule],
providers: [AuthService],
})
export class AuthModule {}
实现 local 策略
在 auth/local.strategy.ts 中继承 passport-local 的 Strategy。默认从请求体读取 username 和 password,因此 super() 不必带额外选项。若希望用邮箱字段,可传入 super({ usernameField: ’email’ })。validate(username, password) 调用前面建立的 AuthService;失败时抛出 UnauthorizedException,让 Nest 异常层处理。成功返回的对象最终成为 req.user。
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service.js';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
这一模式适用于其他策略,只是凭据形式不同。JWT 策略也可以在 validate() 中检查用户是否还存在,或者查询撤销记录;这些都是业务附加检查,并不是 Passport 自动替你实现的能力。把 PassportModule 加入 imports,把 LocalStrategy 加入 providers,策略才会被注册。
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service.js';
import { UsersModule } from '../users/users.module.js';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy.js';
@Module({
imports: [UsersModule, PassportModule],
providers: [AuthService, LocalStrategy],
})
export class AuthModule {}
用守卫启动登录
守卫既能阻止未认证请求进入受限路由,也能启动认证。POST /auth/login 使用 AuthGuard(‘local’),触发读取凭据、调用验证函数和填充 req.user。这里的 local 是 passport-local 的默认策略名,不是路由名。先让控制器返回用户,检查认证链路是否接通。
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller()
export class AppController {
@UseGuards(AuthGuard('local'))
@Post('auth/login')
async login(@Request() req: any) {
return req.user;
}
}
$ # POST to /auth/login
$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
$ # result -> {"userId":1,"username":"john"}
上面的响应只是原文展示的预期结果,不是本轮测试输出。为了避免到处散落策略名称字符串,建立有名字的 LocalAuthGuard,随后在路由上使用这个类。
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(@Request() req: any) {
return req.user;
}
会话注销与 JWT 的边界
原文还给出了 req.logout() 的会话注销分支。它依赖 express-session、passport.initialize() 和 passport.session(),而本教程并未配置这些中间件。原文在该路由上使用 LocalAuthGuard,这会重新校验用户名和密码,并不等于验证已有会话;因此下面保留的是原文示例,不是可直接复制的会话注销端点。真实应用应按已配置的会话认证和路由策略改造。Passport 0.6 起 logout() 需要回调,可包成 Promise。它只清除会话,不会撤销已经签发的 JWT。
@UseGuards(LocalAuthGuard)
@Post('auth/logout')
async logout(@Request() req: any) {
await new Promise<void>((resolve, reject) =>
req.logout((err: any) => (err ? reject(err) : resolve())),
);
}
签发 JWT
用户名密码验证成功后,登录处理器才会执行,req.user 也已经存在。安装 @nestjs/jwt、passport-jwt 和其类型声明,然后把令牌签发集中放在 AuthService。@nestjs/jwt 提供令牌处理工具,passport-jwt 负责请求认证策略。
$ npm install --save @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-jwt
注入 JwtService 并加入 login()。载荷只选取 username 和 userId,将 userId 放入标准 sub 声明。返回对象只有 access_token 字段。注意 JWT 的载荷不等于秘密存储空间,不能把用户密码等敏感数据放进去。
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service.js';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService
) {}
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user && user.password === pass) {
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),
};
}
}
原文 auth/constants.ts 使用下面这条公开固定字符串,使签发和验证引用同一密钥。它是一个明确写着“不要使用”的占位值,而不是安全密钥。直接照抄仍会形成可预测的签名配置。
export const jwtConstants = {
secret: 'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.',
};
编辑修订:下面替代 constants.ts 的版本从运行环境读取密钥,并在缺失时停止启动,不提供默认固定值。密钥必须由部署环境或秘密管理服务提供;这段修订没有替代密钥生成、轮换与保管流程,也未做运行测试。
const secret = process.env.JWT_SECRET;
if (!secret) {
throw new Error('JWT_SECRET must be configured');
}
export const jwtConstants = { secret };
JwtModule.register() 配置签名密钥及有效期。原文将有效期设为 60 秒,方便观察过期拒绝。AuthModule 导出 AuthService,以便上层控制器使用。这里与后面的 JwtStrategy 必须读取同一配置。
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service.js';
import { LocalStrategy } from './local.strategy.js';
import { UsersModule } from '../users/users.module.js';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants.js';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy],
exports: [AuthService],
})
export class AuthModule {}
登录端点现在返回 this.authService.login(req.user),而不是直接把用户对象发回去。
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { LocalAuthGuard } from './auth/local-auth.guard.js';
import { AuthService } from './auth/auth.service.js';
@Controller()
export class AppController {
constructor(private authService: AuthService) {}
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(@Request() req: any) {
return this.authService.login(req.user);
}
}
$ # POST to /auth/login
$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
$ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
$ # Note: above JWT truncated
示例令牌被省略号截断,不能复制为可用 Bearer 令牌。请求命令采用类 Unix shell 的引号形式;在其他命令解释器中应按其规则传递 JSON。
验证 JWT 并保护 profile
创建 auth/jwt.strategy.ts。jwtFromRequest 指定从 Authorization: Bearer 中取令牌;ignoreExpiration: false 保持过期校验,过期会产生 401;secretOrKey 提供验签所需的密钥。这里使用对称签名,其他部署也可按策略支持的选项使用 PEM 公钥等方式。
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants.js';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
Passport 先验证签名、解码载荷并检查有效期,之后才调用 validate(payload)。本例返回 userId 和 username;它们成为 req.user。这只说明令牌通过当前配置的验证,并不自动证明用户此刻仍未禁用,也不等同于资源级授权。可在 validate() 中追加数据库读取和撤销检查。原文还说明可以返回数组:第一个值映射到 user,第二个映射到 req.authInfo。
把 JwtStrategy 加入 AuthModule 的 providers,保留签发与验证共用的密钥。
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service.js';
import { LocalStrategy } from './local.strategy.js';
import { JwtStrategy } from './jwt.strategy.js';
import { UsersModule } from '../users/users.module.js';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants.js';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
再建立 JwtAuthGuard,并给 GET /profile 加上这个守卫。根模块应保留生成器建立的 AuthModule 导入,使控制器能使用其导出的 AuthService。
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
import { Controller, Get, Request, Post, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from './auth/jwt-auth.guard.js';
import { LocalAuthGuard } from './auth/local-auth.guard.js';
import { AuthService } from './auth/auth.service.js';
@Controller()
export class AppController {
constructor(private authService: AuthService) {}
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(@Request() req: any) {
return this.authService.login(req.user);
}
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req: any) {
return req.user;
}
}
原文使用以下请求序列演示结果:不带令牌访问 profile 返回 401;正确登录取得令牌;带令牌访问可得到用户信息。等待超过示例的 60 秒有效期后再次请求,应被拒绝。过期判断由 Passport JWT 策略完成。这是待读者在自己的隔离环境执行的检查序列,本轮未运行。
$ # GET /profile
$ curl http://localhost:3000/profile
$ # result -> {"message":"Unauthorized","statusCode":401}
$ # POST /auth/login
$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
$ # result -> {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm... }
$ # GET /profile using access_token returned from previous step as bearer code
$ curl http://localhost:3000/profile -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm..."
$ # result -> {"userId":1,"username":"john"}
60 秒通常不是实际产品的完整令牌策略。刷新、撤销、注销后的处理和权限模型均需另外设计;本例只是无状态访问令牌流程。
扩展守卫与策略链
需要改变认证过程或错误处理时,可继承 AuthGuard 并重写 canActivate()、handleRequest()。原文保留了调用 super.canActivate() 的位置,也提示会话流程可使用 super.logIn(request)。handleRequest() 必须继续拒绝错误或没有用户的情况,不能在扩展时误把失败请求放行。
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
// Add your custom authentication logic here
// for example, call super.logIn(request) to establish a session.
return super.canActivate(context);
}
handleRequest(err: any, user: any, info: any) {
// You can throw an exception based on either "info" or "err" arguments
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}
AuthGuard 还可以接受策略名称数组。策略依次尝试;第一个成功、重定向或出错的策略会终止链。单个策略的认证失败才继续尝试后面的策略,全部失败后才判定整个认证失败。下列是结构示意,省略号不是可编译实现。
export class JwtAuthGuard extends AuthGuard(['strategy_jwt_1', 'strategy_jwt_2', '...']) { ... }
默认保护全部路由,再声明公开端点
多数端点都需要认证时,可以通过 APP_GUARD 注册全局 JwtAuthGuard。APP_GUARD 从 @nestjs/core 导入。然后用元数据标记公开端点,避免给每个控制器重复添加守卫。
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
@Public()
@Get()
findAll() {
return [];
}
JwtAuthGuard 注入 @nestjs/core 的 Reflector,通过 getAllAndOverride() 先查处理器再查控制器元数据。发现公开标记返回 true,否则继续执行 JWT 验证。下面片段需要与前面的 import 和 IS_PUBLIC_KEY 定义配套使用。
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}
编辑提示:开启全局 JWT 守卫后,登录端点也必须添加 @Public(),同时保留 LocalAuthGuard。Public 只让全局 JWT 守卫跳过该端点,用户名密码校验仍由本地守卫负责,否则用户在拿到令牌之前就会被全局 JWT 守卫挡住。
在策略中使用请求作用域服务
Passport 在全局实例中注册策略,因此策略本身不适合设为请求作用域。它不绑定某条路由,Nest 无法据此决定每个请求应实例化哪一种策略。需要请求作用域服务时,让策略保持可注册的作用域,在 validate() 内动态解析服务。
先从 @nestjs/core 注入 ModuleRef,并把 passReqToCallback 设为 true,让 Passport 将请求对象传给校验函数。随后使用 ContextIdFactory.getByRequest(request) 取得当前请求的上下文标识,再调用 moduleRef.resolve(AuthService, contextId)。不要另建无关上下文,下面省略的部分应继续执行既有验证。
constructor(private moduleRef: ModuleRef) {
super({
passReqToCallback: true,
});
}
async validate(
request: Request,
username: string,
password: string,
) {
const contextId = ContextIdFactory.getByRequest(request);
// "AuthService" is a request-scoped provider
const authService = await this.moduleRef.resolve(AuthService, contextId);
...
}
调整 Passport 配置与命名策略
PassportModule.register() 的 defaultStrategy 和 property 由 @nestjs/passport 使用,其余选项传给 Passport authenticate(),具体选项取决于所用策略。session: true 是其中一个配置示例,但单独这一行不会替你建立前述会话中间件。
PassportModule.register({ session: true });
策略自己的选项仍在构造函数中传给 super()。例如把用户名字段改成 email,同时指定密码字段:
constructor(private authService: AuthService) {
super({
usernameField: 'email',
passwordField: 'password',
});
}
PassportStrategy 的第二个参数可以改策略注册名。使用 myjwt 后,应以 @UseGuards(AuthGuard(‘myjwt’)) 引用它;不传名称时使用底层策略的默认名。
export class JwtStrategy extends PassportStrategy(Strategy, 'myjwt')
在 GraphQL 中取得请求与用户
GraphQL 的执行上下文与 HTTP 控制器不同。继承 AuthGuard 后重写 getRequest(),用 GqlExecutionContext.create(context) 取得 GraphQL 上下文,再返回 ctx.getContext().req。应用的 GraphQL 配置必须实际提供这个 req。
@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
要在 resolver 参数中取得已认证用户,可建立 CurrentUser 参数装饰器,读取同一个 req.user:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
export const CurrentUser = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req.user;
},
);
原文用以下 resolver 展示装饰器组合。这里的 User、usersService.findById() 和 user.id 属于 GraphQL 示例自己的数据模型;前面的 REST JWT 示例返回的是 userId,合并到同一项目时必须统一字段,而不能直接复制后假定存在 id。
@Query(() => User)
@UseGuards(GqlAuthGuard)
whoAmI(@CurrentUser() user: User) {
return this.usersService.findById(user.id);
}
若 GraphQL 使用 passport-local,还要把 GraphQL 参数放入请求体,供本地策略提取用户名密码。否则策略读不到凭据,会返回未授权。原文示例按下面顺序合并:同名 GraphQL 参数会覆盖已有 body 字段。只应传入预期的登录参数,并在 schema 或验证层约束它们。
@Injectable()
export class GqlLocalAuthGuard extends AuthGuard('local') {
getRequest(context: ExecutionContext) {
const gqlExecutionContext = GqlExecutionContext.create(context);
const gqlContext = gqlExecutionContext.getContext();
const gqlArgs = gqlExecutionContext.getArgs();
gqlContext.req.body = { ...gqlContext.req.body, ...gqlArgs };
return gqlContext.req;
}
}
来源与审核说明
本文保留官方章节的完整技术路径,合并了重复语言分支,并明确标出密钥读取、全局登录端点和 GraphQL 字段衔接的编辑提示。研究记录以 Nest 12 与 @nestjs/passport 12 为适用背景;当前文档源码没有为示例锁定完整依赖树,安装时仍需核对对应版本。所有代码只做静态阅读,未安装依赖、启动服务器或执行认证请求。
相关官方资料:@nestjs/passport、@nestjs/jwt、passport-jwt 选项。原作者与来源归属保留;Nest 官方文档仓库采用 MIT 许可,版权信息及许可文本见随稿 LICENSE-MIT.txt。示意图为未完纪原创绘制,不是运行截图。











暂无评论内容