从私有 R2 做订单门禁下载与浏览器内游玩
游戏构建产物存放在一个私有 R2 桶里——里面没有任何东西是可公开读取的。商店从不直接暴露这个桶;它扮演的是一道门禁,在交出任何抵达文件的途径之前,先判断这个请求是否有权拿到该文件。总共有三条彼此不同的通道,它们各自做出了刻意不同的取舍。
已购买的下载:先校验,再签名
买家的桌面端下载走的是专门的 /download/ 路由,而不是指向存储的直链。该路由首先重新确认这个请求确实对应一笔真实、已付款的订单:它加载订单,用恒定时间比较把调用方带来的订单密钥和已存储的那份对比(这样错误的密钥无法靠计时来试探),并要求订单处于已付款状态。只有到这一步,它才为该商品的对象键生成一个短时效的 SigV4 预签名 GET URL,并把浏览器 302 直接重定向到 R2。
预签名的意义在于:商店从不代理文件字节——是 R2 自己把下载送出去的,但这只是因为商店临时为这一个请求做了担保,而且只在一小段时间窗内有效。窗口一关,链接即失效。签名所需的凭据来自环境变量,绝不写进镜像或仓库。构建对象键由打包流水线按平台(macOS / Windows)分别设置,因此同一条路由能服务一款游戏实际构建出来的任意平台。
免费的 authenticator:同样的预签名,无需订单
每款游戏都依赖的桌面 authenticator 应用是免费的,因此它的 /get-authenticator/ 路由完全跳过订单校验,直接为当前构建预签名。它根据自己被服务的主机名来选择构建渠道——开发版还是正式版——于是两个商店前端各自交出与之匹配的构建,无需任何逐请求的配置。
浏览器内游玩:绝不重定向,每次都重新校验
可在网页里游玩的构建是最有意思的一种情形,因为一个网页游戏必须能被浏览器触达,而这恰恰是一条泄露的预签名 URL 会让任何人都能做到的事。所以 /play/ 干脆拒绝把任何指向存储的链接交给客户端。它在每一次加载时都重新校验当前登录账户是否拥有这款游戏——用的是与"我的游戏"列表相同的归属判定来源——然后在服务端取回这份私有的网页构建,把自包含的游戏 HTML 通过自身回流给浏览器。预签名 URL 完全在服务端生成并消费,从不抵达客户端。这就是那处刻意为之的不对称:一个下载链接在外面过期是可以接受的,但一个能被分享出去的游玩 URL 会绕过门禁,所以它干脆从不被发出。
为什么是这个形状
三条通道里有两条依赖"预签名加重定向",因为这样很省:商店不进入字节路径。第三条做不到,因为要交付的东西本身就是内容的入口,而不是内容的一份归档。于是门禁从"证明你付过款,这是一把会褪色的钥匙"变成了"证明你拥有它,我亲自读给你听"。归属判定以账户邮箱为键,正是这一点把一笔订单与一个身份绑在一起——横跨商店本身与游戏应用所读取的权益。