普通视图

发现新文章,点击刷新页面。
昨天 — 2026年4月13日首页

nestjs实战-登录、鉴权(一)

作者 web_bee
2026年4月13日 17:53

一个完整的登录流程中至关重要的就是它的认证方式,现有的认证方式主要有一下3种:

  • session/cookie
  • JWT(Json Web Token)
  • Oauth

一、鉴权方式

Session/Cookie

原理:用户登录后,服务器在内存中创建 Session,并将 Session ID 通过 Cookie 返回给客户端。后续请求自动携带 Cookie。

特点:适用于传统Web应用,有状态,不适合跨域或高并发环境

优点:

  • 较易扩展
  • 开发简单

缺点:

  • 需要在服务端存储session,性能低、多服务器同步session困难
  • 由于cookie只在浏览器上能使用,所以它跨平台困难

JWT

原理:服务端通过特定密钥生成包含用户信息的签名字符串(Token),客户端在请求头(Authorization: Bearer Token)中携带。服务器不存储会话,只验证签名。

特点:无状态、扩展性好,适用于微服务和单页应用(SPA)、跨平台能力

JWT 本质上是一个经过数字签名的字符串,它由三部分组成,用点号(.)分隔:Header.Payload.Signature。token长什么样?

Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOiIyMDAwNCIsInJuU3RyIjoiQVpVUVY2dnAyUllkOFRucnJoaXNsQWtRcTFhSEFxeTAiLCJ0ZW5hbnRJZCI6IjEiLCJzY29wZSI6IlJPTEU6Ok1JQ1JPX0FQUCIsImFwcEtleSI6InJIWnJZeVB1eW1oaXZlSUUiLCJuaWNrTmFtZSI6IuWui-Wwj-aXrSIsImxvZ2luVGltZSI6MTc3NTYxMzI4NzYxNX0.bq8qEE-imza7pLucKOvKw2WvukW2lSPr1WMbAcuJLmU

优点:

  • 支持跨平台:移动端、跨应用
  • 安全、承载信息丰富

缺点:

  • 刷新与过期处理
  • payload不易过大
  • 中间人攻击(没有绝对的安全)

Oauth

第三方授权,例如 微信、支付宝、QQ、谷歌账号登录等

优点:

  • 开放、安全、简单
  • 权限指定

缺点:

  • 需要增加授权服务器
  • 增加网络请求

二、实战

首先 创建身份验证模块,在指定的目录下执行如下命令:

nest g res auth

整个流程我把它分成两部分:

  1. 用户登录过程
  2. 登录成功后,带token请求业务接口的过程

2.1 前置知识

此段内容篇理论介绍,可以先看后面的代码实现,有疑问再来看这里的内容

模块之间的复用

auth模块中如何使用 users 模块中的 users.service.ts 中的方法

之前的章中我们创建了 Users 模块,现在 auth 模块中想要使用 users.service 中的方法:

  • 首先需要在 users.module.ts 中导出 users.service.ts

    // ... 省略
    @Module({
      imports: [TypeOrmModule.forFeature([UserEntity])],
      controllers: [UsersController],
      providers: [UsersService],
      exports: [UsersService], // 导出
    })
    export class UsersModule {}
    
    
  • auth.module.ts 中导入

    import { Module } from '@nestjs/common';
    import { AuthService } from './auth.service';
    import { AuthController } from './auth.controller';
    import { UsersModule } from '../users/users.module';
    
    @Module({
      imports: [UsersModule],
      controllers: [AuthController],
      providers: [AuthService],
    })
    export class AuthModule {}
    
  • 这样就能在 auth.service.ts 中使用了

    import { Injectable } from '@nestjs/common';
    // 注意这里在使用中还是需要 显示的引入,而不是会自动引入
    import { UsersService } from '../users/users.service';
    
    @Injectable()
    export class AuthService {
      constructor(private readonly usersService: UsersService) {}
    
      async register(createUserDto: any) {
        return this.usersService.findAll();
      }
    }
    

JWT 相关包介绍

首先安装依赖:

 pnpm add @nestjs/passport passport passport-jwt @nestjs/jwt 
pnpm add @types/passport @types/passport-jwt -D

在集成 JWT 之前,先学习一下使用到的包,并了解它都提供了哪些功能。这里是掌握整个流程非常重要的环境,参考了网上(包括官网)一上来就是介绍怎么使用、代码怎么写,确实是让人很迷惑。

1. @nestjs/passport
  1. 集成 Passport.js:让 NestJS 应用能够使用 Passport.js 提供的超过 500 种认证策略(如本地用户名密码、JWT、OAuth 2.0 等)。
  2. 提供装饰器:它提供了一系列装饰器(如 @UseGuards(AuthGuard('jwt'))),让你可以非常方便地在控制器(Controller)或路由上应用认证保护。
  3. 简化策略创建:通过 PassportStrategy 类,你可以轻松地创建自定义策略,而无需直接处理 Passport.js 的底层 API。
import { PassportModule, PassportStrategy, AuthGuard } from '@nestjs/passport';
PassportModule
  • 类型: NestJS 模块 (@Module)

  • 作用: 它是整个 Passport 集成的入口。你需要把它导入到你的 NestJS 模块(如 AuthModule)中,才能启用 Passport 功能。

  • 核心功能

    • 它通过 .register() 方法允许你配置全局选项,比如指定默认的认证策略(defaultStrategy)。
    • 它负责将 Passport 的服务注入到 NestJS 的依赖注入容器中
PassportStrategy-策略实现
  • 类型:抽象类 / 辅助函数

  • 作用:它是用来创建具体认证逻辑的基类。Passport.js 本身有各种策略(如 Local, JWT, Google),这个函数帮助我们将这些策略适配到 NestJS 的类结构中。

  • 核心功能

    • 它接受一个具体的策略类(如 passport-localStrategy)作为参数。
    • 它让你重写 validate 方法,在这里编写具体的验证逻辑(比如查数据库比对密码)。
AuthGuard-路由保护
  • 类型:守卫 (CanActivate)

  • 作用:它是 NestJS 的守卫,用于保护路由。它拦截请求,并告诉 Passport 执行哪个策略。

  • 核心功能

    • 它通常配合 @UseGuards() 装饰器使用。
    • 它接收一个参数(字符串),这个字符串必须与你定义的策略名称(或默认名称)匹配,从而触发对应的验证流程。
2. @nestjs/jwt

@nestjs/jwt 是 Nest 官方对 JWT 的封装包,底层基于 jsonwebtoken。它主要解决三件事:

  1. 在 Nest 里统一配置 JWT 密钥、过期时间等(模块化)
  2. 提供 JwtService 来签发和校验 token
  3. 很容易和 Passportjwt strategy 集成成认证体系
import { JwtModule, JwtService } from '@nestjs/jwt';
JwtModule:模块注册器
  • 用来在 Nest DI 容器里注册 JWT 相关配置和服务

  • 常见用法:

    • JwtModule.register({...}) 静态配置
    • JwtModule.registerAsync({...}) 动态读取配置(比如从 ConfigServiceJWT_SECRET
JwtService:具体干活的服务

用它生成 token、验证 token、解码 token,一般在 AuthService 里注入并调用

  • sign(payload, options?)

    • 作用:根据 payload 生成 JWT 字符串
    • 示例:this.jwtService.sign({ sub: user.id, name: user.name })
    • options 可以覆盖模块默认配置,比如 expiresIn, secret
  • verify(token, options?)

    • 作用:验证 token 是否合法、是否过期,并返回解码后的 payload
    • 验证失败会抛异常(如签名不对、过期)
    • 示例:this.jwtService.verify(token)
3. passport-jwt

passport-jwt 这个包的核心作用是:给 Passport 提供“JWT 认证策略”。

import { ExtractJwt, Strategy } from 'passport-jwt';

主要用到两个能力:

  • Strategy

    • JWT 的 Passport 策略类
    • 你在 Nest 里通常写 extends PassportStrategy(Strategy, 'jwt')
    • 用来定义:用什么密钥验签、是否忽略过期、验证成功后返回什么用户信息
  • ExtractJwt

    • token 提取器工具
    • 最常用:ExtractJwt.fromAuthHeaderAsBearerToken()
    • 表示从 Authorization: Bearer <token> 里取 token
    • 也支持从 cookie、query、或自定义函数提取

简化理解:

  • @nestjs/jwt 偏向“签发/校验 token 的服务能力”
  • passport-jwt 偏向“请求进来时怎么从请求里拿 token 并走认证策略”

二者经常一起用:登录时 JwtService.sign() 发 token,请求鉴权时 passport-jwtStrategy 来验。

4. 总结

首先为什么需要安装 passport:

仅安装 @nestjs/passport 是不够的。简单来说,@nestjs/passport 只是一个“适配器”,它的核心作用是将 passport 的功能封装成 NestJS 熟悉的模块和依赖注入形式,但它本身并不包含 passport 的核心逻辑。

你可以这样理解它们的关系:

  • passport: 这是核心的认证库,提供了所有的认证策略和逻辑。
  • @nestjs/passport: 这是一个 NestJS 的官方封装包,它让你能以更符合 NestJS 风格(如使用装饰器、依赖注入)的方式来使用 passport

完整的登录鉴权依赖

要实现一个完整的登录鉴权功能,你通常需要安装以下几个包:

  1. 核心库:

    • passport: 认证的核心。
    • @nestjs/passport: NestJS 的适配器。
  2. 具体策略库 (根据你选择的认证方式):

    • 用户名密码登录: 需要 passport-local
    • JWT 无状态认证: 需要 passport-jwt
  3. 类型定义 (TypeScript 项目需要):

    • @types/passport
    • @types/passport-local (如果使用 local 策略)
    • @types/passport-jwt (如果使用 jwt 策略)

2.2 JWT 集成

auth.module.ts

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';

import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from '../users/users.module';
import { securityRegToken, ISecurityConfig } from '~/config';
import { isDev } from '~/global/env';

@Module({
  imports: [
    PassportModule, // 引入 PassportModule 模块
    // 引入 JwtModule 模块,及配置 JwtModule 模块
    JwtModule.registerAsync({
      imports: [ConfigModule], // 引入 ConfigModule 模块
      useFactory: (configService: ConfigService) => {
        const { jwtSecret, jwtExprire } = configService.get<ISecurityConfig>(securityRegToken)
        return {
          secret: jwtSecret, // 设置密钥
          signOptions: { expiresIn: jwtExprire }, // 设置过期时间
          ignoreExpiration: isDev, // 开发环境忽略过期时间
        }
      },
      inject: [ConfigService], // 注入 ConfigService 服务
    }),
    UsersModule,
  ],
  controllers: [AuthController],
  providers: [
    AuthService,
  ],
})
export class AuthModule {}

以上代码 主要是 引入了 PassportModule, JwtModule,并完善了相关配置;

2.3 用户登录过程

  • 用户输入 用户名、密码 等信息

    通过http(加密|明文),传输给后端

  • 后端接受到来自前端 的http 请求

    • 验证账号密码是否一致
    • 生成 token 传递给前端

auth.controller.ts

import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';

import { AuthService } from './auth.service';
import { LoginDto } from './dto/auth.dto';

@Controller('auth')
export class AuthController {
  constructor(
    private readonly authService: AuthService,
  ) {}

  // 登录
  @Post('login')
  login(@Body() loginDto: LoginDto) {
    return this.authService.login(loginDto);
  }
}

auth.service.ts

import { HttpException, Injectable, HttpStatus } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { isEmpty } from 'lodash';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(
    private readonly usersService: UsersService,
    private readonly jwtService: JwtService,
  ) {}

  async login(loginDto: {
    name: string;
    password: string;
  }) {
    const { name, password } = loginDto;
    const user = await this.usersService.findUserByUserName(name);
    if (isEmpty(user)) {
      throw new HttpException('用户不存在', HttpStatus.NOT_FOUND);
    }

    const { password: userPassword } = user;
    if (userPassword !== password) {
      throw new HttpException('密码错误', HttpStatus.BAD_REQUEST);
    }

    // 生成 JWT 签名
    const jwtSign = await this.jwtService.signAsync({
      id: user.id,
      name: user.name,
      pv: 1, // 版本号
    })
    return {
      access_token: jwtSign
    };
  }
}

用户登录过程代码如上,还是比较简单:

  • 获取用户传递过来的参数,校验用户是否存在,密码是否正确
  • 生成JWT签名,返回token給前端

功能主线如此,密码加密、token管理等有待完善

2.4 业务接口token验证过程

由于文章篇幅,放到下一节讲解;

nestjs实战 - 拦截器,统一处理接口请求与响应结果

作者 web_bee
2026年4月13日 10:13

在之前的篇章中介绍了 拦截器的基本概念、使用方法、使用场景;

本节主要从实战层面开发一个通用功能:统一处理接口请求与响应结果

需求:

  • 统一处理接口请求与响应结果
  • 可选配置(部分接口如果不需要统一处理 可配置)

第一步,全局注入拦截器

首先创建一个 transform.interceptor.ts 文件,并全局注入:

/// app.module.ts
// 省略其它代码
// 主要代码
import { APP_INTERCEPTOR } from '@nestjs/core';
import { TransformInterceptor } from './common/interceptors/transform.interceptor';

@Module({
  // ...
  providers: [
    // 全局注入拦截器,它会作用到所有路由上
    { provide: APP_INTERCEPTOR, useClass: TransformInterceptor }, 
  ],
})
export class AppModule {}

第二步,拦截器功能实现

需要注意的点,我们需要处理

  • 请求参数(前置拦截器)
  • 响应结果(后置拦截器)

在之前的章节中也介绍了这两个概念。

// 首先需要下载两个相关依赖
pnpm add fastify qs  

transform.interceptor.ts

实现拦截器 transform.interceptor.ts 内部逻辑:

import {
  NestInterceptor,
  CallHandler,
  ExecutionContext,
  Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core'
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import type { FastifyRequest } from 'fastify'
import qs from 'qs';

import { ResponseModel } from '../mode/response.mode';
import { BYPASS_KEY } from '../decorators/bypass.decorator';


/**
 * 响应拦截器
 * 用于处理响应数据
 * 可以用于处理响应数据,如添加响应头,添加响应体等
 */
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor {
  constructor(private readonly reflector: Reflector) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<ResponseModel<T>> {
    // ==========================
    // 【阶段 1:控制器执行之前】
    // ==========================
    // 这里的代码会立即同步执行。
    // 此时请求刚到达拦截器,还没进控制器。

    // ✅功能1:获取是否需要跳过拦截器
    const bypass = this.reflector.get<boolean>(
      BYPASS_KEY,
      context.getHandler(),
    )

    // 如果在这里直接 return 一个 Observable (例如 return of({error: 'blocked'}))
    // 而不调用 next.handle(),控制器将永远不会执行(短路)。
    // 调用 next.handle() 启动控制器逻辑
    // 它返回一个 Observable,代表控制器未来的执行结果(流)
    if (bypass)
      return next.handle()
    
    // ✅功能2:获取请求对象
    const http = context.switchToHttp()
    const request = http.getRequest<FastifyRequest>()
    // 处理 query 参数,将数组参数转换为数组,如:?a[]=1&a[]=2 => { a: [1, 2] }
    request.query = qs.parse(request.url.split('?').at(1))


    // ✅功能3:调用控制器逻辑
    const response$ = next.handle(); 
    // 【阶段 2:控制器执行之后】
    // ==========================
    // 这里的代码不会立即执行!
    // 它们被注册为 RxJS 的“操作符”,只有当控制器执行完毕并产生数据时,流才会流动到这里。
    return response$.pipe(
      map((data) => {
        console.log('data', data);
        return ResponseModel.success(data);
      }),
    );
  }
}

response.mode.ts

它是生成 响应数据 的一个构造函数;

import { HttpStatus } from "@nestjs/common";

export class ResponseModel<T = any> {
  code: number;
  message: string;
  data?: T;

  constructor(code: number, message: string, data?: T) {
    this.code = code;
    this.message = message;
    this.data = data ?? undefined;
  }

  static success<T>(data?: T) {
    return new ResponseModel(HttpStatus.OK, 'success', data);
  }

  static error(code: number, message: string) {
    return new ResponseModel(code, message, null);
  }
}

bypass.decorator.ts

配置:是否使用 - 拦截器统一响应数据结构功能,亦可解释为 此拦截器功能的开关

import { SetMetadata } from '@nestjs/common';

export const BYPASS_KEY = '__bypass_key__';

/**
 * 当不需要转换成基础返回格式时添加该装饰器
 */
export const Bypass = () => SetMetadata(BYPASS_KEY, true);

SetMetadata

在 NestJS 中,@SetMetadata() 是一个核心装饰器,用于向路由处理器(Controller 中的方法)或控制器类附加自定义的元数据(Metadata)。简单来说,它允许你给代码“打标签”,这些标签可以在运行时被读取,从而实现灵活、声明式的逻辑控制,例如权限校验、日志记录或缓存策略等。

1. 设置元数据

你可以直接在控制器或其方法上使用 @SetMetadata('key', value) 来设置元数据。

  • key: 一个字符串,作为元数据的唯一标识。
  • value: 任意类型的值,是你想要存储的数据。

示例:

import { Controller, Get, SetMetadata } from '@nestjs/common';

@Controller('cats')
export class CatsController {

  // 为单个方法设置元数据
  @Get()
  @SetMetadata('roles', ['admin']) // key 是 'roles', value 是 ['admin']
  findAll() {
    return 'This action returns all cats';
  }

  // 也可以为整个控制器设置元数据
  @SetMetadata('isPublic', true)
  @Get('public')
  findPublic() {
    return 'This is a public route';
  }
}

设置元数据的最佳实践,如上文中我们的写法,通过一个自定义装饰,为控制器 或 方法 设置。

2. 获取元数据

设置的元数据本身是静态的,它的价值在于在运行时被动态读取。这通常在 守卫(Guards)拦截器(Interceptors)管道(Pipes) 中完成,通过注入 Reflector 辅助类来实现。

Reflector 提供了多种方法来读取元数据,最常用的是 get()

以上文中拦截器为例,获取元数据:

// ...
import KEY_NAME from '../****'

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor {
  constructor(private readonly reflector: Reflector) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<ResponseModel<T>> {
    const bypass = this.reflector.get<boolean>(
      KEY_NAME,
      context.getHandler(), // 获取控制器方法的元数据
    )
  }
}

第三步,使用

测试一下,我们再users.controller.ts 中测试使用

import { Bypass } from '~/common/decorators/bypass.decorator';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  @Bypass() // 过滤全局统一响应数据拦截器
  findAll() {
    return this.usersService.findAll();
  }
}

注意:

const bypass = this.reflector.get<boolean>(
  BYPASS_KEY,
  context.getHandler(), // 获取控制器方法的元数据
)

@Bypass 装饰器只能作用在 路由处理方法 上。

总结:

以上就完成了 统一处理接口请求与响应结果 功能的开发,顺便让我们熟悉了 拦截器的用法;

再次回顾一下拦截器的使用场景:

  • 请求参数统一处理:格式转换
  • 响应数据统一 格式化
  • 响应缓存
  • 超时处理
  • 数据序列化/脱敏
❌
❌