@gulibs/tegg-guard
v0.0.7
Published
Enhanced authentication and authorization plugin for Egg.js framework with JWT, RBAC, rate limiting, and audit logging
Maintainers
Readme
@gulibs/tegg-guard
增强的 Egg.js 框架认证插件,支持 JWT、会话管理和 SuperTokens 风格的功能。
特性
核心认证
- ✅ JWT 认证 - 签名、验证和解码 JWT 令牌
- ✅ 刷新令牌 - 支持自动刷新和轮换
- ✅ 会话管理 - 基于 Redis 或内存的会话存储
- ✅ 令牌撤销 - 支持黑名单和撤销(Redis 或内存)
- ✅ 令牌绑定 - 将令牌绑定到 IP 地址或来源以增强安全性
- ✅ 多密钥支持 - 支持密钥轮换,实现无缝密钥更新
- ✅ 灵活存储 - 在 header 或 cookie 中存储令牌
高级功能
- ✅ RBAC(基于角色的访问控制) - 使用
@Roles()装饰器进行角色检查 - ✅ 权限管理 - 使用
@Permissions()装饰器进行细粒度权限控制 - ✅ 角色层级 - 支持角色继承(如 admin > moderator > user)
- ✅ 速率限制 - 使用
@RateLimit()装饰器限制 API 请求频率 - ✅ 审计日志 - 使用
@AuditLog()装饰器自动记录用户操作 - ✅ Context 扩展 - 通过
ctx.guard.*便捷访问用户信息和会话
开发体验
- ✅ TypeScript 支持 - 完整的 TypeScript 类型定义
- ✅ 装饰器驱动 - 声明式 API,代码简洁优雅
- ✅ 零依赖 - 无需安装额外插件即可使用所有功能
要求
- Node.js >= 22.18.0
- Egg.js >= 4.1.0-beta.35
安装
安装最新版本
npm i @gulibs/tegg-guard
# 或
npm i @gulibs/tegg-guard@latest安装测试版本
要安装测试版本,必须显式指定 @beta 标签:
npm i @gulibs/tegg-guard@beta注意:使用 @latest 或不加标签将安装最新的稳定版本,而不是测试版本。
快速开始
1. 启用插件
// config/plugin.ts
import guardPlugin from '@gulibs/tegg-guard';
export default {
...guardPlugin(),
};2. 配置插件
在 ${baseDir}/config/config.{env}.ts 中填写必要信息:
// config/config.default.ts
export default {
teggGuard: {
enable: true,
secret: 'your-secret-key',
refreshToken: {
enable: true,
accessTokenExpiresIn: '15m',
refreshTokenExpiresIn: '7d',
},
},
};3. 使用装饰器和守卫创建控制器
使用
@HTTPController装饰的控制器会被@eggjs/tegg-controller-plugin自动发现和注册。无需手动配置路由。
装饰器使用指南
导入语句
重要:所有装饰器应该从 egg 导入,而不是从 @eggjs/tegg 导入。
// ✅ 正确:从 'egg' 导入
import { HTTPController, HTTPMethod, HTTPMethodEnum, Context, Middleware } from 'egg';
import { Roles, Permissions, RateLimit, AuditLog } from '@gulibs/tegg-guard';
// ❌ 错误:不要从 '@eggjs/tegg' 导入(虽然也能工作,但不推荐)
// import { HTTPController } from '@eggjs/tegg';原因:egg 重新导出了 @eggjs/tegg 的所有装饰器,所以从 egg 导入是标准做法,可以获得更好的一致性。
装饰器执行顺序
重要: TypeScript 装饰器从下到上执行,但生成的中间件从上到下执行。
为了简化使用,本插件的所有授权装饰器(@Roles, @Permissions 等)都自动包含认证逻辑,无需额外使用 @AuthGuard()。
推荐用法(简洁)
import { HTTPController, HTTPMethod, HTTPMethodEnum, Context } from 'egg';
import { Roles, Permissions, RateLimit, AuditLog } from '@gulibs/tegg-guard';
@HTTPController({ path: '/api/admin' })
export class AdminController {
// ✅ 只使用 @Roles,自动认证 + 角色检查
@Roles('admin')
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/dashboard' })
async dashboard(@Context() ctx: any) {
return { message: 'Admin dashboard', user: ctx.guard.getCurrentUser() };
}
// ✅ 只使用 @Permissions,自动认证 + 权限检查
@Permissions('users:create', 'users:update')
@HTTPMethod({ method: HTTPMethodEnum.POST, path: '/users' })
async createUser() {
return { message: 'User created' };
}
// ✅ 组合多个装饰器(按需组合)
@AuditLog({ action: 'user.deleted' }) // 5. 最后记录审计日志
@RateLimit({ max: 5, window: '1m' }) // 4. 速率限制
@Permissions('users:delete') // 3. 权限检查(自动认证)
@Roles('admin') // 2. 角色检查
@HTTPMethod({ method: HTTPMethodEnum.DELETE, path: '/users/:id' }) // 1. 路由定义
async deleteUser() {
return { message: 'User deleted' };
}
}装饰器说明
1. 认证装饰器
@AuthGuard(options?) - 基础认证装饰器
// 基本用法
@AuthGuard()
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/profile' })
async getProfile() { }
// 禁用认证(公开端点)
@AuthGuard({ enable: false })
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/public' })
async publicEndpoint() { }
// 只对特定路径启用
@AuthGuard({ match: '/api/private' })
@HTTPController()
export class PrivateController { }
// 忽略特定路径
@AuthGuard({ ignore: ['/health', '/metrics'] })
@HTTPController()
export class ApiController { }2. 授权装饰器(自动认证)
@Roles(...roles) - 要求用户拥有所有指定角色
// 单个角色
@Roles('admin')
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/admin' })
async adminOnly() { }
// 多个角色(需要同时拥有)
@Roles('admin', 'moderator')
@HTTPMethod({ method: HTTPMethodEnum.POST, path: '/moderate' })
async moderate() { }@RequireAnyRole(...roles) - 要求用户拥有任意一个指定角色
@RequireAnyRole('admin', 'moderator', 'support')
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/reports' })
async viewReports() { }@Permissions(...permissions) - 要求用户拥有所有指定权限
// 单个权限
@Permissions('users:create')
@HTTPMethod({ method: HTTPMethodEnum.POST, path: '/users' })
async createUser() { }
// 多个权限(需要同时拥有)
@Permissions('users:create', 'users:update')
@HTTPMethod({ method: HTTPMethodEnum.PUT, path: '/users/:id' })
async updateUser() { }
// 通配符权限
@Permissions('users:*') // 匹配 users:create, users:update, users:delete 等
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/users/manage' })
async manageUsers() { }@RequireAnyPermission(...permissions) - 要求用户拥有任意一个指定权限
@RequireAnyPermission('users:read', 'users:list')
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/users' })
async listUsers() { }3. 速率限制装饰器
@RateLimit(options) / @Throttle(options) - 限制请求频率
// 基本用法:每分钟最多 10 次请求
@RateLimit({ max: 10, window: '1m' })
@HTTPMethod({ method: HTTPMethodEnum.POST, path: '/api/action' })
async action() { }
// 自定义 key 生成器(按用户限制)
@RateLimit({
max: 5,
window: '1h',
keyGenerator: (ctx) => ctx.state.user?.id || ctx.ip
})
@HTTPMethod({ method: HTTPMethodEnum.POST, path: '/api/upload' })
async upload() { }
// 自定义错误消息
@RateLimit({
max: 3,
window: '10s',
message: '请求过于频繁,请稍后再试'
})
@HTTPMethod({ method: HTTPMethodEnum.POST, path: '/api/verify' })
async verify() { }4. 审计日志装饰器
@AuditLog(options?) / @TrackActivity(options?) - 记录用户操作
// 基本用法
@AuditLog({ action: 'user.created', resourceType: 'user' })
@HTTPMethod({ method: HTTPMethodEnum.POST, path: '/users' })
async createUser() { }
// 记录请求和响应体
@AuditLog({
action: 'order.created',
resourceType: 'order',
includeBody: true,
includeQuery: true
})
@HTTPMethod({ method: HTTPMethodEnum.POST, path: '/orders' })
async createOrder() { }
// 自定义元数据
@AuditLog({
action: 'payment.processed',
metadata: (ctx, result, error) => ({
amount: ctx.request.body.amount,
success: !error,
errorMessage: error?.message
})
})
@HTTPMethod({ method: HTTPMethodEnum.POST, path: '/payments' })
async processPayment() { }装饰器组合最佳实践
场景 1: 简单认证
// ✅ 推荐:只使用授权装饰器(自动认证)
@Roles('user')
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/profile' })
async getProfile() { }
// ⚠️ 可以但不推荐:显式使用 @AuthGuard(会执行两次认证)
@Roles('user')
@AuthGuard()
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/profile' })
async getProfile() { }场景 2: 多层授权
// ✅ 推荐:从上到下:审计 → 速率 → 权限 → 角色
@AuditLog({ action: 'sensitive.operation' })
@RateLimit({ max: 3, window: '1h' })
@Permissions('admin:execute')
@Roles('admin')
@HTTPMethod({ method: HTTPMethodEnum.POST, path: '/admin/execute' })
async execute() { }场景 3: 公开端点
// ✅ 推荐:不使用任何认证/授权装饰器
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/public/info' })
async publicInfo() { }
// ✅ 或显式禁用
@AuthGuard({ enable: false })
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/public/info' })
async publicInfo() { }场景 4: 混合公开和受保护端点
@HTTPController({ path: '/api/posts' })
export class PostController {
// 公开:任何人都可以查看
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })
async list() { }
// 受保护:需要认证
@AuthGuard()
@HTTPMethod({ method: HTTPMethodEnum.POST, path: '/' })
async create() { }
// 受保护:需要特定角色
@Roles('admin', 'moderator')
@HTTPMethod({ method: HTTPMethodEnum.DELETE, path: '/:id' })
async delete() { }
}自定义角色和权限提取
如果你的用户对象的角色 / 权限字段名不是标准的 roles 和 permissions,可以配置自定义提取函数:
// config/config.default.ts
export default {
teggGuard: {
rbac: {
enable: true,
// 场景 1: 字段名不同
extractRoles: (user) => ({
roles: user.userRoles || [] // 从 userRoles 字段提取
}),
extractPermissions: (user) => ({
permissions: user.scopes || user.authorities || []
}),
// 场景 2: 单个字符串转数组
extractRoles: (user) => ({
roles: user.role ? [user.role] : []
}),
// 场景 3: 从嵌套对象提取
extractRoles: (user) => ({
roles: user.profile?.roles || []
}),
// 场景 4: 同时提取角色和权限
extractRoles: (user) => ({
roles: user.userRoles || [],
permissions: user.userPermissions || []
})
}
}
});错误处理
装饰器会返回明确的错误信息:
// 401 Unauthorized - 未认证
{
"error": "Authentication required"
}
// 403 Forbidden - 角色不足
{
"error": "Insufficient permissions: requires roles [admin, moderator]"
}
// 403 Forbidden - 权限不足
{
"error": "Insufficient permissions: requires permissions [users:create, users:update]"
}
// 429 Too Many Requests - 速率限制
{
"error": "Too many requests, please try again later."
}
// 500 Internal Server Error - 配置错误
{
"error": "Invalid user roles format"
}配置错误时会有详细的日志:
[tegg-guard] @Roles: user.roles is not an array! Got: undefined
[tegg-guard] @Roles: Please ensure user object has a "roles" array propertyAuthGuard 配置
@AuthGuard() 支持与中间件一致的 ignore、match 以及 enable 参数:
@AuthGuard({ match: '/api/private' })
@HTTPController()
export class PrivateController {
@AuthGuard({ enable: false })
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/health' })
async health() {
return { ok: true };
}
}enable— 显式启用 / 禁用某个类或方法的认证。match— 仅在匹配的路径上启用认证(字符串、正则或函数)。ignore— 在匹配的路径上跳过认证(常用于受保护控制器中的健康检查)。
// {app_root}/app/authorizations/controller/user.ts
import {
Context,
HTTPContext,
HTTPController,
HTTPMethod,
HTTPMethodEnum,
} from 'egg';
import { AuthGuard } from '@gulibs/tegg-guard';
@HTTPController({ path: '/api' })
@AuthGuard() // 类级守卫:该控制器下所有路由都需要认证
export class UserController {
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/profile' })
async profile(@HTTPContext() ctx: Context) {
return { user: ctx.state.user };
}
@AuthGuard({ enable: false }) // 方法级覆盖:此路由无需认证
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/public' })
async public() {
return { message: 'Public endpoint' };
}
}4. 签名和验证令牌
@HTTPController({ path: '/api/auth' })
export class AuthController {
@HTTPMethod({ method: HTTPMethodEnum.POST, path: '/login' })
async login(@HTTPContext() ctx: Context) {
const { username, password } = ctx.request.body;
const user = await ctx.service.user.findByUsername(username);
if (!user || !ctx.service.user.verifyPassword(user, password)) {
ctx.status = 401;
ctx.body = { error: 'Invalid credentials' };
return;
}
const tokenPair = ctx.app.guard.generateTokenPair({
userId: user.id,
username: user.username,
});
ctx.app.guard.setToken(ctx, tokenPair.accessToken, 'access');
ctx.app.guard.setToken(ctx, tokenPair.refreshToken, 'refresh');
ctx.body = {
user: { id: user.id, username: user.username },
accessToken: tokenPair.accessToken,
expiresIn: tokenPair.expiresIn,
};
}
}5. 访问用户上下文
@HTTPController({ path: '/api' })
@AuthGuard()
export class UserController {
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/profile' })
async profile(@HTTPContext() ctx: Context) {
// 用户信息自动注入到 ctx.state.user
return { user: ctx.state.user };
}
}配置
基础配置
// config/config.default.ts
export default {
teggGuard: {
enable: true,
secret: 'your-secret-key', // 或密钥数组用于轮换: ['key1', 'key2']
// 忽略认证的路由
ignore: ['/api/public', /^\/api\/v1\/public/],
// 或匹配的路由(如果指定,只有这些路由需要认证)
// match: ['/api/protected'],
},
};兼容传统中间件(可选)
仍需保留 app/router.ts + app.controller 的老项目,可以通过 legacyMiddleware 启用传统模式:
// config/config.default.ts
export default {
teggGuard: {
enable: true,
secret: 'your-secret-key',
legacyMiddleware: {
enable: true,
config: {
ignore: ['/health'],
match: ['/api'],
},
},
},
};legacyMiddleware.enable决定插件是否重新把'guard'中间件放回config.middleware。默认false,优先推荐装饰器写法。legacyMiddleware.config与config.auth的类型完全一致,只覆盖需要调整的字段即可,其他字段会继承teggGuard。- 当该模式关闭时,插件会从全局中间件数组移除
'guard',并保持config.auth.enable = false,这样只有@AuthGuard()会触发认证。
// {app_root}/app/router.ts
export default (app) => {
const { router, controller, middleware } = app;
router.post('/api/auth/login', controller.auth.login);
router.get('/api/profile', middleware.guard(), controller.user.profile);
};传统模式仅用于必须继续
extends app.Controller的场景,新的控制器仍然建议使用@HTTPController+@AuthGuard()实现精细控制。
令牌存储配置
teggGuard: {
storage: {
// 访问令牌位置: 'header', 'cookie', 或 'both'
accessTokenLocation: 'header',
accessTokenCookieName: 'accessToken',
// 刷新令牌位置: 'header', 'cookie', 或 'both'
refreshTokenLocation: 'cookie',
refreshTokenCookieName: 'refreshToken',
refreshTokenHeaderName: 'X-Refresh-Token',
// Cookie 选项
accessTokenCookieOptions: {
httpOnly: false,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
},
refreshTokenCookieOptions: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/',
},
},
}刷新令牌配置
teggGuard: {
refreshToken: {
enable: true,
accessTokenExpiresIn: '15m', // 访问令牌过期时间
refreshTokenExpiresIn: '7d', // 刷新令牌过期时间
autoRefresh: true, // 在即将过期时自动刷新令牌
refreshThreshold: '5m', // 如果令牌在此时间内过期则刷新
secret: 'refresh-secret', // 可选:刷新令牌使用不同的密钥
},
}令牌管理器配置(撤销)
teggGuard: {
tokenManager: {
enable: true,
adapter: 'memory', // 'memory', 'redis', 或 'custom'
// Redis 配置(如果使用 redis 适配器)
redis: {
host: '127.0.0.1',
port: 6379,
password: 'password',
db: 0,
keyPrefix: 'jwt:revoked:',
},
// 自定义适配器(如果使用 custom 适配器)
// customAdapter: yourCustomAdapter,
},
}会话管理配置
teggGuard: {
session: {
enable: true,
rotateRefreshToken: true, // 使用时轮换刷新令牌
maxConcurrentSessions: 5, // 每个用户的最大并发会话数
revokeOldSessions: true, // 达到最大值时撤销旧会话
},
// 会话管理需要启用令牌管理器
tokenManager: {
enable: true,
adapter: 'redis', // 生产环境推荐使用 Redis
},
}令牌绑定配置
teggGuard: {
tokenBinding: {
enable: true,
bindToIp: true, // 将令牌绑定到请求 IP 地址
bindToOrigin: true, // 将令牌绑定到请求来源(host header)
},
}teggGuard: {
enable: true, // 仅对基于 cookie 的会话启用
cookieOptions: {
httpOnly: false, // 必须为 false 以便 JavaScript 访问
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/',
maxAge: 86400000, // 24 小时
},
},
}多密钥配置(密钥轮换)
teggGuard: {
secret: ['new-key', 'old-key'], // 用于轮换的密钥数组
multiKey: {
enable: true,
keyIdField: 'kid', // 令牌 header 中的密钥 ID 字段
},
}用户上下文配置
teggGuard: {
userContext: {
enable: true,
propertyName: 'user', // ctx.state 中的属性名
extractUser: (payload) => {
// 从令牌 payload 中提取用户信息的自定义函数
return {
id: payload.userId,
username: payload.username,
};
},
},
}使用方法
签名令牌
// 基础用法
const token = app.guard.sign({ userId: '123', username: 'john' });
// 使用自定义密钥
const token = app.guard.sign({ userId: '123' }, 'custom-secret');
// 使用选项
const token = app.guard.sign(
{ userId: '123' },
{ expiresIn: '1h', issuer: 'my-app' }
);
// 使用回调
app.guard.sign({ userId: '123' }, (err, token) => {
if (err) {
console.error(err);
return;
}
console.log('Token:', token);
});验证令牌
// 基础用法
const payload = app.guard.verify(token) as { userId: string };
// 使用自定义密钥
const payload = app.guard.verify(token, 'custom-secret');
// 使用选项
const payload = app.guard.verify(token, {
issuer: 'my-app',
audience: 'my-client',
});
// 使用回调
app.guard.verify(token, (err, decoded) => {
if (err) {
console.error(err);
return;
}
console.log('Decoded:', decoded);
});生成令牌对
// 生成访问令牌和刷新令牌对
const tokenPair = app.guard.generateTokenPair({
userId: '123',
username: 'john',
});
// 使用不同的刷新 payload
const tokenPair = app.guard.generateTokenPair(
{ userId: '123' }, // 访问令牌 payload
{ userId: '123', sessionId: 'session-123' } // 刷新令牌 payload
);
// 访问令牌
const { accessToken, refreshToken, expiresIn } = tokenPair;刷新令牌
// 使用刷新令牌刷新访问令牌
const newTokenPair = await app.guard.refreshAccessToken(refreshToken);
// 使用新 payload
const newTokenPair = await app.guard.refreshAccessToken(refreshToken, {
userId: '123',
updatedAt: Date.now(),
});令牌撤销
// 撤销单个令牌
await app.guard.revokeToken(token, 'access');
await app.guard.revokeToken(token, 'refresh');
// 撤销用户的所有令牌
await app.guard.revokeAllTokens(userId);
// 检查令牌是否已撤销
const isRevoked = await app.guard.isTokenRevoked(token, 'access');会话管理
// 获取用户的所有会话(需要启用会话管理)
const sessions = await app.guard.session?.getAllSessions(userId);
// 撤销特定会话
await app.guard.session?.revokeSession(sessionId);
// 撤销用户的所有会话
await app.guard.session?.revokeAllSessions(userId);
令牌提取和设置
// 从请求中提取令牌
const accessToken = app.guard.extractToken(ctx, 'access');
const refreshToken = app.guard.extractToken(ctx, 'refresh');
// 在响应中设置令牌
app.guard.setToken(ctx, token, 'access');
app.guard.setToken(ctx, token, 'refresh');
// 从响应中清除令牌
app.guard.clearToken(ctx, 'access');
app.guard.clearToken(ctx, 'refresh');中间件使用
推荐通过装饰器守卫触发认证逻辑;auth 中间件仍可用于路由级或遗留场景。
全局中间件
当 config.teggGuard.enable = true 时,中间件会自动注册:
// config/config.default.ts
export default {
teggGuard: {
enable: true,
secret: 'your-secret-key',
},
};使用
@AuthGuard()装饰器时,请保持config.auth.enable = false(默认值),这样中间件只会在守卫包裹的控制器内执行。
路由匹配
teggGuard: {
// 仅保护匹配模式的路由
match: ['/api/protected', /^\/api\/v1\/private/],
// 或忽略特定路由
ignore: ['/api/public', '/api/login', /^\/api\/v1\/public/],
}高级装饰器
授权装饰器
@Roles(...roles: string[])
检查用户是否拥有所有指定角色。
import { Roles } from '@gulibs/tegg-guard';
@HTTPController()
export class AdminController {
@Roles('admin', 'moderator')
@HTTPMethod({ method: HTTPMethodEnum.DELETE, path: '/users/:id' })
async deleteUser() {
// 只有同时拥有 'admin' 和 'moderator' 角色的用户才能访问
}
}@RequireAnyRole(...roles: string[])
检查用户是否拥有任意一个指定角色。
@RequireAnyRole('admin', 'moderator', 'support')
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/reports' })
async viewReports() {
// 拥有 'admin'、'moderator' 或 'support' 任一角色即可访问
}@Permissions(...permissions: string[])
检查用户是否拥有所有指定权限。支持通配符匹配(如 users:*)。
@Permissions('users:create', 'users:update')
@HTTPMethod({ method: HTTPMethodEnum.POST, path: '/users' })
async createUser() {
// 需要同时拥有 'users:create' 和 'users:update' 权限
}@RequireAnyPermission(...permissions: string[])
检查用户是否拥有任意一个指定权限。
@RequireAnyPermission('users:read', 'users:list')
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/users' })
async listUsers() {
// 拥有 'users:read' 或 'users:list' 任一权限即可访问
}配置角色层级:
// config/config.default.ts
export default {
teggGuard: {
rbac: {
enable: true,
roleHierarchy: {
admin: ['moderator', 'user'], // admin 继承 moderator 和 user 的权限
moderator: ['user'], // moderator 继承 user 的权限
},
},
},
};速率限制装饰器
@RateLimit(options: RateLimitOptions)
限制 API 端点的请求频率。
import { RateLimit } from '@gulibs/tegg-guard';
@RateLimit({ max: 10, window: '1m' })
@HTTPMethod({ method: HTTPMethodEnum.POST, path: '/api/send-email' })
async sendEmail() {
// 每分钟最多 10 次请求
}选项:
max- 时间窗口内允许的最大请求数window- 时间窗口(支持'1h','15m','60s'等格式)keyGenerator- 自定义 key 生成函数(默认使用用户 ID 或 IP)message- 超过限制时的错误消息skip- 跳过速率限制的条件函数
@Throttle(options: RateLimitOptions)
@RateLimit 的别名。
配置:
// config/config.default.ts
export default {
teggGuard: {
rateLimit: {
enable: true,
adapter: 'redis', // 或 'memory'
redis: app.redis, // Redis 实例(adapter 为 'redis' 时需要)
default: {
max: 100,
window: '1h',
},
},
},
};审计日志装饰器
@AuditLog(options?: AuditLogOptions)
自动记录用户操作,用于审计和合规。
import { AuditLog } from '@gulibs/tegg-guard';
@AuditLog({ action: 'user.created', resourceType: 'user', includeBody: true })
@HTTPMethod({ method: HTTPMethodEnum.POST, path: '/users' })
async createUser(@HTTPBody() body: CreateUserDto) {
// 操作将被自动记录
}选项:
action- 操作名称(如'user.created','post.updated')resourceType- 资源类型(如'user','post')includeBody- 是否在日志中包含请求体includeQuery- 是否在日志中包含查询参数extractMetadata- 自定义元数据提取函数skip- 跳过审计日志的条件函数
@TrackActivity(options?: AuditLogOptions)
@AuditLog 的别名。
配置:
// config/config.default.ts
export default {
teggGuard: {
auditLog: {
enable: true,
adapter: 'memory', // 或 'database'
maxLogs: 10000, // 内存存储的最大日志数
includeBody: false,
includeQuery: true,
},
},
};Context 扩展
插件扩展了 Context 对象,提供便捷的辅助方法:
// 在控制器中使用
@HTTPController()
export class UserController {
@Inject()
private contextHelper: ContextHelper;
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/profile' })
async getProfile() {
const { ctx } = this.contextHelper;
// 获取当前用户
const user = ctx.guard.getCurrentUser();
// 获取用户 ID
const userId = ctx.guard.getUserId();
// 获取会话信息
const session = ctx.guard.getSession();
// 刷新会话
await ctx.guard.refreshSession();
// 撤销其他会话
await ctx.guard.revokeOtherSessions();
return { user };
}
}可用方法:
ctx.guard.getCurrentUser()- 获取当前认证用户(来自ctx.state.user)ctx.guard.getUserId()- 获取当前用户 IDctx.guard.getSession()- 获取当前会话信息ctx.guard.refreshSession()- 手动刷新会话令牌ctx.guard.revokeOtherSessions()- 撤销用户的其他所有会话
装饰器组合
可以组合多个装饰器以实现复杂的访问控制:
@HTTPController()
export class SecureController {
@AuthGuard() // 1. 认证检查
@Roles('admin') // 2. 角色检查
@Permissions('users:delete') // 3. 权限检查
@RateLimit({ max: 5, window: '1m' }) // 4. 速率限制
@AuditLog({ action: 'user.deleted' }) // 5. 审计日志
@HTTPMethod({ method: HTTPMethodEnum.DELETE, path: '/users/:id' })
async deleteUser(@HTTPParam() id: string) {
// 只有通过所有检查的请求才能到达这里
}
}执行顺序: 装饰器按照从下到上的顺序执行(egg 中间件洋葱模型)。
API 参考
app.guard.sign()
签名 JWT 令牌。
sign(
payload: string | Buffer | object,
secretOrPrivateKey?: string | SignOptions,
options?: SignOptions,
callback?: (err: Error | null, token?: string) => void
): string | void参数:
payload- 要签名的数据secretOrPrivateKey- 密钥(可选,如果未提供则使用config.secret)options- 签名选项(可选)callback- 回调函数(可选)
返回: 令牌字符串,如果提供了回调则返回 void
示例:
const token = app.guard.sign({ userId: '123' });app.guard.verify()
验证 JWT 令牌。
verify(
token: string,
secretOrPrivateKey?: string | VerifyOptions,
options?: VerifyOptions,
callback?: (err: Error | null, decoded?: object | string) => void
): object | string | void参数:
token- 要验证的 JWT 令牌secretOrPrivateKey- 密钥(可选,如果未提供则使用config.secret)options- 验证选项(可选)callback- 回调函数(可选)
返回: 解码后的 payload,如果提供了回调则返回 void
示例:
const payload = app.guard.verify(token) as { userId: string };app.guard.decode()
不解密验证直接解码 JWT 令牌。
decode(token: string): string | object | null示例:
const payload = app.guard.decode(token);app.guard.generateTokenPair()
生成访问令牌和刷新令牌对。
generateTokenPair(
payload: string | Buffer | object,
refreshPayload?: string | Buffer | object
): TokenPair返回:
{
accessToken: string;
refreshToken: string;
expiresIn: number; // 过期时间(秒)
}app.guard.refreshAccessToken()
使用刷新令牌刷新访问令牌。
refreshAccessToken(
refreshToken: string,
newPayload?: string | Buffer | object
): Promise<TokenPair>app.guard.revokeToken()
撤销令牌(添加到黑名单)。
revokeToken(
token: string,
tokenType?: 'access' | 'refresh'
): Promise<void>app.guard.revokeAllTokens()
撤销用户的所有令牌。
revokeAllTokens(userId: string): Promise<void>app.guard.isTokenRevoked()
检查令牌是否已撤销。
isTokenRevoked(
token: string,
tokenType?: 'access' | 'refresh'
): Promise<boolean>app.guard.extractToken()
从请求中提取令牌(header 或 cookie)。
extractToken(
ctx: Context,
tokenType?: 'access' | 'refresh'
): string | nullapp.guard.setToken()
在响应中设置令牌(header 或 cookie)。
setToken(
ctx: Context,
token: string,
tokenType?: 'access' | 'refresh'
): voidapp.guard.clearToken()
从响应中清除令牌(删除 cookie 或 header)。
clearToken(
ctx: Context,
tokenType?: 'access' | 'refresh'
): voidapp.guard.session
会话管理方法(仅在启用会话管理时可用)。
app.guard.session.getAllSessions()
获取用户的所有会话。
getAllSessions(userId: string): Promise<Array<{
sessionId: string;
userId: string;
accessToken: string;
refreshToken: string;
createdAt: number;
lastAccessedAt: number;
expiresAt: number;
metadata?: Record<string, unknown>;
}>>app.guard.session.revokeSession()
撤销特定会话。
revokeSession(sessionId: string): Promise<void>app.guard.session.revokeAllSessions()
撤销用户的所有会话。
revokeAllSessions(userId: string): Promise<void>generateToken(): stringgetToken(ctx: Context): string | nullverifyToken(ctx: Context): boolean上下文扩展
中间件自动将用户信息注入到 ctx.state.user:
// 在你的控制器中
async profile() {
const user = this.ctx.state.user; // 解码后的 JWT payload
this.ctx.body = { user };
}你可以自定义属性名:
teggGuard: {
userContext: {
propertyName: 'currentUser', // 默认: 'user'
},
}错误处理
中间件在认证失败时抛出 UnauthorizedError:
import { UnauthorizedError } from 'app.auth';
try {
const payload = app.guard.verify(token);
} catch (error) {
if (error instanceof app.guard.UnauthorizedError) {
// 处理未授权错误
}
}从 tegg-jwt 迁移
如果你正在从 @gulibs/tegg-jwt 迁移,请查看以下破坏性变更:
- 插件名称:
teggJwt→teggGuard - 配置命名空间:
config.teggJwt→config.teggGuard - 应用扩展:
app.jwt→app.auth - 中间件名称:
'jwt'→'guard'(用于守卫或遗留路由)
更新你的代码:
// 之前 (tegg-jwt)
config.teggJwt = { enable: true };
const token = app.jwt.sign({ userId: '123' });
app.get('/api', 'jwt', handler);
// 之后 (tegg-guard)
config.teggGuard = { enable: true };
const token = app.guard.sign({ userId: '123' });
// 方案 A:推荐的装饰器守卫
// @AuthGuard() 标记控制器/方法使用 yarn link 进行本地调试
# 在 @gulibs/tegg-guard 仓库中
yarn install
yarn link
# 在业务应用中
yarn link @gulibs/tegg-guard
yarn dev调试完成后记得恢复:
# 在业务应用中
yarn unlink @gulibs/tegg-guard
yarn install
# 回到插件仓库
yarn unlink通过链接可以在真实项目里验证装饰器守卫和中间件逻辑是否符合预期,无需发布 npm 版本。
示例
完整的认证流程
@HTTPController({ path: '/api/auth' })
export class AuthController {
@HTTPMethod({ method: HTTPMethodEnum.POST, path: '/login' })
async login(@HTTPContext() ctx: Context) {
const { username, password } = ctx.request.body;
// 验证凭据
const user = await ctx.service.user.findByUsername(username);
if (!user || !ctx.service.user.verifyPassword(user, password)) {
ctx.status = 401;
ctx.body = { error: 'Invalid credentials' };
return;
}
// 生成令牌对
const tokenPair = ctx.app.guard.generateTokenPair({
userId: user.id,
username: user.username,
role: user.role,
});
// 创建会话(如果启用了会话管理)
// 注意:如果配置中启用了会话管理,设置令牌时会自动创建会话
// 设置令牌
ctx.app.guard.setToken(ctx, tokenPair.accessToken, 'access');
ctx.app.guard.setToken(ctx, tokenPair.refreshToken, 'refresh');
ctx.body = {
user: { id: user.id, username: user.username },
accessToken: tokenPair.accessToken,
expiresIn: tokenPair.expiresIn,
};
}
@HTTPMethod({ method: HTTPMethodEnum.POST, path: '/logout' })
@AuthGuard()
async logout(@HTTPContext() ctx: Context) {
const token = ctx.app.guard.extractToken(ctx, 'access');
if (token) {
await ctx.app.guard.revokeToken(token, 'access');
}
const refreshToken = ctx.app.guard.extractToken(ctx, 'refresh');
if (refreshToken) {
await ctx.app.guard.revokeToken(refreshToken, 'refresh');
}
ctx.app.guard.clearToken(ctx, 'access');
ctx.app.guard.clearToken(ctx, 'refresh');
ctx.body = { message: 'Logged out successfully' };
}
}// 客户端(JavaScript)
// 在后续请求中包含
fetch('/api/protected', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify({ data: 'value' }),
});
// 或在表单提交中
<form method="POST" action="/api/protected">
<!-- 其他字段 -->
</form>支持与问题
如有问题或需要反馈,请使用 gulibs/tegg-guard issue tracker。
许可证
MIT
