权限与风控
权限模型、路由保护、双因素认证、验证码、请求限流和内容审核
概览
项目内置了多层安全能力,从用户权限到接口风控一应俱全:
| 能力 | 说明 |
|---|---|
| 权限模型 | 基于 CASL 的声明式权限判断(Role / Action / Subject) |
| 管理员权限 | SUPER_ADMIN、OPERATION_ADMIN 两级管理角色 |
| 路由保护 | 按 URL 模式匹配的认证/权限/订阅检查 |
| 双因素认证 | Better Auth twoFactor 插件,支持 TOTP + 备份码 |
| 验证码 | Cloudflare Turnstile 集成 |
| 请求限流 | 可配置窗口的速率限制器(RateLimiter) |
| 内容审核 | 腾讯云文本 / 图片内容安全 |
| 跨域登录 | 通过 COOKIE_DOMAIN 实现跨子域名 Cookie SSO |
权限模型
项目使用 CASL(一个权限判断库,帮你检查用户能不能做某件事)实现声明式的权限控制。核心概念:
- Role(角色):
USER、VIP、ADMIN - Action(操作):
create、read、update、delete、manage(manage 表示所有操作) - Subject(对象):
User、Project、Subscription、Settings、all
各角色的默认权限:
| 角色 | Project | User(自身) | Subscription | Settings |
|---|---|---|---|---|
| USER | 读、创建、更新 | 读、更新 | - | 读 |
| VIP | 读、创建、更新、删除 | 读、更新 | 读 | 读、更新 |
| ADMIN | 全部(manage all) | 全部 | 全部 | 全部 |
权限定义在 packages/permissions/src/abilities.ts。在代码中检查权限:
import { can, cannot, Action, Subject } from "@01mvp/permissions";
import type { AppUser } from "@01mvp/permissions";
const user: AppUser = { id: "user-123", role: Role.USER };
// 检查用户能否创建项目
if (can(user, Action.CREATE, Subject.PROJECT)) {
// 允许操作
}
// 检查用户能否更新其他用户的信息(带具体数据)
if (cannot(user, Action.UPDATE, Subject.USER, { id: "other-user-id" })) {
// 拒绝操作
}packages/permissions/src/utils.ts 还提供了辅助函数:
import { createAppUser, getAvailableActions, mapDatabaseRoleToAppRole } from "@01mvp/permissions";
// 从数据库用户记录创建 AppUser(自动映射 role 字符串到枚举)
const appUser = createAppUser({ id: "1", role: "vip" });
// 获取用户在某个对象上所有可执行的操作
const actions = getAvailableActions(appUser, Subject.PROJECT);
// => ["read", "create", "update", "delete"]mapDatabaseRoleToAppRole 会自动把 "admin" 映射为 ADMIN,"vip" 映射为 VIP,其他值(包括 "user"、"normal")映射为 USER。
定义新的权限规则
在 packages/permissions/src/abilities.ts 的 defineAbilitiesFor 函数中添加规则:
// 示例:给 VIP 用户增加查看订阅详情的权限
can(Action.READ, Subject.SUBSCRIPTION);如果需要新增 Subject(对象类型),在 packages/permissions/src/types.ts 的 Subject 枚举中添加即可。
路由保护
路由保护模块(packages/auth/src/route-protection.ts)提供了一套与具体框架无关的路由访问控制工具。它通过正则表达式匹配 URL,然后根据配置决定是否放行。
工作原理
每个路由配置包含以下字段:
interface RouteConfig {
pattern: RegExp; // URL 匹配模式
type: "page" | "api"; // 路由类型(影响响应格式)
requiresAuth?: boolean; // 是否需要登录(默认 true)
requiredPermission?: { // 所需权限
action: string;
subject: string;
};
requiresSubscription?: boolean; // 是否需要有效订阅
isAuthRoute?: boolean; // 是否为登录页(登录用户会被重定向走)
}检查结果有四种拒绝理由:unauthenticated(未登录)、forbidden(无权限)、subscription_required(需要订阅)、redirect_authenticated(已登录用户不应访问登录页)。
使用示例
import { matchRoute, checkRouteAccess } from "@01mvp/auth/route-protection";
// 定义路由规则
const routes = [
{ pattern: /^\/admin/, type: "page", requiredPermission: { action: "manage", subject: "all" } },
{ pattern: /^\/dashboard/, type: "page", requiresAuth: true },
{ pattern: /^\/login/, type: "page", isAuthRoute: true },
{ pattern: /^\/api\/premium/, type: "api", requiresSubscription: true },
];
// 在中间件中使用
const route = matchRoute(pathname, routes);
if (route) {
const result = checkRouteAccess(route, {
isAuthenticated: !!session,
userId: session?.user?.id,
hasSubscription: userHasSub,
checkPermission: (action, subject) => can(user, action, subject),
});
if (!result.allowed) {
if (result.redirectUrl) return redirect(result.redirectUrl);
return new Response(result.reason, { status: result.statusCode });
}
}双因素认证 (2FA)
项目通过 Better Auth 的 twoFactor 插件支持基于 TOTP(基于时间的一次性验证码,类似 Google Authenticator)的双因素认证。
配置
在 config.auth.enableTwoFactor(应用配置)中开启:
// apps/01mvp-web/src/lib/auth/auth-config.ts
const twoFactorPlugin = (() => {
if (!config.auth.enableTwoFactor) {
return null;
}
return twoFactor();
})();插件初始化失败时会记录错误日志并自动降级(不启用 2FA),不会影响应用启动。
用户流程
- 用户在设置页选择「启用双因素认证」
- 系统生成 TOTP 密钥,前端展示二维码
- 用户用 Authenticator App 扫描二维码
- 输入 App 显示的 6 位验证码完成验证
- 系统同时生成一组备份码,用户需妥善保存
数据库模型
Prisma schema 中的 TwoFactor 模型存储 2FA 数据:
model TwoFactor {
id String @id
userId String
secret String // TOTP 密钥(加密存储)
backupCodes String // 备份码(JSON 数组,加密存储)
user User @relation(fields: [userId], references: [id])
}客户端集成
客户端使用 twoFactorClient() 插件,已在 apps/01mvp-web/src/lib/auth/client.ts 中注册。前端可以通过 Better Auth 客户端 API 调用 twoFactor.enable、twoFactor.verify、twoFactor.disable 等方法。
管理员系统
管理员权限基于 Better Auth 的 admin 插件,配合 packages/auth/src/permissions.ts 中的细粒度权限模型。
角色与权限
两个管理员角色,权限粒度不同:
| 权限 | SUPER_ADMIN | OPERATION_ADMIN |
|---|---|---|
| 查看用户 | yes | yes |
| 管理用户 | yes | yes |
| 封禁用户 | yes | yes |
| 分配角色 | yes | - |
| 查看职能角色 | yes | yes |
| 管理职能角色 | yes | - |
| 查看贡献 | yes | yes |
| 审核贡献 | yes | yes |
| 查看勋章 | yes | yes |
| 管理勋章 | yes | yes |
| 颁发勋章 | yes | yes |
| 查看组织 | yes | yes |
| 管理组织 | yes | - |
| 查看系统配置 | yes | - |
| 管理系统配置 | yes | - |
| 查看仪表板 | yes | yes |
在代码中检查管理员权限:
import { isAdmin, hasPermission, AdminPermission } from "@01mvp/auth/permissions";
if (isAdmin(user)) {
// 是管理员
}
if (hasPermission(user, AdminPermission.BAN_USERS)) {
// 有封禁用户权限
}向后兼容:数据库中旧的 "admin" 角色会被自动识别为 SUPER_ADMIN。
创建管理员用户
通过 Better Auth 的管理接口或直接在数据库中将用户的 role 字段设为 "super_admin" 或 "operation_admin"。
用户封禁
Better Auth admin 插件内置了用户封禁功能。封禁后该用户的 session 会被标记,后续请求会被拦截。
验证码 (CAPTCHA)
项目使用 Cloudflare Turnstile(一种免费的验证码服务,对用户几乎无感)作为验证码方案,集成在 packages/captcha/ 中。
环境变量
# 是否启用验证码(设为 true 或 1 启用)
CAPTCHA_ENABLED=true
# Cloudflare Turnstile 秘钥
CLOUDFLARE_TURNSTILE_SECRET_KEY=your-turnstile-secret-key后端验证
import { createCaptchaVerifier } from "@01mvp/captcha";
const verifier = createCaptchaVerifier({
enabled: true,
provider: "cloudflare-turnstile",
cloudflare: { secretKey: process.env.CLOUDFLARE_TURNSTILE_SECRET_KEY! },
});
// 在 API 路由中验证
const result = await verifier.verify(tokenFromFrontend, requestIp);
if (!result.success) {
// 验证失败,拒绝请求
}createCaptchaVerifier 接受 enabled: false,此时所有验证自动通过——方便开发环境或不需要验证码的场景。
前端集成
在表单中嵌入 Turnstile widget,用户完成验证后会获得一个 token,将该 token 发送到后端进行验证。具体步骤参考 Cloudflare Turnstile 官方文档。
请求限流
packages/rate-limit/ 提供了一个轻量的速率限制器,可配合中间件使用,防止接口被滥用。
基本用法
import { createRateLimiter, setRateLimitHeaders } from "@01mvp/rate-limit";
// 创建限制器:每个 IP 每 60 秒最多 100 次请求
const limiter = createRateLimiter({
limit: 100,
window: 60, // 时间窗口(秒)
prefix: "api", // key 前缀(可选)
});
// 在 API 路由中检查
const result = await limiter.check(clientIp);
// 设置标准响应头
setRateLimitHeaders(response.headers, result);
if (!result.success) {
return new Response("Too Many Requests", { status: 429 });
}配置说明
| 参数 | 说明 |
|---|---|
limit | 时间窗口内允许的最大请求数 |
window | 时间窗口大小(秒) |
prefix | key 前缀,用于隔离不同场景的限流(可选) |
store | 存储后端,默认使用 MemoryStore;生产环境建议替换为 Redis 实现 |
返回的 RateLimitResult 包含:
success: 是否放行limit: 最大请求数remaining: 剩余可用次数reset: 窗口重置时间(Unix 毫秒时间戳)
setRateLimitHeaders 会自动设置 X-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Reset 和 Retry-After 标准响应头。
内容审核
项目通过 @01mvp/tencent-cloud 包集成腾讯云内容安全服务(TMS 文本审核 + IMS 图片审核)。
文本审核
import { createTencentCloudModerationClient } from "@01mvp/tencent-cloud";
const client = createTencentCloudModerationClient({
secretId: process.env.TENCENT_SECRET_ID!,
secretKey: process.env.TENCENT_SECRET_KEY!,
});
const result = await client.textModeration("待审核的文本内容");
// result.suggestion: "Pass"(通过)| "Block"(拦截)| "Review"(人工复审)
// result.label: 内容分类标签
// result.score: 置信度分数
// result.keywords: 触发的敏感词图片审核
const result = await client.imageModeration("https://example.com/image.jpg");
// result.suggestion: "Pass" | "Block" | "Review"
// result.label: 违规类型标签
// result.subLabel: 细分子标签
// result.score: 置信度分数环境变量
TENCENT_SECRET_ID=your-secret-id
TENCENT_SECRET_KEY=your-secret-key@01mvp/content-moderation 是一个轻量封装层,目前图片审核为 stub 实现(始终返回 { ok: true })。完整的文本和图片审核能力在 @01mvp/tencent-cloud 包中。
跨子域名登录 (SSO)
如果应用有多个子域名(如 app.example.com 和 api.example.com),可以通过设置 COOKIE_DOMAIN 环境变量实现跨子域名单点登录。
# .env.local
COOKIE_DOMAIN=.example.com配置后,Better Auth 的 session cookie 会设置在 .example.com 域下,所有子域名共享同一个登录状态。