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

29 阅读8分钟

与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

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 的实现类,提供权限和角色数据源:

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'];
  }
}

然后在模块注册时注入:

SaTokenModule.forRoot({
  stpInterface: {
    useClass: MyStpInterface,
  },
}),

使用注解进行鉴权

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 进行编程式鉴权操作:

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 };
  }
}

配置项说明

配置项类型默认值说明
tokenNamestring'satoken'Token 名称(Header/Cookie/Body 字段名)
timeoutnumber2592000Token 有效期(秒),-1 为永不过期
activeTimeoutnumber-1临时有效期(秒),-1 表示不启用
isConcurrentbooleantrue是否允许同一账号多地同时登录
maxLoginCountnumber12同一账号最大登录数量
isSharebooleantrue多设备登录时是否共用同一个 Token
tokenStylestring'uuid'Token 风格
tokenPrefixstring''Token 前缀(如 Bearer
autoRenewbooleantrue是否自动续签
jwtSecretKeystring-JWT 密钥(tokenStyle 为 jwt 时必填)
isReadHeaderbooleantrue是否从 Header 读取 Token
isReadCookiebooleantrue是否从 Cookie 读取 Token
isReadBodybooleantrue是否从 Body/Query 读取 Token
isWriteHeaderbooleantrue登录时是否写入 Header
isLogbooleanfalse是否打印操作日志
isColorLogbooleantrue是否打印彩色日志

Cookie 配置

config: {
  cookie: {
    domain: string;     // Cookie 域名
    path: '/';          // Cookie 路径
    secure: boolean;    // 仅 HTTPS
    httpOnly: true;     // HttpOnly
    sameSite: 'lax';    // SameSite 策略
    maxAge: number;     // 过期时间(毫秒)
  },
}

Token 风格

风格说明示例
uuid标准 UUID v4f2b8c4e1-a3d7-4e9f-b5c6-8d2a1e0f3b7a
simple-uuid简化 UUID(去横线)f2b8c4e1a3d74e9fb5c68d2a1e0f3b7a
random-3232位随机字符串xK8mP2qRvLwN5tZjFhCdEgBiAuYsIoJk
random-6464位随机字符串-
random-128128位随机字符串-
jwtJSON Web TokeneyJhbGciOiJIUzI1NiIs...

注解一览

鉴权注解

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

参数装饰器

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

路由拦截

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

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,用于存储账号级别的数据:

// 获取 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 级别的数据:

const tokenSession = await this.stpUtil.getTokenSession(req);

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

二级认证

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

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

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

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

账号封禁

// 封禁账号
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 状态码错误码
NotLoginException40111011
NotPermissionException40311012
NotRoleException40311013
DisableServiceException40311014
NotSafeException40311015

异常响应示例:

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

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

自定义 DAO

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

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[]> { /* ... */ }
}

项目结构

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                # 统一导出入口

开发

# 安装依赖
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(可选)