给博客存档系统打个补丁:别让安全机制先把玩家的档删了
前段时间,我给博客塞了一套游戏系统。原因也不复杂。普通博客的玩法基本都是打开文章、看完、退出,流程稳定得像新手教程。我想让它多一点可以探索的东西,于是加了等级、经验、成就和特殊事件,后面还打算继续往上叠功能。
至于这些东西要靠什么串起来?当然是存档。整套游戏系统的脑洞在这里:想把博客做得像一场游戏。
第一版:先把防作弊点满
最开始设计存档时,我优先考虑的是防篡改。毕竟等级、经验和成就以后都会影响其他功能。如果浏览器改几个数字就能直接满级,那这套系统也不用玩了,大家打开控制台输入一串代码就能速通。所以第一版的原则非常直接:不信任本地数据,一切以服务端存档为准。
经验和成就在服务端结算,浏览器只负责显示。LocalStorage 最多在断线时临时顶一下,重新连接后还是由服务端存档覆盖。匿名访客则通过 Cookie 识别身份,提交评论后再尝试把匿名档合并到评论身份。安全性看起来没问题。甚至有点过于自信。
最初的存档逻辑

然后 Bug 就开始刷新了。
存档没被篡改,但它会消失
第一版只回答了一个问题:
这份数据可信吗?
却漏掉了另一个更重要的问题:
这份存档到底是谁的?
当时的身份主要依赖匿名 Cookie 和评论 Cookie,登录账号并没有真正参与存档归属。结果就是:同一个账号换台设备,找不回原来的进度;同一台设备换个账号,还是可能继续用原来的存档;服务端一旦识别到另一份记录,还会理直气壮地覆盖本地状态。
防作弊确实成功了。顺便也把正常玩家防出去了。
这就像为了避免别人修改存档,直接把读档按钮一起删掉。安全评分很高,游戏体验直接 GG。
问题不在“谁覆盖谁”
最初我一直在纠结:本地存档和服务端存档冲突时,到底该相信哪一边?后来发现,这个问题从一开始就问歪了。本地数据当然不能直接当成可信存档,但正常访客产生的进度也不能说扔就扔。真正需要拆开的其实是三件事:
- 数据是否经过服务端认可;
- 存档当前属于哪个账号;
- 哪台设备正在使用它。
把这三件事混在一起,逻辑迟早会打结。拆开之后就简单多了:服务端继续负责校验数据,匿名存档负责保存登录前的正常进度,账号主存档负责长期归属,设备绑定则负责决定当前该读取哪一份档。匿名档可以并入账号主档,但两个账号的主存档不能自动混合。设备可以换绑,旧会话却不能靠刷新把绑定抢回去。边界锁好,剩下的交给流程。
优化后的存档逻辑

这次补丁改了什么
第二版没有取消服务端校验,也没有开始无条件相信浏览器。真正改变的是:系统不再把“本地不可信”理解成“本地进度可以随便丢”。
访客没有登录时,设备拥有自己的匿名档;登录后,经过服务端确认的匿名进度可以进入账号主档。账号换到其他设备时,绑定会跟着迁移;同一台设备切换账号时,系统先断开旧关系,再建立新关系,绝不会把两份账号主档熔成一锅。
退出登录也不会顺手删档。退出账号只是结束登录状态,不是按下“删除存档”。博客又不是什么高难度 Roguelike(死亡后删档重来),没必要每次登出都让人从 LV.1 重开。
这套逻辑更像“滚动绑定”。同一时间只认一组有效的账号与设备关系,设备迁移时把归属交接清楚。少一点看似智能的自动操作,反而更不容易串档。
为什么第一版会翻车
说到底,第一版不是代码少写了一个判断,而是设计目标少算了一项。我当时把“真实性”当成了存档系统的全部,却忽略了另外两条同样重要的属性:
- 连续性:正常产生的进度不能莫名消失;
- 归属感:系统必须知道一份存档属于谁,又该在哪台设备上使用。
只保证真实性,最多能得到一份没人改得动的存档。至于玩家还能不能找到它,那就全看运气了。现在的方案肯定也不是最终版本。以后等级、成就和特殊事件继续增加,新的边界情况大概还会从地图角落里冒出来。没关系,发现 Bug 就打补丁,遇到 Boss 就拆机制。反正做这种系统,本来就是边玩边更新。
至少这一次,存档还在,玩家不用删号重练。
算通关。暂时的。