端点保护Skill bknd-protect-endpoint

此技能用于在Bknd框架中保护特定API端点,实现认证、授权、角色检查和权限管理,确保后端API安全,关键词包括:API端点保护、Bknd、认证、授权、角色权限、HTTP触发器、插件路由、Guard权限系统。

后端开发 0 次安装 0 次浏览 更新于 3/8/2026

name: bknd-protect-endpoint description: 在Bknd中保护特定API端点时使用。涵盖保护自定义HTTP触发器、插件路由、Flows的认证中间件、自定义端点的权限检查以及基于角色的端点访问。

保护端点

通过认证和授权检查保护特定API端点。

前提条件

  • Bknd项目,采用代码优先配置
  • 启用认证(auth: { enabled: true }
  • 启用授权守卫(guard: { enabled: true }
  • 定义角色(参见 bknd-create-role

何时使用UI模式

  • 查看已注册的路由:管理面板 > 系统 > 调试
  • 检查角色权限

注意: 端点保护需要代码模式。UI是只读的。

何时使用代码模式

  • 创建受保护的自定义端点
  • 向HTTP触发器添加认证检查
  • 构建受保护的插件路由
  • 实现端点特定权限

代码方法

理解端点类型

Bknd有几种端点类型需要保护:

类型 路径模式 如何保护
数据API /api/data/* 守卫权限(自动)
认证API /api/auth/* 内置保护
媒体API /api/media/* 守卫权限(自动)
HTTP触发器 自定义路径 手动认证检查
插件路由 自定义路径 手动认证检查

步骤1:保护HTTP触发器(Flow)

通过FunctionTask向自定义端点添加认证:

import { serve } from "bknd/adapter/bun";
import { Flow, HttpTrigger, FunctionTask } from "bknd";

// 受保护端点流程
const protectedFlow = new Flow("protected-endpoint", [
  new FunctionTask({
    name: "checkAuth",
    handler: async (input, ctx) => {
      // ctx.app提供对模块的访问
      const authModule = ctx.app.modules.get("auth");
      const user = await authModule.authenticator.getUserFromRequest(input);

      if (!user) {
        throw new Response(JSON.stringify({ error: "未授权" }), {
          status: 401,
          headers: { "Content-Type": "application/json" },
        });
      }

      // 将用户传递给下一个任务
      return { user, body: await input.json() };
    },
  }),
  new FunctionTask({
    name: "processRequest",
    handler: async (input) => {
      // input包含从前一个任务传来的{ user, body }
      return {
        message: `你好 ${input.user.email}`,
        data: input.body,
      };
    },
  }),
]);

protectedFlow.setTrigger(
  new HttpTrigger({
    path: "/api/custom/protected",
    method: "POST",
    respondWith: "processRequest",
  })
);

serve({
  connection: { url: "file:data.db" },
  config: {
    flows: {
      flows: [protectedFlow],
    },
  },
});

步骤2:保护插件路由

在插件的onServerInit中添加认证检查:

import { serve } from "bknd/adapter/bun";
import { createPlugin } from "bknd";

const protectedPlugin = createPlugin({
  name: "protected-routes",

  onServerInit: (server) => {
    // 受保护端点
    server.post("/api/custom/data", async (c) => {
      // 从上下文获取应用
      const app = c.get("app");
      const authModule = app.modules.get("auth");

      // 从请求解析用户
      const user = await authModule.authenticator.getUserFromRequest(c.req.raw);

      if (!user) {
        return c.json({ error: "未授权" }, 401);
      }

      // 处理受保护逻辑
      const body = await c.req.json();
      return c.json({
        message: "受保护数据",
        user: user.email,
        received: body,
      });
    });

    // 公共端点(无认证检查)
    server.get("/api/custom/public", (c) => {
      return c.json({ message: "公共数据" });
    });
  },
});

serve({
  connection: { url: "file:data.db" },
  plugins: [protectedPlugin],
});

步骤3:基于角色的端点保护

检查用户角色以进行特定权限:

const roleProtectedPlugin = createPlugin({
  name: "role-protected",

  onServerInit: (server) => {
    // 仅管理员端点
    server.delete("/api/admin/users/:id", async (c) => {
      const app = c.get("app");
      const authModule = app.modules.get("auth");
      const user = await authModule.authenticator.getUserFromRequest(c.req.raw);

      // 检查认证
      if (!user) {
        return c.json({ error: "未授权" }, 401);
      }

      // 检查角色
      if (user.role !== "admin") {
        return c.json({ error: "禁止访问:需要管理员角色" }, 403);
      }

      // 执行管理员操作
      const userId = c.req.param("id");
      // ... 删除用户逻辑
      return c.json({ deleted: userId });
    });
  },
});

步骤4:使用Guard进行基于权限的保护

使用Guard进行精细权限检查:

import { createPlugin, DataPermissions } from "bknd";

const guardProtectedPlugin = createPlugin({
  name: "guard-protected",

  onServerInit: (server) => {
    server.post("/api/custom/sync", async (c) => {
      const app = c.get("app");
      const authModule = app.modules.get("auth");
      const guard = authModule.guard;

      const user = await authModule.authenticator.getUserFromRequest(c.req.raw);

      if (!user) {
        return c.json({ error: "未授权" }, 401);
      }

      // 使用Guard检查特定权限
      try {
        guard.granted(
          DataPermissions.databaseSync,  // 要检查的权限
          { role: user.role },           // 用户上下文
          {}                             // 权限上下文
        );
      } catch (error) {
        return c.json({
          error: "禁止访问",
          message: error.message,
        }, 403);
      }

      // 用户有权限 - 继续
      return c.json({ status: "同步已启动" });
    });
  },
});

步骤5:实体特定权限检查

检查特定实体操作的权限:

server.post("/api/custom/posts/batch", async (c) => {
  const app = c.get("app");
  const authModule = app.modules.get("auth");
  const guard = authModule.guard;

  const user = await authModule.authenticator.getUserFromRequest(c.req.raw);

  if (!user) {
    return c.json({ error: "未授权" }, 401);
  }

  // 检查帖子实体的创建权限
  try {
    guard.granted(
      DataPermissions.entityCreate,
      { role: user.role },
      { entity: "posts" }  // 实体特定上下文
    );
  } catch (error) {
    return c.json({
      error: "无法创建帖子",
      message: error.message,
    }, 403);
  }

  // 处理批量创建
  const body = await c.req.json();
  // ... 创建帖子
  return c.json({ created: body.length });
});

步骤6:可重用的认证中间件

创建用于一致认证检查的辅助函数:

// auth-middleware.ts
type AuthContext = {
  user: any;
  role: string;
};

export async function requireAuth(
  c: any,
  app: any
): Promise<AuthContext | Response> {
  const authModule = app.modules.get("auth");
  const user = await authModule.authenticator.getUserFromRequest(c.req.raw);

  if (!user) {
    return c.json({ error: "未授权" }, 401);
  }

  return { user, role: user.role };
}

export async function requireRole(
  c: any,
  app: any,
  allowedRoles: string[]
): Promise<AuthContext | Response> {
  const result = await requireAuth(c, app);

  if (result instanceof Response) {
    return result;
  }

  if (!allowedRoles.includes(result.role)) {
    return c.json({
      error: "禁止访问",
      required: allowedRoles,
      current: result.role,
    }, 403);
  }

  return result;
}

// 在插件中使用
server.get("/api/reports/admin", async (c) => {
  const app = c.get("app");
  const auth = await requireRole(c, app, ["admin", "manager"]);

  if (auth instanceof Response) return auth;

  // auth.user可用
  return c.json({ reports: [] });
});

步骤7:使用认证任务保护Flow

为Flows创建可重用的认证任务:

import { Flow, HttpTrigger, FunctionTask } from "bknd";

// 可重用认证任务
const authTask = new FunctionTask({
  name: "requireAuth",
  handler: async (input, ctx) => {
    const authModule = ctx.app.modules.get("auth");
    const user = await authModule.authenticator.getUserFromRequest(input);

    if (!user) {
      throw new Response(
        JSON.stringify({ error: "未授权" }),
        { status: 401, headers: { "Content-Type": "application/json" } }
      );
    }

    return { request: input, user };
  },
});

// 可重用角色检查任务
const requireAdmin = new FunctionTask({
  name: "requireAdmin",
  handler: async (input) => {
    if (input.user.role !== "admin") {
      throw new Response(
        JSON.stringify({ error: "需要管理员" }),
        { status: 403, headers: { "Content-Type": "application/json" } }
      );
    }
    return input;
  },
});

// 受保护流程
const adminFlow = new Flow("admin-action", [
  authTask,
  requireAdmin,
  new FunctionTask({
    name: "performAction",
    handler: async (input) => {
      return { success: true, admin: input.user.email };
    },
  }),
]);

adminFlow.setTrigger(
  new HttpTrigger({
    path: "/api/admin/action",
    method: "POST",
    respondWith: "performAction",
  })
);

常见模式

可选认证(公开带额外功能)

server.get("/api/posts", async (c) => {
  const app = c.get("app");
  const authModule = app.modules.get("auth");
  const api = app.getApi();

  // 尝试获取用户(可能为null)
  const user = await authModule.authenticator.getUserFromRequest(c.req.raw);

  if (user) {
    // 已认证:显示所有帖子,包括草稿
    const posts = await api.data.readMany("posts", {
      where: {
        $or: [
          { status: "published" },
          { author_id: user.id },
        ],
      },
    });
    return c.json(posts.data);
  } else {
    // 匿名:仅显示已发布
    const posts = await api.data.readMany("posts", {
      where: { status: "published" },
    });
    return c.json(posts.data);
  }
});

受限制的受保护端点

const rateLimits = new Map<string, { count: number; reset: number }>();

server.post("/api/expensive-operation", async (c) => {
  const app = c.get("app");
  const authModule = app.modules.get("auth");
  const user = await authModule.authenticator.getUserFromRequest(c.req.raw);

  if (!user) {
    return c.json({ error: "未授权" }, 401);
  }

  // 按用户简单限制
  const key = `user:${user.id}`;
  const now = Date.now();
  const limit = rateLimits.get(key);

  if (limit && limit.reset > now && limit.count >= 10) {
    return c.json({
      error: "速率限制超出",
      retryAfter: Math.ceil((limit.reset - now) / 1000),
    }, 429);
  }

  // 更新速率限制
  if (!limit || limit.reset < now) {
    rateLimits.set(key, { count: 1, reset: now + 60000 });
  } else {
    limit.count++;
  }

  // 继续
  return c.json({ result: "成功" });
});

API密钥认证

用于服务到服务或外部API访问:

const API_KEYS = new Set([
  process.env.SERVICE_API_KEY,
  process.env.PARTNER_API_KEY,
]);

server.post("/api/webhook/external", async (c) => {
  const apiKey = c.req.header("X-API-Key");

  if (!apiKey || !API_KEYS.has(apiKey)) {
    return c.json({ error: "无效API密钥" }, 401);
  }

  // 处理webhook
  const body = await c.req.json();
  return c.json({ received: true });
});

验证

1. 测试未认证访问

# 应返回401
curl -X POST http://localhost:7654/api/custom/protected \
  -H "Content-Type: application/json" \
  -d '{"test": "data"}'

2. 测试认证访问

# 先登录
TOKEN=$(curl -s -X POST http://localhost:7654/api/auth/password/login \
  -H "Content-Type: application/json" \
  -d '{"email": "user@test.com", "password": "pass123"}' | jq -r '.token')

# 访问受保护端点
curl -X POST http://localhost:7654/api/custom/protected \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"test": "data"}'

3. 测试角色限制

# 以非管理员身份登录
TOKEN=$(curl -s -X POST http://localhost:7654/api/auth/password/login \
  -H "Content-Type: application/json" \
  -d '{"email": "user@test.com", "password": "pass123"}' | jq -r '.token')

# 应返回403
curl -X DELETE http://localhost:7654/api/admin/users/1 \
  -H "Authorization: Bearer $TOKEN"

4. 以管理员角色验证

# 以管理员身份登录
ADMIN_TOKEN=$(curl -s -X POST http://localhost:7654/api/auth/password/login \
  -H "Content-Type: application/json" \
  -d '{"email": "admin@test.com", "password": "admin123"}' | jq -r '.token')

# 应成功
curl -X DELETE http://localhost:7654/api/admin/users/1 \
  -H "Authorization: Bearer $ADMIN_TOKEN"

常见陷阱

用户始终为Null

问题: 即使有有效令牌,getUserFromRequest()也返回null

修复: 确保令牌正确发送:

// 头部认证
fetch("/api/custom/protected", {
  headers: { "Authorization": `Bearer ${token}` }
});

// 或Cookie认证(如使用Cookie)
fetch("/api/custom/protected", {
  credentials: "include"  // 发送Cookie
});

Guard不可用

问题: authModule.guard未定义

修复: 确保Guard已启用:

{
  auth: {
    enabled: true,
    guard: { enabled: true },  // 必需!
  },
}

权限检查抛出错误错误

问题: Guard抛出意外错误类型

修复: 捕获特定异常:

import { GuardPermissionsException } from "bknd";

try {
  guard.granted(permission, context, permContext);
} catch (error) {
  if (error instanceof GuardPermissionsException) {
    return c.json({ error: error.message }, 403);
  }
  throw error;  // 重新抛出意外错误
}

CORS阻止认证头部

问题: 预检请求因Authorization头部失败

修复: 配置CORS:

serve({
  // ...
  config: {
    server: {
      cors: {
        origin: ["http://localhost:3000"],
        credentials: true,
        allowHeaders: ["Authorization", "Content-Type"],
      },
    },
  },
});

Flow任务无应用上下文

问题: FunctionTask中的ctx.app未定义

修复: 通过执行上下文访问:

new FunctionTask({
  name: "withApp",
  handler: async (input, ctx) => {
    // ctx.app在FunctionTask中可用
    const app = ctx.app;
    // ...
  },
});

要做和不要做

要做:

  • 在处理敏感请求前始终检查认证
  • 使用Guard进行权限检查(与Bknd系统一致)
  • 返回适当的HTTP状态码(401, 403)
  • 创建可重用的认证辅助函数以确保一致性
  • 记录认证失败以进行安全监控

不要做:

  • 未经验证信任客户端提供的用户ID
  • 暴露认证失败的详细错误消息
  • 假设“内部”端点是安全的而跳过认证检查
  • 在JWT负载中存储敏感数据(仅使用用户ID)
  • 忘记处理头部和Cookie两种认证方法

相关技能

  • bknd-create-role - 定义角色用于授权
  • bknd-assign-permissions - 配置角色权限
  • bknd-public-vs-auth - 公共vs认证访问
  • bknd-row-level-security - 数据级访问控制
  • bknd-custom-endpoint - 创建自定义API端点