太强了!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~

xlt-token 交流群

一个为 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:readuser: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 \| numberoptions: { 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: stringtimeout: 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 交流群,和作者面对面交流~

xlt-token 交流群

← Sa-Token 第 10000 个star 里程碑,感谢三年来所有关注 Sa-Token 的小伙伴!

当前博客已开放版权,所有人均可免费转载,无需联系 Sa-Token 团队获取授权。只需要在转载时保留底部 Sa-Token 官网链接 + 开源仓库链接即可。

Copyright ©2026 Sa-Token java 权限认证 | sa-token.com | 鲁ICP备18046274号-5 | 鲁公网安备37011202002956号