资讯动态

OpenWork Den API 中间件体系解析:Hono 认证、组织上下文与校验器的分层设计

发布时间:2026/9/13 7:29:16 来源:尧图企业网站定制
OpenWork Den API 中间件体系解析Hono 认证、组织上下文与校验器的分层设计【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork导读本文以 ee/apps/den-api/src/middleware/README.md 为核心骨架系统讲解 OpenWork 企业版 Den API 中基于 Hono 构建的可复用中间件层认证管理员白名单 / 登录用户 / 会话、组织上下文用户所属组织、:orgSlug组织与成员上下文、成员团队以及 Zod 校验器。读完本文你将掌握这套中间件的导出面、c.get(...)上下文契约、按需组合的接入方式以及 Den API 默认拒绝deny-by-default的路由访问策略如何在底层用中间件标记强制实施。一、中间件目录的定位与设计哲学Den API位于 ee/apps/den-api是一个以 Hono 为框架的 REST 服务。为了让多个路由区域route areas共享通用的请求处理逻辑仓库在 ee/apps/den-api/src/middleware/README.md 中明确了该目录的定位This folder contains reusable Hono middleware that route areas can compose as needed.这句话点出了两个关键约束可复用reusable中间件必须跨路由区域有价值而不是某个路由的一次性辅助函数按需组合compose as needed路由只装配自己需要的中间件不强制全量挂载。实际目录中除 README 提到的 7 个文件外还包含一个由源码注释反复强调的route-access.ts它承载 Den API 的默认拒绝路由访问策略我们将在第四节单独展开。目录完整文件清单如下ee/apps/den-api/src/middleware/ ├── README.md # 使用说明与设计准则 ├── index.ts # 公共导出面 ├── admin.ts # 需要认证的管理员白名单 ├── current-user.ts # 需要认证的登录用户 ├── route-access.ts # 路由访问策略标记deny-by-default ├── user-organizations.ts # 加载当前用户所属组织 ├── organization-context.ts # 为 :orgSlug 路由加载组织 成员上下文 ├── member-teams.ts # 加载当前组织成员所属团队 └── validation.ts # JSON / query / params 的 Zod 校验包装二、公共导出面 index.ts一条 import 链接入全部中间件ee/apps/den-api/src/middleware/index.ts 是整个中间件目录的门面它不实现任何逻辑只是将各文件以命名导出named exports的方式统一转发export * from ./admin.js export * from ./current-user.js export * from ./route-access.js export * from ./user-organizations.js export * from ./organization-context.js export * from ./member-teams.js export * from ./validation.js注意两点导出使用./xxx.js后缀而非.ts这是 Den API以及该仓库多数 TS 服务遵循的 ESM 风格——源码编译后运行时的导入路径与源码一致README 中给出的推荐用法就是从src/middleware/index.js批量导入例如import { jsonValidator, paramValidator, requireUserMiddleware, resolveOrganizationContextMiddleware, } from ../../middleware/index.js然后只组合路由真正需要的那一部分。这条规则与第四节的访问策略标记体系相互配合认证与上下文中间件不是靠路由手动散装拼凑而是通过route-access.ts中的工厂函数统一返回并登记。三、认证中间件管理员白名单与登录用户会话认证层由 ee/apps/den-api/src/middleware/admin.ts 与 ee/apps/den-api/src/middleware/current-user.ts 提供它们都声明为MiddlewareHandler{ Variables: AuthContextVariables }AuthContextVariables定义于 ee/apps/den-api/src/session.tsexport type AuthContextVariables { user: AuthSessionValue[user] | null session: AuthSessionValue[session] | null apiKey: DenApiKeySession | null }也就是说经过会话层解析后c.get(user)/c.get(session)/c.get(apiKey)已经可用认证中间件只需要在此基础上做二次判定。3.1 requireUserMiddleware最基础的登录门槛ee/apps/den-api/src/middleware/current-user.ts 的逻辑非常精简——只要c.get(user)?.id不存在立即返回401 { error: unauthorized }否则放行next()。export const requireUserMiddleware: MiddlewareHandler{ Variables: AuthContextVariables } async (c, next) { if (!c.get(user)?.id) { return c.json({ error: unauthorized }, 401) as never } await next() }3.2 requireUserSessionMiddleware必须是真人会话与requireUserMiddleware只要求有用户不同requireUserSessionMiddleware 进一步要求请求必须来自签发的用户会话如果走的是 API Keyc.get(apiKey)非空或会话缺少id/token或session.id无法通过 TypeID 归一化都会返回403提示请使用已登录的用户会话执行该操作。这一设计把API Key 自动化调用与需要用户身份的操作如修改会话自身状态区分开避免凭证越权。3.3 requireAdminMiddleware平台管理员白名单ee/apps/den-api/src/middleware/admin.ts 的判定链是无user.id→401 unauthorized邮箱为空normalizeEmail对trim().toLowerCase()归一化后为空→403 admin_email_required邮箱不在管理员白名单 →403 forbidden全部通过才next()。白名单判定核心是 isAdminEmailAllowed它会先调用ensureAdminAllowlistSeeded()确保白名单表完成种子初始化再查询AdminAllowlistTable中是否存在该邮箱isPlatformAdminUserId 则是从用户 ID 反查邮箱后再走同一判定并同时供admin 路由中间件与den-admin MCP 端点两处复用源码注释明确说明这一共享关系。四、路由访问策略Den API 的默认拒绝机制README 未单列、但源码注释极其强调的 ee/apps/den-api/src/middleware/route-access.ts 定义了 Den API 的安全基线。文件头部注释写明Den API routes are deny-by-default: everyapp.get/post/patch/delete/all/onregistration must include one explicit access policy marker from this file.即每条路由注册都必须显式挂一个访问策略标记否则test/route-access-policy.test.ts会在 CI 中失败。这从工程上杜绝了忘记加认证这类低级漏洞。各标记及其语义如下标记工厂/常量语义底层中间件publicRoute公开路由空操作放行signedWebhookRoute签名 Webhook空操作放行鉴权在 handler 内做专门校验tokenRoutetoken 路由空操作放行handler 内专门校验delegatedRoute委托代理路由空操作放行handler 内专门校验authenticatedRoute()登录用户requireUserMiddlewareuserSessionRoute()用户会话requireUserSessionMiddlewareadminRoute()白名单管理员requireAdminMiddlewareorgMemberRoute()组织成员默认resolveOrganizationContextMiddleware传{ useUserOrganizations: true }时用resolveUserOrganizationsMiddlewareorgRoleRoute(roles)组织角色校验先解析组织上下文再按角色层级校验cloudTransportRoute()MCP 云传输校验 MCP 请求签名、DEN_MCP_WRITE_SCOPE作用域与组织成员关系关键实现细节登记机制hasExplicitAuthGuardHandler 依赖explicitAuthGuardHandlers一个WeakSetobject内置中间件及orgRoleRoute动态生成的 handler 都会被登记进去供路由访问策略测试检测是否显式挂载了合法标记角色判定verifyOrgRole 中如果所需角色列表包含member则直接放行否则调用organizationRoleValueSatisfies定义于 ee/apps/den-api/src/organization-role-hierarchy.ts按组织角色层级比较当前成员角色是否满足要求isOwner作为最高权限参与比较云传输路径cloudTransportRouteHandler通过verifyMcpRequest校验 MCP 请求主体验证、要求携带DEN_MCP_WRITE_SCOPE再调用getOrganizationContextForUser加载组织上下文成员关系被撤销时返回403 mcp_membership_revoked并把上下文写入c.set(organizationContext, ...)。五、组织上下文中间件user → organizations → orgContext → teams这一组中间件解决当前请求发生在哪个组织、该用户在该组织中是什么角色的问题是 Den API 多租户路由的基础。README 列出的上下文值中userOrganizations/activeOrganizationId/activeOrganizationSlug/organizationContext/memberTeams全部由这一组产出。5.1 resolveUserOrganizationsMiddleware用户视角的组织列表ee/apps/den-api/src/middleware/user-organizations.ts 的执行顺序是无user.id→401确定作用域组织 IDscopedOrganizationId优先取 API Key 绑定的组织getApiKeyScopedOrganizationId其次取请求头指定的组织请求头支持两个组织 ID 头x-openwork-org-idORG_SCOPE_HEADER与兼容旧版代理的x-openwork-legacy-org-idLEGACY_ORG_PROXY_HEADER后者作为前者缺失时的回退调用resolveUserOrganizations解析用户组织若存在作用域组织则把组织列表过滤到仅该组织设置userOrganizations、activeOrganizationId、activeOrganizationSlug三个上下文变量。此外它还承担会话活性组织水合session hydration当请求没有显式作用域、会话也没有记录活跃组织、但解析出了默认活跃组织时调用hydrateSessionActiveOrganization把该组织写回 Better Auth 会话并同步更新c.set(session, ...)——这样用户下一次访问时活跃组织状态已经持久化无需重复解析。5.2 resolveOrganizationContextMiddleware组织 成员全量上下文ee/apps/den-api/src/middleware/organization-context.ts 面向:orgSlug类路由产出 README 中描述的organizationContextorg 记录、当前成员、成员列表、邀请、角色。它的解析优先级是API Key 作用域组织 → 请求头组织 ID → 已有activeOrganizationId→ 会话活跃组织 → 用户默认活跃组织调用getOrganizationContextForUser({ userId, organizationId })加载上下文加载失败且无显式作用域时回退到用户默认组织重试无组织可解析 →404 organization_not_foundAPI Key 作用域校验若使用 API Key则必须与目标组织一致isScopedApiKeyForOrganization否则403This API key is scoped to a different organization.若 API Key 元数据中绑定了orgMembershipId还必须与当前成员的id一致否则403no longer valid for the current organization member——这能实时撤销离职成员已签发的组织级 API Key最终写入organizationContext、activeOrganizationId、activeOrganizationSlug。5.3 resolveMemberTeamsMiddleware当前成员所在团队ee/apps/den-api/src/middleware/member-teams.ts 是组织上下文的下游依赖它要求c.get(organizationContext)必须先被解析否则返回500 organization_context_required这是一个编码错误而非客户端错误。随后基于context.organization.id与context.currentMember.id调用listTeamsForMember把结果写入memberTeams。它不重复做认证也不重复查组织充分体现按需组合、各司其职的分层思想。六、校验中间件Zod 驱动的 400 响应标准化ee/apps/den-api/src/middleware/validation.ts 基于hono-openapi的validator as zValidator封装了三个校验器覆盖 Hono 的三类输入位置export function jsonValidatorT extends ZodSchema(schema: T) // 请求体 JSON export function queryValidatorT extends ZodSchema(schema: T) // URL query export function paramValidatorT extends ZodSchema(schema: T) // 路径参数三个函数结构一致把 Zod schema 交给zValidator当校验失败时统一返回400 { error: invalid_request, details: result.error }。details携带 Zod 原始的校验错误对象字段名、错误路径、错误信息前端与调试工具可直接据此定位非法字段。使用示例import { z } from zod import { jsonValidator, paramValidator } from ../../middleware/index.js app.post( /api/orgs/:orgSlug/members, paramValidator(z.object({ orgSlug: z.string().min(1) })), jsonValidator(z.object({ email: z.string().email(), role: z.string() })), handler, )由于校验失败在中间件阶段即被拦截业务 handler 内可以放心地把参数当作已通过 schema 校验的类型使用无需再写重复的防御式判断。七、上下文契约速查路由内可以安全读取什么结合 README 的 Available context 与源码实现中间件层向后续 handler 提供的c.get(...)契约可汇总如下上下文键类型/内容由谁写入典型使用场景user当前认证用户session.ts 的sessionMiddleware判定身份、展示个人信息session当前 Better Auth 会话sessionMiddleware会话级操作、活跃组织水合apiKey当前 API Key 会话sessionMiddleware区分凭证来源、作用域校验userOrganizations当前用户所属组织摘要数组resolveUserOrganizationsMiddleware组织列表 UI、切换组织activeOrganizationId当前活跃组织 IDTypeID用户组织 / 组织上下文中间件多租户数据过滤activeOrganizationSlug当前活跃组织 slug用户组织 / 组织上下文中间件路由重定向、URL 构造organizationContextorg 记录、当前成员、成员、邀请、角色resolveOrganizationContextMiddleware含cloudTransportRouteHandler:orgSlug路由、成员管理、角色判定memberTeams当前组织成员所属团队摘要resolveMemberTeamsMiddleware团队级授权与数据过滤需要说明的是user/session/apiKey的真正来源是 session.ts 中的sessionMiddleware它按内部 MCP principal 头 → 签名 Cookie 会话 → Bearer token的优先级解析身份API Key 优先于 Cookie 解析且登出请求/api/auth/sign-out会跳过会话解析。内部 MCP principal 头使用进程内随机密钥做 HMAC-SHA256 签名并设 60 秒 TTL将信任边界绑定到进程内调用方。中间件目录中的各认证中间件正是消费这套已解析的身份再做授权判定。八、组合示例一个典型的组织管理路由将以上中间件按最小必要原则组合可以得到 Den API 中组织管理路由的标准写法import { Hono } from hono import { jsonValidator, paramValidator, orgMemberRoute, orgRoleRoute, } from ../../middleware/index.js const orgRoutes new Hono{ Variables: AuthContextVariables }() // 所有 /:orgSlug 路由先解析组织上下文 orgRoutes.use(/:orgSlug/*, orgMemberRoute()) // 只有组织 owner 能读取成员列表 orgRoutes.get( /:orgSlug/members, orgRoleRoute([owner]), async (c) { const { organizationContext } c.get(organizationContext) return c.json(organizationContext.members) }, ) // 新增成员路径参数 请求体双重校验 orgRoutes.post( /:orgSlug/members, orgRoleRoute([owner, admin]), paramValidator(z.object({ orgSlug: z.string() })), jsonValidator(z.object({ email: z.string().email() })), async (c) { /* ... */ }, )这里可以清楚看到各中间件的分工orgMemberRoute()负责谁有资格访问这个组织成员判定 上下文注入orgRoleRoute([...])负责该成员是否有权限做这件事角色层级判定paramValidator/jsonValidator负责请求数据是否合法。三者叠加后handler 内几乎只剩纯业务逻辑。九、设计准则何时放入中间件目录README 以 Rule of thumb 给出两条边界清晰的准则这也是判断代码归属的决策树If a value is broadly useful across multiple route areas, put it here—— 如果某个能力被多个路由区域复用认证、组织上下文、通用校验放入本目录并在 index.ts 统一导出If a helper only exists for one route area, keep it in that route folder instead—— 如果只是单个路由区域的一次性辅助函数应留在该路由目录内部避免中间件目录变成杂物抽屉。配合route-access.ts的默认拒绝策略与test/route-access-policy.test.ts的 CI 强制校验Den API 在中间件复用与路由安全之间形成了互相支撑的闭环公共逻辑集中、可测试、可审计路由显式声明访问策略漏挂即失败。对于希望在自己的 Hono 服务中建立类似多租户中间件层的团队这套认证 → 组织上下文 → 角色授权 → 输入校验的分层与组合模式是可直接借鉴的范本。【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价