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端点