用 iii-browser-sdk 把浏览器变成一等 Worker:RBAC 网关接入、实时点击流与服务器反向调用实战
2026/9/15 1:59:39 网站建设 项目流程

用 iii-browser-sdk 把浏览器变成一等 Worker:RBAC 网关接入、实时点击流与服务器反向调用实战

【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii

导读

本篇是 Linkly 全栈教程(教程总览)第 7 章的完整实战:把浏览器标签页变成一个与本地 worker 完全同级的 iii worker。你将掌握三件事——通过iii-worker-manager的 RBAC 网关(auth函数 +expose_functions白名单)安全接入不受信任的浏览器连接;用iii-browser-sdk直接调用link::create而无需任何 REST API 网关;以及订阅clicks实时流刷新计数器,并注册一个由服务器反向调用的user::confirm_destructive_op函数实现“删除前人工确认”。读完本章,你会看到客户端与服务器端在 iii 总线上再无功能差别,只有鉴权与权限管理的差异。

本章目标:让浏览器成为第 N 个 worker

在 Linkly 前几章中,linkanalyticsclick-streamer等 worker 都通过引擎内置端口连接。本章要把浏览器也拉上同一条总线:

  • 浏览器经 WebSocket 直连引擎,直接调用link::create(中间没有 REST API 网关);
  • 订阅clicks实时流,更新页面上的实时点击计数器;
  • 注册一个user::confirm_destructive_op函数,当服务器要删除链接时反向询问浏览器里的真人

关键差异在于信任模型:浏览器不是你亲手运行的进程,不能像本地 worker 一样默认信任。因此浏览器连接必须走iii-worker-managerRBAC 网关监听器,与本地 worker 使用的可信端口分开。

第一步:创建 auth worker

认证逻辑单独封装进一个authworker,让linkworker 继续只关注链接本身。先在项目目录下脚手架一个新的 TypeScript worker(若你刚从第 6 章test-channels目录过来,先cd ..):

iii worker init auth --language typescript

第二步:同时跑两个监听器

引擎内置端口49134可信监听器,本地 worker(link、analytics 等)都从这里接入;浏览器绝不能走这里。在config.yaml中声明两个iii-worker-manager:一个保留可信的49134,一个在3110上提供 RBAC 网关给浏览器:

workers: # ... # Trusted listener for local workers. Replaces the engine's built-in 49134. - name: iii-worker-manager config: port: 49134 # Browser-facing listener. The auth function gates every connection; only the # functions in `expose_functions` are reachable from sessions it admits. - name: iii-worker-manager config: host: 127.0.0.1 port: 3110 rbac: auth_function_id: auth::browser expose_functions: - match("link::create") - match("link::request_delete") - match("stream::*")

两个字段的分工:

  • expose_functions允许列表:一个浏览器会话能调用哪些函数 ID,每条规则是函数 ID 上的通配符match("...")
  • auth_function_id指定iii-worker-manager每次连接时调用的鉴权函数,用它决定准入或拒绝,就是你下一步要写的那个函数。

关于可信监听器的替换规则

有一个必须牢记的细节(同样写在 worker-manager 访问控制文档 中):只要在config.yaml里声明了任意一个iii-worker-manager,默认的49134可信监听器就会被替换掉。所以你必须把可信的49134监听器和 RBAC 监听器一起声明,否则你自己的 worker 会失去它们原本的接入端口。

第三步:用 auth 函数把关每个连接

auth::browser在每个浏览器连接建立时运行一次:它收到请求的headersquery_paramsip_address,返回该会话的权限(额外的允许/拒绝项、任意上下文);抛异常即拒绝连接。替换生成的auth/src/index.ts

import { registerWorker } from "iii-sdk"; import { Logger } from "@iii-dev/helpers/observability"; const worker = registerWorker(process.env.III_URL ?? "ws://localhost:49134", { workerName: "auth", }); const logger = new Logger(); worker.registerFunction( "auth::browser", async (input: { headers: Record<string, string>; query_params: Record<string, string[]>; ip_address: string; }) => { const token = input.query_params.token?.[0]; if (!token || token !== (process.env.LINKLY_BROWSER_TOKEN ?? "dev-token")) { throw new Error("unauthorized"); } return { allowed_functions: [], forbidden_functions: [], allow_trigger_type_registration: false, allow_function_registration: true, context: { source: "browser" }, }; }, ); logger.info("auth worker ready");

几点值得展开:

  • token 为什么走 query 参数:浏览器无法发送自定义 WebSocket 头,所以 token 只能以 URL 查询参数携带。生产环境应当从会话存储(session store)查 token,但函数签名与返回结构保持不变。
  • auth worker 自身走可信端口:它通过III_URL(默认ws://localhost:49134)连接可信监听器——它是你自己的 worker;只有不受信任的浏览器客户端从3110进来。这个连接拓扑在 worker-manager 文档 中有明确图示说明。
  • 返回对象就是AuthResult,完整字段语义如下:
字段含义
allowed_functionsexpose_functions之外额外允许的函数 ID
forbidden_functions即使匹配也拒绝的函数 ID,优先级最高
allowed_trigger_types会话可以绑定的触发器类型,省略则允许全部
allow_trigger_type_registration会话是否可以注册新的触发器类型
allow_function_registration会话是否可以注册函数(默认true
context任意对象,转发给每次调用的中间件
function_registration_prefix应用于会话注册的每个函数的前缀

示例返回中allowed_functionsforbidden_functions都为空、allow_function_registrationtrue,意味着会话可注册自己的回调(前端需要注册ui::on_clickuser::confirm_destructive_op),但不能注册新的触发器类型。

注册到项目:

iii worker add ./auth

源码层面的 RBAC 决策流

从引擎侧源码 rbac_config.rs 可以看到,一次调用的放行判定顺序(该文件注释中的决策流)为:

  1. function_idforbidden_functions中 →拒绝(且全局生效,跨命名空间);
  2. 命中会话按命名空间授予的 grants(session_namespaces)→ 放行;
  3. 无命名空间的allowed_functions仅在default命名空间生效 → 放行;
  4. INFRASTRUCTURE_FUNCTIONS(如engine::log::*engine::workers::registerengine::channels::create始终放行,保证连接建立、日志、上下文传播可用;
  5. 任一expose_functions过滤规则匹配 → 放行;
  6. 否则拒绝(默认拒绝,fail-closed)。

expose_functionsmatch("...")通配符语法也由该文件实现:支持前缀engine::*、后缀*::public、中间api::*::read等任意*通配,另有基于函数metadata的过滤规则(见 worker-manager 文档 的expose_functions小节)。引擎的端到端测试 rbac_infrastructure_e2e.rs 验证了“受限expose_functions下基础设施函数仍可用”与“forbidden_functions优先于基础设施放行”这两条关键性质。

第四步:服务器发起的删除(反向调用)

先给linkworker 增加一个真正的link::delete,同时清理数据库与state缓存:

worker.registerFunction("link::delete", async (payload: { code: string }) => { await worker.trigger({ function_id: "database::execute", payload: { db: DB, sql: "DELETE FROM links WHERE code = ?", params: [payload.code] }, }); await worker.trigger({ function_id: "state::delete", payload: { scope: "links", key: payload.code }, }); logger.info("link deleted", { code: payload.code }); return { deleted: true }; });

再包一层“先问浏览器、确认后才删”的封装。服务器端worker.trigger一个浏览器注册的函数,用的正是你在服务器 worker 之间用过无数次的原语,只是方向反转:不是浏览器问服务器要确认,而是服务器问浏览器。别的架构要用 SSE 才能做到的事,这里就是普通的同步代码:

worker.registerFunction("link::request_delete", async (payload: { code: string }) => { const { confirmed } = await worker.trigger< { code: string; action: string }, { confirmed: boolean } >({ function_id: "user::confirm_destructive_op", payload: { code: payload.code, action: `delete link "${payload.code}"` }, }); if (!confirmed) { return { deleted: false }; } await worker.trigger({ function_id: "link::delete", payload: { code: payload.code } }); return { deleted: true }; });

第五步:脚手架前端

初始化 Vite 项目

linkly/frontend/下创建 Vite + React + TypeScript 应用:

npm create vite@latest frontend -- --template react-ts

注意:Vite 会询问 “Install with npm and start now”,此处回答 no,因为我们还要先装iii-browser-sdk。随后安装依赖:

cd frontend npm install npm install iii-browser-sdk

建立客户端 worker

frontend/src/iii.ts中把 SDK 连到 iii 实例。注意这里使用了与服务器 worker 不同的 URL 格式和端口——这正是前面iii-worker-manager+ RBAC 配置的落地:

import { registerWorker } from "iii-browser-sdk"; const TOKEN = import.meta.env.VITE_LINKLY_TOKEN ?? "dev-token"; export const worker = registerWorker(`ws://localhost:3110?token=${encodeURIComponent(TOKEN)}`);

VITE_LINKLY_TOKEN与服务器端LINKLY_BROWSER_TOKEN(或默认dev-token)必须一致,因为auth::browser校验的就是这个 token。

浏览器 SDK 的连接行为

从浏览器 SDK 源码 sdk/packages/node/iii-browser/src/iii.ts 可以看到registerWorker的几个值得注意的默认行为:

  • 浏览器没有 pid 可区分身份,workerName默认为browser:<随机后缀>,避免两个标签页共享固定名字时互相驱逐;
  • 浏览器没有process.env,命名空间必须通过options.namespace显式传入,否则走引擎的default
  • 内置断线重连(指数退避 + 抖动)、调用超时默认 30000ms、连接状态监听等能力与 Node SDK 一致;
  • 浏览器 WebSocket 无法携带自定义头,SDK 明确忽略headers选项,鉴权凭证只能走 query 参数或 cookie——这与前面 token 走 query 的设计相互印证。

第六步:编写应用

下面按片段组装src/App.tsx,直接用这些代码替换模板生成的src/App.tsx

引入与类型

Clickclicks表的一行,StreamEventiii-stream投递给订阅者的包装结构:

import { useEffect, useState } from "react"; import { worker } from "./iii.js"; type Click = { code: string; clicked_at: string }; type StreamEvent = { event: { type: "create" | "update" | "delete"; data: Click }; };

客户端状态

表单字段、新建的链接、实时点击计数:

export default function App() { const [url, setUrl] = useState('') const [code, setCode] = useState('') const [created, setCreated] = useState<{ code: string; url: string } | null>(null) const [clicks, setClicks] = useState(0) const [latest, setLatest] = useState<Click | null>(null)

订阅clicks实时流

订阅第 5 章(streaming.mdx)搭好的clicks流。useEffect注册一个浏览器暴露的函数ui::on_click,再注册一个stream触发器把每条新记录路由给它;卸载时两者一起注销:

useEffect(() => { const fn = worker.registerFunction("ui::on_click", async (event: StreamEvent) => { setClicks((n) => n + 1); setLatest(event.event.data); return null; }); const trig = worker.registerTrigger({ type: "stream", function_id: "ui::on_click", config: { stream_name: "clicks", group_id: "all" }, }); return () => { trig.unregister(); fn.unregister(); }; }, []);

这段代码与第 5 章中click-streamerstream::set(写clicks/all组)正好闭合:stream::set既存储条目,又把它推送给订阅了该流与组的所有 WebSocket。SDK 侧StreamTriggerConfig支持stream_namegroup_iditem_idcondition_function_id(见 sdk/packages/node/iii-browser/src/stream.ts),这里按流 + 组订阅即可收到全部新点击。

注册服务器反向调用的函数

注册服务器需要人工确认时回调的函数:弹原生confirm并返回用户决定。注意——这个函数的注册与运行方式和前面章节的服务器函数完全一样;除了鉴权与权限管理,客户端与服务器端没有任何功能差别:

useEffect(() => { const fn = worker.registerFunction( "user::confirm_destructive_op", async (data: { action: string; code: string }) => { const confirmed = window.confirm(`Confirm: ${data.action}?`); return { confirmed }; }, ); return () => fn.unregister(); }, []);

直接创建链接,没有网关

提交表单时直接调用link::create这里没有fetch、没有 REST API,浏览器里的客户端 worker 与任何其他 worker 的运行方式完全一致:

async function onSubmit(e: React.FormEvent) { e.preventDefault(); const link = await worker.trigger<{ url: string; code?: string }, { code: string; url: string }>({ function_id: "link::create", payload: { url, code: code || undefined }, }); setCreated(link); setUrl(""); setCode(""); }

拼装 UI

最后是界面:短链表单、最近创建的链接、实时流式点击计数:

return ( <main> <h1>Linkly</h1> <form onSubmit={onSubmit}> <label>URL <input value={url} onChange={(e) => setUrl(e.target.value)} required /></label> <label>Code (optional) <input value={code} onChange={(e) => setCode(e.target.value)} /></label> <button type="submit">Shorten</button> </form> {created && ( <p> Created <code>{created.code}</code> → <code>{created.url}</code>. </p> )} <section> <h2>Live clicks: {clicks}</h2> {latest && ( <p>Last: <code>{latest.code}</code> at <code>{latest.clicked_at}</code></p> )} </section> </main> ) }

跑起来看效果

启动 UI:

npm run dev

Vite 默认把本地站点托管在 http://localhost:5173。

缩短一个链接,实时看到访问流入

在表单里缩短一个链接,然后多次访问http://localhost:3111/s/<code>——页面的 “Live clicks” 计数会实时上涨。数据链路:link::record_click发布link.clicked(pubsub)→click-streamersubscribe触发器触发click-streamer::broadcaststream::set写入clicks/all→ 浏览器ui::on_click收到StreamEvent更新计数。

从后端直接请求用户确认

在终端里通过 iii 触发浏览器中的函数:

iii trigger link::request_delete code=<code>

浏览器会弹出 confirm 提示框,只有你点击 OK 后服务器才真正执行删除(此时link::request_delete继续调用link::delete清理数据库与state缓存);点击取消则返回{ deleted: false },链接保留。

小结

客户端就是一个与所有其他 worker 完全相同的 worker。它通过iii-worker-manager提供的 RBAC 网关监听器接入,由一个auth函数负责准入——因为浏览器不像我们的其他 worker 那样受信任。这套机制并不局限于浏览器:任何其他 worker 都可以用同样的方式被网关保护

一切就绪后,客户端直接调用服务器函数、订阅实时流获取更新、注册函数供服务器回调——全部发生在与 Linkly 其他部分相同的 iii 总线上。想要更系统地理解 RBAC 网关(含中间件拦截、注册钩子与多语言示例),可继续阅读 worker-manager 访问控制文档;若要完整重跑整个 Linkly 项目,可回到 教程总览 从第 1 章开始。

【免费下载链接】iiiEffortlessly compose, extend, and observe every service in real-time for the first time ever.项目地址: https://gitcode.com/GitHub_Trending/mo/iii

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询