nestjs-sa-token
v1.0.3
Published
A powerful and lightweight authorization framework for NestJS
Maintainers
Readme
SA-Token for NestJS
轻量级、强大的 NestJS 权限认证框架
简介
SA-Token for NestJS 是一个基于 Sa-Token 设计理念的 NestJS 权限认证框架,提供了完整的登录鉴权、权限校验、角色管理、Session 管理、账号封禁、二级认证等功能。支持内存和 Redis 两种持久化方式,可灵活扩展。
特性
- 登录鉴权 — 单端登录、多端登录、同端互斥登录、多端共用 Token
- 权限校验 — 注解式/编程式权限验证,支持 AND/OR 模式
- 角色管理 — 角色校验,支持多角色组合判断
- Session 管理 — Account-Session / Token-Session 双层会话体系
- 踢人下线 — 根据账号或设备踢出在线用户
- 账号封禁 — 按服务类型封禁,支持封禁等级与时间
- 二级认证 — 敏感操作二次验证(如修改密码、转账等)
- Token 策略 — 支持 UUID、Simple-UUID、Random、JWT 等多种 Token 风格
- 路由拦截 — 声明式路由匹配与权限校验链
- 持久化 — 内置内存实现,可选 Redis 实现(可自定义 DAO)
- 自动续签 — 可配置的 Token 活跃超时自动续期
快速开始
安装
npm install nestjs-sa-token如需使用 Redis 持久化:
npm install ioredis注册模块
在 AppModule 中引入 SaTokenModule:
// app.module.ts
import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { SaTokenModule, SaRouterMiddleware, SaTokenDaoRedis } from 'nestjs-sa-token';
import { AppController } from './app.controller';
import { RedisModule } from './redis/redis.module';
import Redis from 'ioredis';
import { StpInterfaceImpl } from './auth/stp-interface.impl';
@Module({
imports: [
// 1. 配置模块
ConfigModule.forRoot({
isGlobal: true,
}),
// 2. Redis 模块
RedisModule,
// 3. SA-Token 模块
SaTokenModule.forRoot({
stpInterface: {
useClass: StpInterfaceImpl,
},
config: {
tokenName: 'Authorization',
tokenPrefix: 'Bearer',
timeout: 2592000,
activeTimeout: -1,
isConcurrent: true,
isShare: false,
maxLoginCount: 5,
tokenStyle: 'jwt',
jwtSecretKey: 'your_jwt_secret_key',
isLog: false,
},
interceptors: {
responseTransform: true, // 成功响应统一包装
requestLog: true, // 请求耗时日志
},
routerConfig: (router) => {
// ✅ 匹配所有路径
router.match('/**')
.notMatch(
'/', // ✅ 排除根路径
'/debug/**', // 排除调试
'/api/auth/test',
)
.checkLogin();
},
dao: {
useFactory: (redis: Redis) => {
console.log('📦 创建 SaTokenDaoRedis');
return new SaTokenDaoRedis(redis);
},
inject: ['REDIS_CLIENT'],
},
global: true,
}),
],
controllers: [AppController],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(SaRouterMiddleware).forRoutes('*');
}
}实现权限接口
创建 StpInterface 的实现类,提供权限和角色数据源:
import { Injectable } from '@nestjs/common';
import { StpInterface } from 'nestjs-sa-token';
@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 'nestjs-sa-token';
@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 'nestjs-sa-token';
@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 配置
config: {
cookie: {
domain: string; // Cookie 域名
path: '/'; // Cookie 路径
secure: boolean; // 仅 HTTPS
httpOnly: true; // HttpOnly
sameSite: 'lax'; // SameSite 策略
maxAge: number; // 过期时间(毫秒)
},
}redis 配置
import { Module, Global } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Redis from 'ioredis';
const redisProvider = {
provide: 'REDIS_CLIENT',
useFactory: (configService: ConfigService) => {
const redis = new Redis({
host: configService.get('REDIS_HOST', '127.0.0.1'),
port: configService.get('REDIS_PORT', 6379),
password: configService.get('REDIS_PASSWORD'),
db: configService.get('REDIS_DB', 0),
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
},
});
redis.on('connect', () => {
console.log('✅ Redis 连接成功');
});
redis.on('ready', () => {
console.log('✅ Redis 已就绪');
});
redis.on('error', (err) => {
console.error('❌ Redis 错误:', err.message);
});
redis.on('close', () => {
console.log('⚠️ Redis 连接已关闭');
});
return redis;
},
inject: [ConfigService],
};
@Global() // 设为全局模块
@Module({
providers: [redisProvider],
exports: ['REDIS_CLIENT'], // 导出供其他模块使用
})
export class RedisModule {}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 值 |
路由拦截
使用声明式路由规则配置全局鉴权:
import { SaRouter } from 'nestjs-sa-token';
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 状态码 | 错误码 |
| ------------------------- | ----------- | ------ |
| NotLoginException | 401 | 11011 |
| NotPermissionException | 403 | 11012 |
| NotRoleException | 403 | 11013 |
| DisableServiceException | 403 | 11014 |
| NotSafeException | 403 | 11015 |
异常响应示例:
// 未登录
{
"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依赖要求
| 依赖 | 版本要求 | | -------------- | ------------------- | | 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(可选) |
联系本人
未来规划
1 实现更多对齐java sa-token 2 完成api doc文档开发
