Back to Sa Token

Sa-Token

sa-token-doc/blog/special/tai-qiang-le-sa-token-de-nodejs-ban-ben-2.html

1.46.016.8 KB
Original Source

太强了!Sa-Token 的 NodeJS 版本!

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 认证授权框架:

✨ 核心特性

  • 🔐 灵活的 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:

typescript
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 等)。

typescript
// 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. 安装

bash
pnpm add xlt-token
# 或
npm install xlt-token
# 或
yarn add xlt-token
  • 1
  • 2
  • 3
  • 4
  • 5

如需使用 Redis 存储,还需安装 redis 包:

bash
pnpm add redis
  • 1

2. 注册模块

typescript
// 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. 用户登录

typescript
// 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. 使用守卫 + 装饰器

typescript
// 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. 注册全局守卫

typescript
// 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. 权限 / 角色校验

typescript
// 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
typescript
// 在 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 一致。

typescript
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 失效。可查询下线原因:

typescript
const record = await StpUtil.getOfflineReason(token);
// { reason: 'KICK_OUT' | 'BE_REPLACED', time: 1714112400000 }
  • 1
  • 2

⚠️ 异常处理

库提供三种业务异常,建议在全局 ExceptionFilter 中统一处理:

异常HTTP 状态触发场景
NotLoginException401未登录 / token 无效 / 被顶 / 被踢 / 冻结 / 超时
NotPermissionException403@XltCheckPermission 校验失败
NotRoleException403@XltCheckRole 校验失败

NotLoginException 提供了 NotLoginType 常量用于区分登录失败场景:

typescript
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 客户端即可使用:

typescript
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 接口即可:

typescript
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

🎛️ 配置选项

字段类型默认值说明
tokenNamestring'authorization'HTTP header / cookie / query 中读取 token 的键名
timeoutnumber2592000(30 天)token 有效期(秒)
activeTimeoutnumber-1滑动过期秒数,-1 表示不启用
isConcurrentbooleantrue是否允许同账号多端同时在线
isSharebooleantrue同账号多次登录是否共享同一 token
tokenStyle'uuid' | 'simple-uuid' | 'random-32''uuid'token 格式
isReadHeaderbooleantrue是否从 HTTP Header 读取 token
isReadCookiebooleanfalse是否从 Cookie 读取
isReadQuerybooleanfalse是否从 URL Query 读取
tokenPrefixstring'Bearer 'Header 中 token 的前缀(读取时自动剥离)
defaultCheckbooleantrue全局守卫默认模式:true=黑名单,false=白名单

📖 核心 API

StpLogic / StpUtil

方法参数返回值说明
login(loginId, options?)loginId: string | numberoptions: { timeout?, device?, token? }Promise<string>登录,返回 token
logout(token)token: stringPromise<boolean | null>登出(通过 token)
logoutByLoginId(loginId)loginId: stringPromise<boolean | null>登出(通过 loginId)
kickout(loginId)loginId: stringPromise<boolean | null>踢人下线
renewTimeout(token, timeout)token: stringtimeout: numberPromise<boolean | null>续签 token
isLogin(req)req: RequestPromise<boolean>判断是否登录
checkLogin(req)req: RequestPromise<{ ok, loginId?, token?, reason? }>校验登录(未登录抛异常)
getTokenValue(req)req: RequestPromise<string | null>获取 token 值
getLoginId(req)req: RequestPromise<string>获取当前登录用户 ID
getSession(loginId)loginId: stringXltSession获取会话对象
getOfflineReason(token)token: stringPromise<{ 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 交流群,和作者面对面交流~

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