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

发布时间:2026/9/15 1:59:47
用 iii-browser-sdk 把浏览器变成一等 Worker:RBAC 网关接入、实时点击流与服务器反向调用实战 用 iii-browser-sdk 把浏览器变成一等 WorkerRBAC 网关接入、实时点击流与服务器反向调用实战【免费下载链接】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 前几章中link、analytics、click-streamer等 worker 都通过引擎内置端口连接。本章要把浏览器也拉上同一条总线浏览器经 WebSocket 直连引擎直接调用link::create中间没有 REST API 网关订阅clicks实时流更新页面上的实时点击计数器注册一个user::confirm_destructive_op函数当服务器要删除链接时反向询问浏览器里的真人。关键差异在于信任模型浏览器不是你亲手运行的进程不能像本地 worker 一样默认信任。因此浏览器连接必须走iii-worker-manager的RBAC 网关监听器与本地 worker 使用的可信端口分开。第一步创建 auth worker认证逻辑单独封装进一个authworker让linkworker 继续只关注链接本身。先在项目目录下脚手架一个新的 TypeScript worker若你刚从第 6 章test-channels目录过来先cd ..iii worker init auth --language typescript第二步同时跑两个监听器引擎内置端口49134是可信监听器本地 workerlink、analytics 等都从这里接入浏览器绝不能走这里。在config.yaml中声明两个iii-worker-manager一个保留可信的49134一个在3110上提供 RBAC 网关给浏览器workers: # ... # Trusted listener for local workers. Replaces the engines 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在每个浏览器连接建立时运行一次它收到请求的headers、query_params和ip_address返回该会话的权限额外的允许/拒绝项、任意上下文抛异常即拒绝连接。替换生成的auth/src/index.tsimport { 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: Recordstring, string; query_params: Recordstring, 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_functions在expose_functions之外额外允许的函数 IDforbidden_functions即使匹配也拒绝的函数 ID优先级最高allowed_trigger_types会话可以绑定的触发器类型省略则允许全部allow_trigger_type_registration会话是否可以注册新的触发器类型allow_function_registration会话是否可以注册函数默认truecontext任意对象转发给每次调用的中间件function_registration_prefix应用于会话注册的每个函数的前缀示例返回中allowed_functions与forbidden_functions都为空、allow_function_registration为true意味着会话可注册自己的回调前端需要注册ui::on_click与user::confirm_destructive_op但不能注册新的触发器类型。注册到项目iii worker add ./auth源码层面的 RBAC 决策流从引擎侧源码 rbac_config.rs 可以看到一次调用的放行判定顺序该文件注释中的决策流为function_id在forbidden_functions中 →拒绝且全局生效跨命名空间命中会话按命名空间授予的 grantssession_namespaces→ 放行无命名空间的allowed_functions仅在default命名空间生效 → 放行INFRASTRUCTURE_FUNCTIONS如engine::log::*、engine::workers::register、engine::channels::create始终放行保证连接建立、日志、上下文传播可用任一expose_functions过滤规则匹配 → 放行否则拒绝默认拒绝fail-closed。expose_functions的match(...)通配符语法也由该文件实现支持前缀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 vitelatest 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。引入与类型Click是clicks表的一行StreamEvent是iii-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] useStateClick | 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-streamer的stream::set写clicks/all组正好闭合stream::set既存储条目又把它推送给订阅了该流与组的所有 WebSocket。SDK 侧StreamTriggerConfig支持stream_name、group_id、item_id与condition_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 h1Linkly/h1 form onSubmit{onSubmit} labelURL input value{url} onChange{(e) setUrl(e.target.value)} required //label labelCode (optional) input value{code} onChange{(e) setCode(e.target.value)} //label button typesubmitShorten/button /form {created ( p Created code{created.code}/code → code{created.url}/code. /p )} section h2Live clicks: {clicks}/h2 {latest ( pLast: code{latest.code}/code at code{latest.clicked_at}/code/p )} /section /main ) }跑起来看效果启动 UInpm run devVite 默认把本地站点托管在 http://localhost:5173。缩短一个链接实时看到访问流入在表单里缩短一个链接然后多次访问http://localhost:3111/s/code——页面的 “Live clicks” 计数会实时上涨。数据链路link::record_click发布link.clickedpubsub→click-streamer的subscribe触发器触发click-streamer::broadcast→stream::set写入clicks/all→ 浏览器ui::on_click收到StreamEvent更新计数。从后端直接请求用户确认在终端里通过 iii 触发浏览器中的函数iii trigger link::request_delete codecode浏览器会弹出 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),仅供参考