在nest.js中我想把Java的Sa-Token搬来

与NestJS相伴的两年,我从磕磕绊绊到熟练运用装饰器、依赖注入搭建企业级项目,愈发偏爱其严谨优雅。但痛点明显:Nest生态缺少像Java版Sa-Token那样的一站式权限框架,现存方案要么功能简陋,要么配置繁琐,或生硬套用其他框架思路,使用别扭。

Sa-Token是我Java开发时深耕的权限框架,其简洁易用令人印象深刻:几行代码实现登录认证,灵活注解管控角色权限,会话踢人、限流等刚需功能开箱即用,省去大量重复开发。每次用Nest做权限模块,我都盼着它有原生适配版本。

这个念头埋藏已久,直到接手多模块Nest项目,权限模块的重复开发与隐藏bug让我内耗严重。那一刻我下定决心:将Sa-Token核心思想融入Nest架构,做一个懂Nest开发者的权限框架。

目前还在本地使用yalc 构建npm包link,没有发布到npm仓库等待v1.0版本功能测试完毕

目前已经实现的功能

  • 登录鉴权 --- 单端登录、多端登录、同端互斥登录、多端共用 Token
  • 权限校验 --- 注解式/编程式权限验证,支持 AND/OR 模式
  • 角色管理 --- 角色校验,支持多角色组合判断
  • Session 管理 --- Account-Session / Token-Session 双层会话体系
  • 踢人下线 --- 根据账号或设备踢出在线用户
  • 账号封禁 --- 按服务类型封禁,支持封禁等级与时间
  • 二级认证 --- 敏感操作二次验证(如修改密码、转账等)
  • Token 策略 --- 支持 UUID、Simple-UUID、Random、JWT 等多种 Token 风格
  • 路由拦截 --- 声明式路由匹配与权限校验链
  • 持久化 --- 内置内存实现,可选 Redis 实现(可自定义 DAO)
  • 自动续签 --- 可配置的 Token 活跃超时自动续期

AppModule 中引入 SaTokenModule

typescript 复制代码
import { Module } from '@nestjs/common';
import { SaTokenModule } from '@sa-token/nestjs';
import { SaTokenDaoRedis } from '@sa-token/nestjs';

@Module({
  imports: [
    SaTokenModule.forRoot({
      config: {
        timeout: 2592000,       // Token 有效期(秒),默认30天
        tokenStyle: 'uuid',     // Token 风格:uuid | simple-uuid | random-32 | random-64 | random-128 | jwt
        isConcurrent: true,     // 是否允许同一账号多地同时登录
        autoRenew: true,        // 是否自动续签
      },
      // 使用 Redis 存储(可选,默认使用内存存储)
      dao: {
        useClass: SaTokenDaoRedis,
      },
    }),
  ],
})
export class AppModule {}

实现权限接口

创建 StpInterface 的实现类,提供权限和角色数据源:

typescript 复制代码
import { Injectable } from '@nestjs/common';
import { StpInterface } from '@sa-token/nestjs';

@Injectable()
export class MyStpInterface implements StpInterface {
  async getPermissionList(loginId: string | number, loginType: string): Promise<string[]> {
    // 从数据库查询用户权限列表
    return ['user:add', 'user:delete', 'user:update'];
  }

  async getRoleList(loginId: string | number, loginType: string): Promise<string[]> {
    // 从数据库查询用户角色列表
    return ['admin', 'super-admin'];
  }
}

然后在模块注册时注入:

css 复制代码
SaTokenModule.forRoot({
  stpInterface: {
    useClass: MyStpInterface,
  },
}),

使用注解进行鉴权

less 复制代码
import { Controller, Get, Post, Body } from '@nestjs/common';
import {
  SaCheckLogin,
  SaCheckPermission,
  SaCheckRole,
  SaCheckSafe,
  SaIgnore,
  LoginId,
  TokenValue,
} from '@sa-token/nestjs';

@Controller('user')
export class UserController {

  @Get('info')
  @SaCheckLogin()
  async getInfo(@LoginId() loginId: string) {
    return { loginId };
  }

  @Post('add')
  @SaCheckPermission('user:add')
  async addUser(@Body() body: any) {
    // 需要 user:add 权限
  }

  @Delete('delete')
  @SaCheckRole('admin')
  async deleteUser() {
    // 需要 admin 角色
  }

  @Post('password')
  @SaCheckSafe('update-password')   // 二级认证
  async updatePassword(@Body() body: any) {
    // 修改密码需要先通过二级认证
  }

  @Get('public')
  @SaIgnore()
  async publicData() {
    // 无需登录即可访问
  }

  @Get('or-permission')
  @SaCheckPermissionOr('user:add', 'user:update')
  async orPermission() {
    // 满足任一权限即可
  }
}

编程式调用

注入 StpUtil 进行编程式鉴权操作:

less 复制代码
import { Controller, Get, Req, Res } from '@nestjs/common';
import { StpUtil, SaLoginModel } from '@sa-token/nestjs';

@Controller('auth')
export class AuthController {
  constructor(private readonly stpUtil: StpUtil) {}

  @Post('login')
  async login(@Req() req: any, @Res() res: any, @Body() body: any) {
    const { username, password } = body;

    // 校验账号密码...
    const userId = await this.verifyUser(username, password);

    // 执行登录
    const tokenValue = await this.stpUtil.login(userId, req, res, {
      device: 'PC',
      tag: 'online',
    });

    return { token: tokenValue };
  }

  @Post('logout')
  async logout(@Req() req: any, @Res() res: any) {
    await this.stpUtil.logout(req, res);
    return { msg: '注销成功' };
  }

  @Get('check')
  async check(@Req() req: any) {
    const isLogin = await this.stpUtil.isLogin(req);
    const loginId = await this.stpUtil.getLoginIdDefaultNull(req);
    return { isLogin, loginId };
  }
}

配置项说明

配置项 类型 默认值 说明
tokenName string 'satoken' Token 名称(Header/Cookie/Body 字段名)
timeout number 2592000 Token 有效期(秒),-1 为永不过期
activeTimeout number -1 临时有效期(秒),-1 表示不启用
isConcurrent boolean true 是否允许同一账号多地同时登录
maxLoginCount number 12 同一账号最大登录数量
isShare boolean true 多设备登录时是否共用同一个 Token
tokenStyle string 'uuid' Token 风格
tokenPrefix string '' Token 前缀(如 Bearer
autoRenew boolean true 是否自动续签
jwtSecretKey string - JWT 密钥(tokenStyle 为 jwt 时必填)
isReadHeader boolean true 是否从 Header 读取 Token
isReadCookie boolean true 是否从 Cookie 读取 Token
isReadBody boolean true 是否从 Body/Query 读取 Token
isWriteHeader boolean true 登录时是否写入 Header
isLog boolean false 是否打印操作日志
isColorLog boolean true 是否打印彩色日志
typescript 复制代码
config: {
  cookie: {
    domain: string;     // Cookie 域名
    path: '/';          // Cookie 路径
    secure: boolean;    // 仅 HTTPS
    httpOnly: true;     // HttpOnly
    sameSite: 'lax';    // SameSite 策略
    maxAge: number;     // 过期时间(毫秒)
  },
}

Token 风格

风格 说明 示例
uuid 标准 UUID v4 f2b8c4e1-a3d7-4e9f-b5c6-8d2a1e0f3b7a
simple-uuid 简化 UUID(去横线) f2b8c4e1a3d74e9fb5c68d2a1e0f3b7a
random-32 32位随机字符串 xK8mP2qRvLwN5tZjFhCdEgBiAuYsIoJk
random-64 64位随机字符串 -
random-128 128位随机字符串 -
jwt JSON Web Token eyJhbGciOiJIUzI1NiIs...

注解一览

鉴权注解

注解 说明 参数
@SaCheckLogin() 登录校验 type? --- 登录类型
@SaCheckPermission(...perms) 权限校验(AND) 权限码列表
@SaCheckPermissionOr(...perms) 权限校验(OR) 权限码列表
@SaCheckRole(...roles) 角色校验(AND) 角色标识列表
@SaCheckRoleOr(...roles) 角色校验(OR) 角色标识列表
@SaCheckSafe(service?) 二级认证 服务名称
@SaIgnore() 忽略认证 -

参数装饰器

装饰器 说明
@LoginId() 获取当前登录 ID
@TokenValue() 获取当前 Token 值

路由拦截

使用声明式路由规则配置全局鉴权:

javascript 复制代码
import { SaRouter } from '@sa-token/nestjs';

SaTokenModule.forRoot({
  routerConfig: (router: SaRouter) => {
    router
      .match('/api/**')           // 匹配所有 /api/** 路径
      .notMatch('/api/public/**') // 排除公开接口
      .checkLogin();              // 要求登录

    router
      .match('/api/admin/**')
      .checkLogin()
      .checkRole('admin');        // 要求 admin 角色

    router
      .match('/api/user/**')
      .notMatch('/api/user/info')
      .check(async (req, res, stpUtil) => {
        // 自定义校验逻辑
        await stpUtil.checkPermission(req, 'user:read');
      });
  },
});

Session 操作

Account-Session(账号会话)

每个登录账号对应一个 Session,用于存储账号级别的数据:

csharp 复制代码
// 获取 Session
const session = await this.stpUtil.getSessionByLoginId(10001);

// 存取数据
session.set('nickname', '张三');
session.set('avatar', '/avatar/10001.png');

const nickname = session.get<string>('nickname');

Token-Session(Token 会话)

每个 Token 对应一个独立 Session,用于存储 Token 级别的数据:

csharp 复制代码
const tokenSession = await this.stpUtil.getTokenSession(req);

tokenSession.set('lastIp', req.ip);
tokenSession.set('loginTime', Date.now());

二级认证

适用于敏感操作的二次验证场景:

less 复制代码
// 1. 开启二级认证(如输入密码/短信验证后)
await this.stpUtil.openSafe(req, 'transfer', 300); // 300秒有效

// 2. 在需要保护的方法上加注解
@SaCheckSafe('transfer')
async transferMoney(@Body() body: any) {
  // 已通过二级认证才能执行
}

// 3. 关闭二级认证
await this.stpUtil.closeSafe(req, 'transfer');

账号封禁

csharp 复制代码
// 封禁账号
await this.stpUtil.disable(10001, 'comment', 1, 3600); // 封禁评论功能1小时

// 判断是否被封禁
const isDisable = await this.stpUtil.isDisable(10001, 'comment');

// 获取剩余封禁时间
const time = await this.stpUtil.getDisableTime(10001, 'comment');

// 解封
await this.stpUtil.untieDisable(10001, 'comment');

异常处理

框架内置了全局异常过滤器 SaTokenExceptionFilter,自动捕获并格式化异常响应:

异常 HTTP 状态码 错误码
NotLoginException 401 11011
NotPermissionException 403 11012
NotRoleException 403 11013
DisableServiceException 403 11014
NotSafeException 403 11015

异常响应示例:

json 复制代码
// 未登录
{
  "code": 11011,
  "message": "未能读取到有效Token",
  "data": null,
  "loginType": "login",
  "type": "-1"
}

// 缺少权限
{
  "code": 11012,
  "message": "缺少权限: user:delete",
  "data": null,
  "permission": "user:delete"
}

自定义 DAO

实现 SaTokenDao 接口即可对接任意存储:

typescript 复制代码
import { Injectable } from '@nestjs/common';
import { SaTokenDao } from '@sa-token-nestjs';

@Injectable()
export class CustomDao implements SaTokenDao {
  async get(key: string): Promise<string | null> { /* ... */ }
  async set(key: string, value: string, timeout: number): Promise<void> { /* ... */ }
  async update(key: string, value: string): Promise<void> { /* ... */ }
  async delete(key: string): Promise<void> { /* ... */ }
  async getTimeout(key: string): Promise<number> { /* ... */ }
  async updateTimeout(key: string, timeout: number): Promise<void> { /* ... */ }
  async getObject(key: string): Promise<any> { /* ... */ }
  async setObject(key: string, object: any, timeout: number): Promise<void> { /* ... */ }
  async updateObject(key: string, object: any): Promise<void> { /* ... */ }
  async deleteObject(key: string): Promise<void> { /* ... */ }
  async getObjectTimeout(key: string): Promise<number> { /* ... */ }
  async updateObjectTimeout(key: string, timeout: number): Promise<void> { /* ... */ }
  async searchData(prefix: string, keyword: string, start: number, size: number, sortType: boolean): Promise<string[]> { /* ... */ }
}

项目结构

csharp 复制代码
src/
├── auth/                    # 认证逻辑
│   ├── stp-logic.ts         # 核心鉴权引擎(StpLogic)
│   ├── stp-util.ts          # 便捷工具类(StpUtil)
│   └── sa-login-model.ts    # 登录参数模型
├── core/                    # 核心定义
│   ├── sa-token-config.ts   # 配置接口与默认值
│   └── constants.ts         # 常量与元数据键
├── dao/                     # 持久化层
│   ├── sa-token-dao.interface.ts  # DAO 接口定义
│   ├── memory-dao.ts               # 内存实现(默认)
│   └── redis-dao.ts                # Redis 实现
├── decorators/             # 装饰器
│   └── index.ts            # 鉴权注解 & 参数装饰器
├── exception/              # 异常体系
│   └── sa-token-exception.ts       # 异常类定义
├── filters/                # 过滤器
│   └── sa-token-exception.filter.ts # 全局异常过滤器
├── guards/                 # 守卫
│   └── sa-token.guard.ts   # 全局鉴权守卫
├── permission/             # 权限接口
│   └── stp-interface.ts    # 权限/角色数据源接口
├── router/                 # 路由拦截
│   ├── sa-router.ts        # 路由匹配引擎
│   └── sa-router.middleware.ts # 路由中间件
├── session/                # 会话管理
│   └── sa-session.ts       # Session 实现
├── token/                  # Token 策略
│   ├── token-strategy.interface.ts # 策略接口
│   ├── token-strategy-factory.ts   # 策略工厂
│   ├── uuid-strategy.ts            # UUID 策略
│   ├── simple-uuid-strategy.ts     # Simple UUID 策略
│   ├── random-strategy.ts          # Random 策略
│   └── jwt-strategy.ts             # JWT 策略
├── sa-token.module.ts      # 模块定义
└── index.ts                # 统一导出入口

开发

bash 复制代码
# 安装依赖
npm install

# 构建
npm run build

# 监听模式构建
npm run build:watch

# 代码检查
npm run lint

# 格式化
npm run format

# 测试
npm test

# 本地开发(yalc 链接)
npm run quick-dev

依赖要求

| 依赖 | 版本要求 |
|----------------|---------------|---|----------|
| Node.js | >= 16.0.0 |
| @nestjs/common | ^9.0.0 | | ^10.0.0 |
| @nestjs/core | ^9.0.0 | | ^10.0.0 |
| uuid | ^9.0.0(内置依赖) |
| ioredis | ^5.3.0(可选) |
| jsonwebtoken | ^9.0.2(可选) |

相关推荐
Sheldon一蓑烟雨任平生2 小时前
grid(一文读懂 css 网格布局)
前端·css·grid·grid-template·现代css·css 网格布局
神奇小汤圆2 小时前
MySQL CPU飙到680%:一次「僵尸查询」引发的雪崩
后端
砍材农夫2 小时前
Hermes 搭建可视化web-dashboard界面
前端·人工智能
Z_Wonderful2 小时前
Qiankun 子应用数据互通 + 资源共享 完整方案(React+Vue)
前端·vue.js·react.js
你的牧游哥2 小时前
Electron核心api详解
前端·javascript·electron
浪客川2 小时前
【百例RUST - 006】一文理解所有权和切片
开发语言·后端·rust
渣渣xiong2 小时前
从零开始:前端转型AI agent直到就业第十二天-第十三天
前端·人工智能
05Nuyoah2 小时前
CSS 基础认知和基础选择器
前端·javascript·css·node.js
香香甜甜的辣椒炒肉2 小时前
Spring JDBC 万能模板
java·后端·spring