太强了!Sa-Token 的 NodeJS 版本!
这是由 Sa-Token 社区成员发起的一个项目:xlt-token —— sa-token 的 NodeJS 版本。
对 NodeJS / TypeScript 熟悉的同学速来围观啦。
目前该仓库已完成 1.0.0-rc.1 版本发布,完成度相当不错,单测覆盖率 98%+,195 个测试用例全部通过。源码全部使用 TypeScript 编写,类型定义完整无缺。
开源地址:https://github.com/xiaoLangtou/xlt-token
📱 加群交流:作者已建好 xlt-token 交流群,欢迎大家扫码加入,一起讨论、提需求、报 Bug~

一个为 NestJS 设计的轻量级、功能完备的 Token 认证授权框架:
✨ 核心特性
- 🔐 灵活的 Token 管理 — 支持登录、登出、续签、踢人下线等完整生命周期
- 🌐 多端登录 — 支持同账号多设备同时在线,可配置互踢模式
- 🎨 Token 策略 — 支持多种 token 格式(UUID、Simple UUID、随机字符串)
- 🛡️ 全局守卫 — 黑名单 / 白名单双模式,默认安全
- 🧩 可扩展守卫 — 提供
XltAbstractLoginGuard抽象基类,通过onAuthSuccess/onAuthFail钩子注入业务会话 - 🎯 声明式装饰器 —
@XltIgnore/@XltCheckLogin/@LoginId/@TokenValue/@XltCheckPermission/@XltCheckRole - 🔑 权限 / 角色校验 —
StpPermLogic引擎,支持 AND / OR 模式 + 通配符匹配(user:*) - 🗂️ 会话对象 —
XltSession承载一次登录期间的扩展数据,与 token 同生命周期 - 📜 下线追溯 — 被踢 / 被顶后可查询下线时间和原因
- 💾 内置存储 — 内置内存存储和 Redis 存储实现,开箱即用
- 🔧 零业务依赖 — 纯粹的认证库,不依赖任何业务代码
- 📦 TypeScript — 完整的类型定义
- ⚡ 静态门面 — 提供
StpUtil静态方法,无需注入即可使用 - 🧪 质量保障 — 195 个测试用例(158 单测 + 37 E2E),单测覆盖率 98%+
📦 项目结构
xlt-token/
├── src/
│ ├── auth/ # 认证核心逻辑
│ │ ├── stp-logic.ts # Token 生命周期管理(登录、登出、踢人、续签)
│ │ └── stp-util.ts # 静态门面 StpUtil
│ ├── core/
│ │ └── xlt-token-config.ts # 配置定义
│ ├── perm/ # 权限 / 角色校验
│ │ ├── stp-interface.ts # 业务接口(权限列表、角色列表)
│ │ ├── stp-perm-logic.ts # 校验引擎(AND/OR + 通配符)
│ │ └── perm-pattern-match.ts # 通配符匹配引擎
│ ├── session/
│ │ └── xlt-session.ts # 会话管理
│ ├── guards/ # 守卫层
│ │ ├── xlt-token.guard.ts # 全局 Token 守卫
│ │ └── xlt-abstract-login.guard.ts # 可扩展登录守卫基类
│ ├── decorators/ # 声明式装饰器
│ │ ├── xlt-ignore.decorator.ts
│ │ ├── xlt-check-login.decorator.ts
│ │ ├── login-id.decorator.ts
│ │ ├── token-value.decorator.ts
│ │ ├── xlt-check-permission.decorator.ts
│ │ └── xlt-check-role.decorator.ts
│ ├── store/ # 存储后端
│ │ ├── xlt-token-store.interface.ts # 存储接口
│ │ ├── memory-store.ts # 内存存储(开发环境)
│ │ └── redis-store.ts # Redis 存储(生产环境)
│ ├── token/ # Token 生成策略
│ │ ├── token-strategy.interface.ts
│ │ └── uuid-strategy.ts
│ ├── exceptions/ # 异常定义
│ │ ├── not-login.exception.ts
│ │ ├── not-permission.exception.ts
│ │ └── not-role.exception.ts
│ ├── const/ # 常量定义
│ │ └── index.ts
│ ├── index.ts # 包入口
│ └── xlt-token.module.ts # NestJS 模块入口
├── test/ # 测试用例
│ ├── unit/ # 158 个单元测试
│ └── e2e/ # 37 个 E2E 测试
├── docs/ # 📖 在线文档
├── package.json
└── tsconfig.json
🎯 核心组件
1. StpLogic
核心认证授权逻辑:
- Token 生成、验证、续签和刷新
- Session 管理
- 权限和角色检查
- 多端登录、互踢模式
- 下线原因追溯
2. StpUtil(静态门面)
提供无需注入即可直接调用的静态 API:
import { StpUtil } from 'xlt-token';
// 无需注入,直接调用
const token = await StpUtil.login(userId);
const loginId = await StpUtil.getLoginId(req);- 1
- 2
- 3
- 4
- 5
3. 声明式装饰器
| 装饰器 | 作用 | 参数 |
|---|---|---|
@XltIgnore() |
忽略登录校验(黑名单模式下放行) | 无 |
@XltCheckLogin() |
强制校验登录(白名单模式下开启) | 无 |
@LoginId() |
注入当前登录用户 ID(参数装饰器) | 无 |
@TokenValue() |
注入当前 token 值(参数装饰器) | 无 |
@XltCheckPermission(perms, options?) |
校验权限 | perms: string \| string[],options?: { mode: XltMode } |
@XltCheckRole(roles, options?) |
校验角色 | roles: string \| string[],options?: { mode: XltMode } |
4. 可扩展守卫(XltAbstractLoginGuard)
如果你需要在校验通过后把用户信息加载到 request.user、记录审计日志、使用自己的元数据键(如 @RequireLogin()),请继承 XltAbstractLoginGuard,仅重写你关心的钩子即可。token 校验、异常抛出、默认元数据解析已在基类完成。
生命周期:
canActivate
├─ requiresLogin(ctx) // 可重写:替换元数据策略
│ └─ 否 → 直接放行
├─ stpLogic.checkLogin(request)
├─ !ok → onAuthFail(result, request) // 可重写
│ throw NotLoginException
└─ ok → request.stpLoginId / stpToken 赋值
→ onAuthSuccess(result, request) // 可重写:业务会话加载
5. 权限 / 角色校验引擎
StpPermLogic 支持 AND / OR 两种模式,以及通配符匹配(user:* 可匹配 user:read、user:write 等)。
// AND 模式:全部满足
@XltCheckPermission(['order:read', 'order:write'], { mode: XltMode.AND })
// OR 模式:任一满足
@XltCheckRole(['admin', 'super'], { mode: XltMode.OR })
// 通配符匹配
@XltCheckPermission('order:*')- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
🚀 快速开始
1. 安装
pnpm add xlt-token
# 或
npm install xlt-token
# 或
yarn add xlt-token- 1
- 2
- 3
- 4
- 5
如需使用 Redis 存储,还需安装 redis 包:
pnpm add redis- 1
2. 注册模块
// app.module.ts
import { Module } from '@nestjs/common';
import { XltTokenModule } from 'xlt-token';
@Module({
imports: [
XltTokenModule.forRoot({
isGlobal: true,
config: {
tokenName: 'authorization',
timeout: 2592000, // 30 天
tokenStyle: 'uuid',
tokenPrefix: 'Bearer ',
},
}),
],
})
export class AppModule {}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
3. 用户登录
// auth.service.ts
import { Injectable } from '@nestjs/common';
import { StpLogic } from 'xlt-token';
@Injectable()
export class AuthService {
constructor(private readonly stpLogic: StpLogic) {}
async login(userId: string) {
const token = await this.stpLogic.login(userId);
return { token };
}
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
4. 使用守卫 + 装饰器
// user.controller.ts
import { Controller, Get, Post } from '@nestjs/common';
import { XltIgnore, LoginId } from 'xlt-token';
@Controller('user')
export class UserController {
@XltIgnore() // 忽略登录校验
@Post('login')
async login() {
// 登录逻辑
}
@Get('profile')
async getProfile(@LoginId() loginId: string) {
return { userId: loginId };
}
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
5. 注册全局守卫
// app.module.ts
import { APP_GUARD } from '@nestjs/core';
import { XltTokenGuard } from 'xlt-token';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: XltTokenGuard,
},
],
})
export class AppModule {}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
XltTokenGuard 只做 token 校验并把 loginId / token 挂到 request.stpLoginId / request.stpToken,不涉及业务。
6. 权限 / 角色校验
// stp.service.ts — 实现 StpInterface 业务接口
import { Injectable } from '@nestjs/common';
import { StpInterface } from 'xlt-token';
@Injectable()
export class StpService implements StpInterface {
async getPermissionList(loginId: string): Promise<string[]> {
// 从数据库 / 缓存读取
return ['user:read', 'user:write', 'order:*'];
}
async getRoleList(loginId: string): Promise<string[]> {
return ['admin'];
}
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
// 在 Controller 上使用
import { XltCheckPermission, XltCheckRole, XltMode } from 'xlt-token';
@Controller('order')
export class OrderController {
@XltCheckPermission('order:read') // 单一权限
@Get()
list() {}
@XltCheckPermission(['order:read', 'order:write'],
{ mode: XltMode.AND }) // 全部满足
@Post()
create() {}
@XltCheckRole(['admin', 'super'],
{ mode: XltMode.OR }) // 任一满足
@Delete(':id')
remove() {}
@XltCheckPermission('order:*') // 通配符匹配
@Patch(':id')
update() {}
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
权限校验失败抛出 NotPermissionException(HTTP 403),角色校验失败抛出 NotRoleException(HTTP 403)。
🗂️ 会话管理(XltSession)
每个登录账号关联一个 XltSession,用于存储登录期间的扩展数据(昵称、最近 IP、扩展字段等)。生命周期与 token 一致。
import { StpUtil } from 'xlt-token';
// 写入
const session = StpUtil.getSession(loginId);
await session.set('nickname', 'xlt');
await session.set('lastLoginIp', '127.0.0.1');
// 读取
const nickname = await session.get<string>('nickname');
// 其他方法
await session.has('nickname'); // boolean
await session.remove('nickname');
const keys = await session.keys(); // string[]
await session.clear();- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
📜 下线原因追溯
被踢 / 被顶后,旧 token 失效。可查询下线原因:
const record = await StpUtil.getOfflineReason(token);
// { reason: 'KICK_OUT' | 'BE_REPLACED', time: 1714112400000 }- 1
- 2
⚠️ 异常处理
库提供三种业务异常,建议在全局 ExceptionFilter 中统一处理:
| 异常 | HTTP 状态 | 触发场景 |
|---|---|---|
NotLoginException |
401 | 未登录 / token 无效 / 被顶 / 被踢 / 冻结 / 超时 |
NotPermissionException |
403 | @XltCheckPermission 校验失败 |
NotRoleException |
403 | @XltCheckRole 校验失败 |
NotLoginException 提供了 NotLoginType 常量用于区分登录失败场景:
import { NotLoginException, NotLoginType } from 'xlt-token';
try {
await stpLogic.checkLogin(req);
} catch (e) {
if (e instanceof NotLoginException) {
switch (e.message) {
case NotLoginType.NOT_TOKEN: // 请求中没 token
break;
case NotLoginType.INVALID_TOKEN: // token 在服务端找不到
break;
case NotLoginType.TOKEN_TIMEOUT: // token 已过期
break;
case NotLoginType.TOKEN_FREEZE: // 临时活跃过期
break;
case NotLoginType.BE_REPLACED: // 被顶号
break;
case NotLoginType.KICK_OUT: // 被踢下线
break;
}
}
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
💾 使用 Redis 存储
库已内置 RedisStore 实现,只需提供 Redis 客户端即可使用:
import { Module } from '@nestjs/common';
import { XltTokenModule, RedisStore, XLT_REDIS_CLIENT } from 'xlt-token';
import { createClient } from 'redis';
@Module({
imports: [
XltTokenModule.forRoot({
store: { useClass: RedisStore },
providers: [
{
provide: XLT_REDIS_CLIENT,
useFactory: async () => {
const client = createClient({
url: 'redis://localhost:6379',
});
await client.connect();
return client;
},
},
],
}),
],
})
export class AppModule {}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
🔧 自定义 Store
如需实现自定义存储,实现 XltTokenStore 接口即可:
import { XltTokenStore } from 'xlt-token';
export class CustomStore implements XltTokenStore {
async get(key: string): Promise<string | null> { /* ... */ }
async set(key: string, value: string, timeoutSec: number): Promise<void> { /* ... */ }
async delete(key: string): Promise<void> { /* ... */ }
async has(key: string): Promise<boolean> { /* ... */ }
async update(key: string, value: string): Promise<void> { /* ... */ }
async updateTimeout(key: string, timeoutSec: number): Promise<void> { /* ... */ }
async getTimeout(key: string): Promise<number> { /* ... */ }
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
🎛️ 配置选项
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
tokenName |
string |
'authorization' |
HTTP header / cookie / query 中读取 token 的键名 |
timeout |
number |
2592000(30 天) |
token 有效期(秒) |
activeTimeout |
number |
-1 |
滑动过期秒数,-1 表示不启用 |
isConcurrent |
boolean |
true |
是否允许同账号多端同时在线 |
isShare |
boolean |
true |
同账号多次登录是否共享同一 token |
tokenStyle |
'uuid' \| 'simple-uuid' \| 'random-32' |
'uuid' |
token 格式 |
isReadHeader |
boolean |
true |
是否从 HTTP Header 读取 token |
isReadCookie |
boolean |
false |
是否从 Cookie 读取 |
isReadQuery |
boolean |
false |
是否从 URL Query 读取 |
tokenPrefix |
string |
'Bearer ' |
Header 中 token 的前缀(读取时自动剥离) |
defaultCheck |
boolean |
true |
全局守卫默认模式:true=黑名单,false=白名单 |
📖 核心 API
StpLogic / StpUtil
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
login(loginId, options?) |
loginId: string \| number,options: { timeout?, device?, token? } |
Promise<string> |
登录,返回 token |
logout(token) |
token: string |
Promise<boolean \| null> |
登出(通过 token) |
logoutByLoginId(loginId) |
loginId: string |
Promise<boolean \| null> |
登出(通过 loginId) |
kickout(loginId) |
loginId: string |
Promise<boolean \| null> |
踢人下线 |
renewTimeout(token, timeout) |
token: string,timeout: number |
Promise<boolean \| null> |
续签 token |
isLogin(req) |
req: Request |
Promise<boolean> |
判断是否登录 |
checkLogin(req) |
req: Request |
Promise<{ ok, loginId?, token?, reason? }> |
校验登录(未登录抛异常) |
getTokenValue(req) |
req: Request |
Promise<string \| null> |
获取 token 值 |
getLoginId(req) |
req: Request |
Promise<string> |
获取当前登录用户 ID |
getSession(loginId) |
loginId: string |
XltSession |
获取会话对象 |
getOfflineReason(token) |
token: string |
Promise<{ reason, time }> |
获取下线原因 |
📣 小结
xlt-token 作为 Sa-Token 在 NodeJS / NestJS 生态的移植版本,在 API 设计上非常接近 Java 原版的使用体验,同时充分利用了 TypeScript 的类型系统和 NestJS 的装饰器 / 守卫机制,做到了大道至简、开箱即用。
对于使用 NestJS 开发后端项目、又需要一套成熟的认证授权方案的小伙伴来说,这是一个非常值得关注的项目。195 个测试用例 + 98% 的单测覆盖率,质量方面也让人放心 👍
🔗 开源地址
感兴趣的同学可以来关注一波:
👉 https://github.com/xiaoLangtou/xlt-token
📖 在线文档:https://xiaolangtou.github.io/xlt-token/
📱 还没有加群? 扫下方二维码加入 xlt-token 交流群,和作者面对面交流~

鲁公网安备37011202002956号