功能指南

权限与风控

权限模型、路由保护、双因素认证、验证码、请求限流和内容审核

概览

项目内置了多层安全能力,从用户权限到接口风控一应俱全:

能力说明
权限模型基于 CASL 的声明式权限判断(Role / Action / Subject)
管理员权限SUPER_ADMIN、OPERATION_ADMIN 两级管理角色
路由保护按 URL 模式匹配的认证/权限/订阅检查
双因素认证Better Auth twoFactor 插件,支持 TOTP + 备份码
验证码Cloudflare Turnstile 集成
请求限流可配置窗口的速率限制器(RateLimiter)
内容审核腾讯云文本 / 图片内容安全
跨域登录通过 COOKIE_DOMAIN 实现跨子域名 Cookie SSO

权限模型

项目使用 CASL(一个权限判断库,帮你检查用户能不能做某件事)实现声明式的权限控制。核心概念:

  • Role(角色):USERVIPADMIN
  • Action(操作):createreadupdatedeletemanage(manage 表示所有操作)
  • Subject(对象):UserProjectSubscriptionSettingsall

各角色的默认权限:

角色ProjectUser(自身)SubscriptionSettings
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.tsdefineAbilitiesFor 函数中添加规则:

// 示例:给 VIP 用户增加查看订阅详情的权限
can(Action.READ, Subject.SUBSCRIPTION);

如果需要新增 Subject(对象类型),在 packages/permissions/src/types.tsSubject 枚举中添加即可。

路由保护

路由保护模块(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),不会影响应用启动。

用户流程

  1. 用户在设置页选择「启用双因素认证」
  2. 系统生成 TOTP 密钥,前端展示二维码
  3. 用户用 Authenticator App 扫描二维码
  4. 输入 App 显示的 6 位验证码完成验证
  5. 系统同时生成一组备份码,用户需妥善保存

数据库模型

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.enabletwoFactor.verifytwoFactor.disable 等方法。

管理员系统

管理员权限基于 Better Auth 的 admin 插件,配合 packages/auth/src/permissions.ts 中的细粒度权限模型。

角色与权限

两个管理员角色,权限粒度不同:

权限SUPER_ADMINOPERATION_ADMIN
查看用户yesyes
管理用户yesyes
封禁用户yesyes
分配角色yes-
查看职能角色yesyes
管理职能角色yes-
查看贡献yesyes
审核贡献yesyes
查看勋章yesyes
管理勋章yesyes
颁发勋章yesyes
查看组织yesyes
管理组织yes-
查看系统配置yes-
管理系统配置yes-
查看仪表板yesyes

在代码中检查管理员权限:

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时间窗口大小(秒)
prefixkey 前缀,用于隔离不同场景的限流(可选)
store存储后端,默认使用 MemoryStore;生产环境建议替换为 Redis 实现

返回的 RateLimitResult 包含:

  • success: 是否放行
  • limit: 最大请求数
  • remaining: 剩余可用次数
  • reset: 窗口重置时间(Unix 毫秒时间戳)

setRateLimitHeaders 会自动设置 X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-ResetRetry-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.comapi.example.com),可以通过设置 COOKIE_DOMAIN 环境变量实现跨子域名单点登录。

# .env.local
COOKIE_DOMAIN=.example.com

配置后,Better Auth 的 session cookie 会设置在 .example.com 域下,所有子域名共享同一个登录状态。

常见问题

相关资源