sa-token-doc/blog/special/tai-qiang-le-sa-token-de-nodejs-ban-ben-2.html
2026-05-16特殊文章
这是由 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 认证授权框架:
XltAbstractLoginGuard 抽象基类,通过 onAuthSuccess / onAuthFail 钩子注入业务会话@XltIgnore / @XltCheckLogin / @LoginId / @TokenValue / @XltCheckPermission / @XltCheckRoleStpPermLogic 引擎,支持 AND / OR 模式 + 通配符匹配(user:*)XltSession 承载一次登录期间的扩展数据,与 token 同生命周期StpUtil 静态方法,无需注入即可使用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
核心认证授权逻辑:
提供无需注入即可直接调用的静态 API:
import { StpUtil } from 'xlt-token';
// 无需注入,直接调用
const token = await StpUtil.login(userId);
const loginId = await StpUtil.getLoginId(req);
| 装饰器 | 作用 | 参数 |
|---|---|---|
@XltIgnore() | 忽略登录校验(黑名单模式下放行) | 无 |
@XltCheckLogin() | 强制校验登录(白名单模式下开启) | 无 |
@LoginId() | 注入当前登录用户 ID(参数装饰器) | 无 |
@TokenValue() | 注入当前 token 值(参数装饰器) | 无 |
@XltCheckPermission(perms, options?) | 校验权限 | perms: string | string[],options?: { mode: XltMode } |
@XltCheckRole(roles, options?) | 校验角色 | roles: string | string[],options?: { mode: XltMode } |
如果你需要在校验通过后把用户信息加载到 request.user、记录审计日志、使用自己的元数据键(如 @RequireLogin()),请继承 XltAbstractLoginGuard,仅重写你关心的钩子即可。token 校验、异常抛出、默认元数据解析已在基类完成。
生命周期:
canActivate
├─ requiresLogin(ctx) // 可重写:替换元数据策略
│ └─ 否 → 直接放行
├─ stpLogic.checkLogin(request)
├─ !ok → onAuthFail(result, request) // 可重写
│ throw NotLoginException
└─ ok → request.stpLoginId / stpToken 赋值
→ onAuthSuccess(result, request) // 可重写:业务会话加载
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:*')
pnpm add xlt-token
# 或
npm install xlt-token
# 或
yarn add xlt-token
如需使用 Redis 存储,还需安装 redis 包:
pnpm add redis
// 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 {}
// 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 };
}
}
// 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 };
}
}
// app.module.ts
import { APP_GUARD } from '@nestjs/core';
import { XltTokenGuard } from 'xlt-token';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: XltTokenGuard,
},
],
})
export class AppModule {}
XltTokenGuard 只做 token 校验并把 loginId / token 挂到 request.stpLoginId / request.stpToken,不涉及业务。
// 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'];
}
}
// 在 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() {}
}
权限校验失败抛出 NotPermissionException(HTTP 403 ),角色校验失败抛出 NotRoleException(HTTP 403 )。
每个登录账号关联一个 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();
被踢 / 被顶后,旧 token 失效。可查询下线原因:
const record = await StpUtil.getOfflineReason(token);
// { reason: 'KICK_OUT' | 'BE_REPLACED', time: 1714112400000 }
库提供三种业务异常,建议在全局 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;
}
}
}
库已内置 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 {}
如需实现自定义存储,实现 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> { /* ... */ }
}
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
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=白名单 |
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
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 交流群,和作者面对面交流~