ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Electric Proxy Auth 实战:为 Shape 同步请求实现服务端授权的完整剖析

Electric Proxy Auth 实战:为 Shape 同步请求实现服务端授权的完整剖析 Electric Proxy Auth 实战为 Shape 同步请求实现服务端授权的完整剖析【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric本文基于 Electric 官方仓库中的 proxy-auth 演示文档与其完整示例工程讲解如何用反向代理为 Electric 的GET /v1/shape同步请求做认证与授权包括代理路由的逐行实现、客户端如何携带Authorization头、代理必须服务端控制的查询参数以及示例的本地运行方式。读完之后你将掌握「代理授权proxy auth」这一模式的全部关键细节能够将其迁移到自己的 Next.js / Edge 函数等 API 层中实现对同步数据的行级访问控制。模式定位Shape 是资源请求可以被代理Electric 的同步建立在「一切皆 HTTP」这一原则上客户端通过GET /v1/shape请求一个 ShapeShape 定义放在查询参数中如?tableusers。因此对 Shape 的授权与对任何 Web 资源的授权完全一致——你不需要把授权逻辑固化进数据库规则而是可以让请求在到达 Electric 之前先经过一层代理。完整的模式定义见 Auth 指南其中「Proxy auth」小节描述了该模式的标准步骤为GET /v1/shape请求附加Authorization头代理使用该头校验客户端身份并确认其有权访问该 Shape若无权返回401或403若有权代理在服务端设置 Shape 参数后把请求转发给 Electric并将流式响应原样传回客户端。仓库中的 proxy-auth 示例 正是这一模式的完整实现一个 Next.js 应用内含一条代理路由/shape-proxy、一个用useShape同步数据的页面以及一张带示例数据的users表。它演示了代理对 Shape 请求的三种处置方式拒绝访问deny——缺少凭据时直接返回 401放行全部allow full access——管理员可看到所有行改写 Shape 请求modify——为普通用户追加where条件使其只能看到本组织的行。补充仓库中还有另一个思路相反的示例 gatekeeper-auth由 API 签发 shape 作用域令牌、再由代理校验令牌与请求参数是否一致。两者是官方文档并列推荐的两种模式proxy auth 是其中最简单的一种。示例工程结构示例位于 monorepo 的examples/proxy-auth目录下参与 pnpm workspace 构建关键文件如下路径作用examples/proxy-auth/app/shape-proxy/route.ts授权代理路由整个示例的核心约 66 行examples/proxy-auth/app/page.tsxNext.js 客户端页面用useShape通过代理同步users表examples/proxy-auth/db/migrations/001-create-users-table-with-examples.sql建表迁移与示例数据examples/proxy-auth/package.json脚本定义dev、backend:up、db:migrate等examples/proxy-auth/sst.config.tsSST 部署配置注入ELECTRIC_URL、ELECTRIC_SOURCE_ID、ELECTRIC_SOURCE_SECRET环境变量数据模型非常小一行迁移脚本即全部create table users ( id int primary key generated always as identity, name text not null, org_id int not null ); insert into users (name, org_id) values (Alice, 1), (Bob, 1), (Charlie, 1), (David, 2), (Eve, 2), (Frank, 2);组织 1 有 Alice/Bob/Charlie组织 2 有 David/Eve/Frank——页面切换「Alice — org 1 / David — org 2 / Admin」三个身份时用户就能看到行级过滤的效果。代理路由逐行解析核心代码下面是 app/shape-proxy/route.ts 的完整实现它是官方文档页面内联展示的主代码import { ELECTRIC_PROTOCOL_QUERY_PARAMS } from electric-sql/client export async function GET(request: Request) { const url new URL(request.url) // Constuct the upstream URL const baseUrl process.env.ELECTRIC_URL ?? http://localhost:3000 const originUrl new URL(/v1/shape, baseUrl) // Only pass through Electric protocol parameters url.searchParams.forEach((value, key) { if (ELECTRIC_PROTOCOL_QUERY_PARAMS.includes(key)) { originUrl.searchParams.set(key, value) } }) // Set the table server-side originUrl.searchParams.set(table, users) if (process.env.ELECTRIC_SOURCE_ID) { originUrl.searchParams.set(source_id, process.env.ELECTRIC_SOURCE_ID) } if (process.env.ELECTRIC_SOURCE_SECRET) { originUrl.searchParams.set(secret, process.env.ELECTRIC_SOURCE_SECRET) } // authentication and authorization // Note: in a real-world authentication scheme, this is where you would // veryify the authentication token and load the user. To keep this example simple, // were just passing directly through the org_id. const org_id request.headers.get(authorization) let user if (org_id) { user { org_id, isAdmin: org_id admin } } // If the user isnt set, return 401 if (!user) { return new Response(authorization header not found, { status: 401 }) } // Only query orgs the user has access to. // Use parameterized query to prevent SQL injection if (!user.isAdmin) { originUrl.searchParams.set(where, org_id $1) originUrl.searchParams.set(params[1], user.org_id) } const response await fetch(originUrl) // Fetch decompresses the body but doesnt remove the // content-encoding content-length headers which would // break decoding in the browser. // // See https://github.com/whatwg/fetch/issues/1729 const headers new Headers(response.headers) headers.delete(content-encoding) headers.delete(content-length) headers.set(Vary, Authorization) return new Response(response.body, { status: response.status, statusText: response.statusText, headers, }) }这段不到 70 行的代码浓缩了 proxy auth 模式的全部要点可以分成五个部分来读1. 只透传协议参数拒绝客户端的任意参数代理把上游 URL 指向${ELECTRIC_URL}/v1/shape然后遍历客户端请求上的查询参数只保留ELECTRIC_PROTOCOL_QUERY_PARAMS白名单内的键第 10–14 行。这个白名单定义在 TypeScript 客户端的 constants.ts从源码结构看它包括live、live_sse、handle、offset、log、subset 查询相关参数where/limit/offset/order_by及其表达式变体和缓存破坏参数等——即客户端同步状态必需、但无法扩大数据可见范围的参数。table、secret等决定性参数不在白名单中因此客户端永远无法通过 URL 自行指定表或秘密。2. 表名、源标识与秘密一律服务端注入第 17–25 行在代理侧强制设置tableusers并按需附加source_id与secret。这两个环境变量由部署配置 sst.config.ts 注入ELECTRIC_URL指向 Electric 实例ELECTRIC_SOURCE_ID/ELECTRIC_SOURCE_SECRET由 SST 的createDatabaseForCloudElectric创建数据库时生成。本地开发时二者缺省即可ELECTRIC_URL回退为http://localhost:3000。这对应 Auth 指南 中「Parameters your proxy must control」清单的核心要求参数位置安全考量tableURL必须服务端设置否则客户端可访问任意表where主 Shape WHEREURL必须服务端设置——这就是授权过滤器secretURL必须服务端设置绝不能暴露给客户端queryable_columnsURL若表含敏感列必须服务端设置列白名单而offset同步位点、handle续传句柄、live/live_sse/replica/log协议行为开关等参数则可以放心透传它们要么无法扩大数据访问要么是客户端维护同步状态所必需的。3. 认证从 Authorization 头推导身份第 31–35 行读取authorization头并构造user { org_id, isAdmin: org_id admin }。源码注释明确说明这是刻意简化的做法真实项目中这里应该是校验认证令牌如 JWT并加载用户的位置。示例把「组织 ID」直接放在头里、用字符串admin充当管理员身份只是为了让演示可交互——页面导航栏切换链接时只是改写?org_id1|2|admin查询参数并刷新。第 38–40 行完成「拒绝访问」分支没有Authorization头时返回401响应体为authorization header not found客户端 ShapeStream 会将其作为错误呈现。4. 授权参数化的行级过滤第 44–47 行是「改写请求」分支的关键——对非管理员用户代理在服务端追加whereorg_id $1 params[1]user.org_idElectric 的 HTTP API 通过params[N]查询参数把$1、$2等占位符安全地替换为参数值Auth 指南中「Handling parameterized queries」小节有完整说明这正是防 SQL 注入的标准手段org_id的值虽然来自客户端请求头但只作为参数值进入查询而不会拼进 SQL 文本。管理员则不追加where看到全部 6 行。对于更复杂的 WHERE 生成需求列名编译期校验Auth 指南还给出了用 Drizzle / Kysely 等查询构建器生成类型安全 WHERE 片段的完整示例可在此基础上平滑升级。5. 响应转发两个容易踩的坑第 49–65 行处理上游响应的回传包含两个实战细节剥离content-encoding与content-lengthfetch会自动解压响应体但不会移除这两个响应头若原样带回浏览器按content-encoding: gzip再解码一次就会乱码。注释中引用的 WHATWG fetch issuewhatwg/fetch#1729说明了这一行为。这是所有转发 Electric 流式响应的代理都必须处理的点。设置Vary: Authorization第 59 行告诉浏览器与 CDN 把认证头计入缓存键。不同用户的 Shape 响应因此被分别缓存用户登出后凭据消失就取不到之前以旧身份缓存的响应。这一点在 Auth 指南的「Session Invalidation with Vary Headers」一节有专门展开。客户端useShape 携带 Authorization 头页面组件 app/page.tsx 展示客户端一侧的全部改动——相比直接连 Electric只是把 URL 指向自己的代理路由并附加请求头const usersShape (): ShapeStreamOptions { if (typeof window ! undefined) { const queryParams new URLSearchParams(window.location.search) const org_id queryParams.get(org_id) return { url: new URL(/shape-proxy?org_id${org_id}, window.location.origin) .href, headers: { Authorization: org_id || , }, } } else { return { url: new URL(https://not-sure-how-this-works.com/shape-proxy).href, } } } export default function Home() { const { data: users, isError, error } useShapeUser(usersShape()) // ... }几个值得注意的点使用的是electric-sql/react的useShapeUser(options)ShapeStreamOptions类型来自electric-sql/client见 package.json 中workspace:*依赖。代理端点 URL 上保留的?org_id...只是给页面切换身份用的「演示参数」真正决定授权的是Authorization头两者在示例中被设置为同值。typeof window ! undefined分支是 Next.js 的 SSR 保护服务端渲染时无法读取浏览器 URL故返回占位 URL真实同步在客户端水合后才发起。当选择「Not logged in」时Authorization头为空串代理返回 401页面以红框展示error.toString()直观验证了「拒绝」分支。本地运行示例是 monorepo 的一部分运行步骤来自 examples/proxy-auth/README.md脚本定义见 package.json# 1. 在 monorepo 根目录安装并构建所有 workspace 包 cd monorepo-root pnpm install pnpm run -r build # 2. 回到示例目录启动后端Postgres ElectricDocker Compose cd examples/proxy-auth pnpm backend:up # 等价于 PROJECT_NAMEproxy-auth-example pnpm -C ../../ run example-backend:up pnpm db:migrate # 3. 启动 Next.js 开发服务器5173 端口 pnpm dev # 4. 结束后清理 pnpm backend:down注意事项backend:up会停止并删除其他示例后端容器挂载的卷保证示例每次都以干净数据库启动db:migrate使用pg-migrations apply执行 db/migrations连接串取自仓库根的.env.dev开发环境下代理的ELECTRIC_URL缺省指向http://localhost:3000即本地 Docker Compose 拉起的 Electric 实例线上部署则由 sst.config.ts 负责创建数据库、运行迁移并把ELECTRIC_URL、ELECTRIC_SOURCE_ID、ELECTRIC_SOURCE_SECRET注入 Next.js 运行环境部署域名proxy-auth.examples.electric-sql.com。生产化时的加固方向示例刻意做了简化从代码结构看以下位置是迁移到生产时的改造点认证逻辑route.ts第 28–31 行的注释位置应替换为真实的令牌校验如解码 JWT、查询用户/组织关系对外部授权服务细粒度 ACL 类的调用也应放在此处。Auth 指南特别指出如果使用分布式一致性授权服务proxy auth 模式比 gatekeeper 模式更合适因为每次 Shape 请求都会显式授权不存在令牌过期导致权限陈旧的问题。代理位置若 Electric 部署在 CDN 之后授权代理宜放在边缘CDN 与用户之间proxy 与 gatekeeper 两种模式都适用于边缘函数。大 WHERE / 子集查询当 ACL 子查询使 WHERE 很长时GET 的 URL 可能触发414 Request-URI Too Long。Electric 支持以 POST 携带 subset 参数where/params/limit/offset/order_by的 JSON 体且 POST body 中的 subset WHERE 会与服务端主 WHERE 以AND组合——子集查询只能收窄结果永远无法扩大结果因此透传 subset 参数是安全的。客户端可将subsetMethod设为POST以提前兼容后续版本对 subset GET 请求的弃用计划详见 Auth 指南 的「Using POST for subset queries」一节。登出处理除Vary: Authorization保证 HTTP 缓存隔离外客户端在登出时还应整页刷新以清掉内存中上一位用户已同步的 Shape 数据。小结proxy-auth 示例用一条 66 行的 Next.js 路由完整演示了 Electric 推荐的「代理授权」模式客户端照常使用useShape只是改指向代理并携带Authorization头代理只透传协议白名单参数强制服务端注入table、where、secret从结构上杜绝客户端越权指定表或秘密授权决策分三档401 拒绝 / 全量放行管理员/ 追加参数化where做行级过滤防注入响应转发时剥离content-encoding/content-length并补上Vary: Authorization解决浏览器解码与缓存隔离两个隐蔽问题。由于 Electric「一切皆 HTTP」的底层设计这套模式可以落在任何能处理 HTTP 的位置——你自己的 API、独立代理服务或 CDN 前的边缘函数——授权逻辑可以查库、调外部服务完全不受数据库规则系统的约束。若需要「客户端提出 Shape 定义、服务端签发 shape 作用域令牌」的变体可进一步研究 gatekeeper-auth 示例两者的对比与完整参数安全清单均收录在 Auth 指南 中。【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表