与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 |
是否打印彩色日志 |
Cookie 配置
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(可选) |