r2-gated-downloads-and-play

从私有 R2 做订单门禁下载与浏览器内游玩

游戏构建产物存放在一个私有 R2 桶里——里面没有任何东西是可公开读取的。商店从不直接暴露这个桶;它扮演的是一道门禁,在交出任何抵达文件的途径之前,先判断这个请求是否有权拿到该文件。总共有三条彼此不同的通道,它们各自做出了刻意不同的取舍。

已购买的下载:先校验,再签名

买家的桌面端下载走的是专门的 /download/ 路由,而不是指向存储的直链。该路由首先重新确认这个请求确实对应一笔真实、已付款的订单:它加载订单,用恒定时间比较把调用方带来的订单密钥和已存储的那份对比(这样错误的密钥无法靠计时来试探),并要求订单处于已付款状态。只有到这一步,它才为该商品的对象键生成一个短时效的 SigV4 预签名 GET URL,并把浏览器 302 直接重定向到 R2。

预签名的意义在于:商店从不代理文件字节——是 R2 自己把下载送出去的,但这只是因为商店临时为这一个请求做了担保,而且只在一小段时间窗内有效。窗口一关,链接即失效。签名所需的凭据来自环境变量,绝不写进镜像或仓库。构建对象键由打包流水线按平台(macOS / Windows)分别设置,因此同一条路由能服务一款游戏实际构建出来的任意平台。

免费的 authenticator:同样的预签名,无需订单

每款游戏都依赖的桌面 authenticator 应用是免费的,因此它的 /get-authenticator/ 路由完全跳过订单校验,直接为当前构建预签名。它根据自己被服务的主机名来选择构建渠道——开发版还是正式版——于是两个商店前端各自交出与之匹配的构建,无需任何逐请求的配置。

浏览器内游玩:绝不重定向,每次都重新校验

可在网页里游玩的构建是最有意思的一种情形,因为一个网页游戏必须能被浏览器触达,而这恰恰是一条泄露的预签名 URL 会让任何人都能做到的事。所以 /play/ 干脆拒绝把任何指向存储的链接交给客户端。它在每一次加载时都重新校验当前登录账户是否拥有这款游戏——用的是与"我的游戏"列表相同的归属判定来源——然后在服务端取回这份私有的网页构建,把自包含的游戏 HTML 通过自身回流给浏览器。预签名 URL 完全在服务端生成并消费,从不抵达客户端。这就是那处刻意为之的不对称:一个下载链接在外面过期是可以接受的,但一个能被分享出去的游玩 URL 会绕过门禁,所以它干脆从不被发出。

为什么是这个形状

三条通道里有两条依赖"预签名加重定向",因为这样很省:商店不进入字节路径。第三条做不到,因为要交付的东西本身就是内容的入口,而不是内容的一份归档。于是门禁从"证明你付过款,这是一把会褪色的钥匙"变成了"证明你拥有它,我亲自读给你听"。归属判定以账户邮箱为键,正是这一点把一笔订单与一个身份绑在一起——横跨商店本身与游戏应用所读取的权益。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →