Aekor

Aekor
专注于用户阅读体验的响应式博客主题
  1. 首页
  2. Blog
  3. 正文

激活码 + 设备指纹绑定,用 Cloudflare Workers 轻松搞定

2026-07-03 24582点热度 113人点赞 0条评论

本文讲述如何零服务器、零数据库,仅凭 Cloudflare Workers + KV 实现一套完整的激活码 + 设备指纹鉴权体系。涵盖 ECDSA 签名验签、离线宽限期、心跳续签、时钟防回拨等工程细节。

为什么选择 Cloudflare Workers + KV

做付费/授权分发时,开发者面临的核心难题是:

痛点传统方案代价
需要一台服务器域名 + SSL + 运维,月成本 ≥ ¥50
需要数据库激活码、设备记录的存储与备份
全球低延迟需要 CDN 加速 API 响应
需要防破解自建签名服务,安全性难保障

Cloudflare Workers + KV 的组合拳,把上面的痛点一次性击穿:

  • Workers:Serverless 函数,全球 300+ 边缘节点部署,冷启动 < 5ms
  • KV:全球分布式键值存储,最终一致(~60s 全球同步),免费层每天 10 万次读
  • 零成本启动:免费层可支撑约 1 万活跃用户
  • Web Crypto API 原生支持:Workers 和浏览器端都内置crypto.subtle,ECDSA-P256 签名/验签零依赖

系统架构总览

📦 付费授权系统整体流程(树状图)
│
├── 🌱 【用户打开插件】
│     │
│     ├── 本地有没有“激活凭证”?
│     │     ├── 没有 → 🌳 【显示激活表单】
│     │     │           │
│     │     │           ├── 用户输入激活码
│     │     │           ├── 插件采集设备指纹
│     │     │           ├── 发送给 Cloudflare Worker
│     │     │           │     │
│     │     │           │     ├── 查激活码是否有效?
│     │     │           │     │     ├── 无效 → ❌ 拒绝(码不存在/过期/被拉黑)
│     │     │           │     │     └── 有效 ↓
│     │     │           │     │
│     │     │           │     ├── 查该码绑了几台设备?
│     │     │           │     │     ├── 已达上限 → ❌ 拒绝(换台电脑再买一个吧)
│     │     │           │     │     └── 未达上限 ↓
│     │     │           │     │
│     │     │           │     └── 存进 KV:{激活码 + 指纹 + 当前时间}
│     │     │           │
│     │     │           └── Worker 返回“带签名的激活凭证”
│     │     │                 │
│     │     │                 └── 插件验签名 → 存本地 → ✅ 激活成功
│     │     │
│     │     └── 有 ↓
│     │
│     ├── 【检查本地凭证是否被篡改】
│     │     │
│     │     ├── 验数字签名 → 不通过 → ❌ 拒绝(凭证被篡改)
│     │     └── 通过 ↓
│     │
│     ├── 【时钟回拨检测】
│     │     │
│     │     ├── 当前时间 < 上次记录的最大时间 - 5分钟?
│     │     │     ├── 是 → 强制联网验证(防改时间作弊)
│     │     │     └── 否 ↓
│     │     │
│     │     └── 更新“最大时间戳”为当前时间
│     │
│     ├── 【检查 7 天宽限期】
│     │     │
│     │     ├── 当前时间 < 宽限期截止时间?
│     │     │     ├── 是 → ✅ 放行(免联网,直接使用)
│     │     │     └── 否 ↓(已超过 7 天)
│     │     │
│     │     └── 【联网续命(心跳)】
│     │           │
│     │           ├── 发送设备指纹 + Token 给 Worker
│     │           │     │
│     │           │     ├── 查 KV:这个指纹还在吗?
│     │           │     │     ├── 找不到 → ❌ 拒绝(被拉黑/已解绑)
│     │           │     │     └── 找得到 ↓
│     │           │     │
│     │           │     └── 更新 KV 中的最后心跳时间
│     │           │
│     │           └── Worker 返回新的凭证(宽限期刷新到“当前+7天”)
│     │                 │
│     │                 └── 插件存新凭证 → ✅ 续命成功,继续用
│     │
│     └── ✅ 【正常使用】—— 后台每30分钟自动续一次心跳
│
│
└── 🛠️ 【管理员后台】(独立分支)
      │
      ├── 生成激活码(批量或单个)
      ├── 查看所有码的使用情况
      ├── 解绑某码下的所有设备
      └── 一键撤销激活码(写入黑名单,立即生效)

为了帮你更快理解,我再配一张“数据流向”简图:

1
🖥️ 用户打开插件
采集设备指纹 + 检查本地是否有激活凭证
✅ 有凭证 → 走验证流程 ❌ 没凭证 → 走激活流程
2
📝 激活流程
用户输入激活码 → 发送给 Cloudflare Worker
3
☁️ Worker 验证(看门大爷)
📌 查 KV:激活码有效吗?
📌 查 KV:该码绑了几台设备?
📌 通过后 → 写入 “激活码 + 指纹 + 时间”
4
✅ 激活成功
Worker 返回带签名的“激活凭证” → 插件存本地 → 可离线使用 7 天

5
🔍 验证流程(有凭证时)
📌 验数字签名 → 防篡改
📌 时钟回拨检测 → 防改时间作弊
📌 检查 7 天宽限期 → 没过期就直接用,不联网
6
💓 心跳续命(每30分钟后台自动)
插件发送指纹 + Token → Worker 刷新宽限期 → 续上 7 天 → 用户无感知
💡 核心:前端认电脑(指纹) + 后端管钥匙(激活码) + 7天宽限期 = 零成本、防破解、离线可用

数据模型设计

四个 KV 命名空间,职责分明:

NamespaceKey 格式Value用途
LICENSESlic:<CODE>{maxDevices, expireAt, createdAt, revoked, note}激活码本身的信息
DEVICESdev:<deviceId>{deviceId, code, fingerprintHash, fingerprintParts[], token, tokenExpireAt, activatedAt, lastHeartbeat}每台设备的详细记录
CODE_DEVICEScd:<CODE>[deviceId, deviceId, ...]激活码 → 设备列表的反向索引
TOKEN_INDEXtk:<token>deviceIdToken → 设备的快速查找索引

为什么需要四个 KV?

KV 不支持按 value 查询,也不支持二级索引。如果只有DEVICES,想知道"某个 token 对应哪个设备"就得全量扫描——这在 KV 中是不可行的。因此用TOKEN_INDEX 做了一层手工索引:

activate/verify/heartbeat 请求带 token
  → TOKEN_INDEX["tk:<token>"] 得到 deviceId
  → DEVICES["dev:<deviceId>"] 得到完整设备记录
  → 2 次 KV 读操作搞定

同理,CODE_DEVICES 解决的是"一个激活码绑了几台设备"的查询:

activate 时检查设备数
  → CODE_DEVICES["cd:<CODE>"] 得到 deviceId[]
  → 判断 length < maxDevices

设备指纹:

设备指纹是整套方案中"谁在用"的核心依据。

采集什么

const parts = [
  navigator.platform,           // 操作系统平台
  navigator.userAgent,          // 浏览器 UA
  navigator.hardwareConcurrency, // CPU 核心数
  screen.width + "x" + screen.height + "x" + screen.colorDepth, // 屏幕
  Intl.DateTimeFormat().resolvedOptions().timeZone, // 时区
  navigator.language,           // 语言
  chrome.runtime.id,            // 扩展 ID
];
const hash = sha256(parts.join("|") + "|y3t00l-salt-v1");

7 个维度,全部是浏览器原生 API 可获取、无需用户授权的信息。

模糊匹配策略

浏览器升级、屏幕分辨率微调等情况会导致指纹漂移。如果严格匹配,用户体验会很差。因此引入模糊匹配:

7 项指纹中匹配 ≥ 5 项 → 认定为同一设备
function countFingerprintMatch(a, b) {
  const len = Math.min(a.length, b.length);
  let hit = 0;
  for (let i = 0; i < len; i++) if (a[i] === b[i]) hit++;
  return hit;
}
// 匹配数 >= 5 即通过

这样即使用户升级了浏览器版本(UA 变化),只要平台、屏幕、时区、语言、扩展 ID 等没变,依然被识别为同一设备。

指纹漂移自动更新

心跳环节有一个精巧设计——当指纹轻微漂移时,自动更新服务端记录:

// 心跳时发现指纹变了
if (device.fingerprintHash !== fingerprintHash) {
  const matchCount = countFingerprintMatch(device.fingerprintParts, body.parts);
  if (matchCount >= 4) {  // 至少 4 项匹配才允许更新
    device.fingerprintHash = fingerprintHash;
    device.fingerprintParts = body.parts;
  }
}

这意味着指纹会随用户环境自然演化,而非一次绑定终身不变。

ECDSA-P256 数字签名

为什么需要签名

激活/校验的响应通过公网传输。如果不签名,攻击者可以:

  • 伪造一个假的后端返回,让插件"永久激活"
  • 中间人劫持后篡改graceUntil(宽限期)的值

密钥分发

gen-keys.js 生成 ECDSA-P256 密钥对
  ├─ 私钥(PKCS8 hex) → wrangler secret put → 注入 Worker
  └─ 公钥(SPKI hex) → 写入 config.js → 硬编码在插件里

私钥永远不出 Worker。客户端只持有公钥,即使反编译插件也只能拿到公钥,无法伪造签名。

签名内容

签名原文 = JSON.stringify(responseData) + "|" + serverTime + "|" + nonce
  • responseData:响应体,包含deviceId, token, expireAt, graceUntil 等
  • serverTime:服务器时间戳,防止重放攻击(客户端校验 ±60s 时间漂移)
  • nonce:一次性随机数,防止同一响应被重复利用

客户端验签

async function verifySign(body, serverTime, nonce, signB64) {
const payload = strToBytes(
    JSON.stringify(body) + "|" + serverTime + "|" + nonce
  );
const sig = base64ToBytes(signB64);
const key = await getPublicKey(); // 从 config.js 导入公钥
return crypto.subtle.verify(
    { name: "ECDSA", hash: "SHA-256" },
    key, sig, payload
  );
}

Web Crypto API 是浏览器原生支持的,性能极高且安全——私钥永远不需要在客户端出现。

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: Cloudflare Workers
最后更新:2026-08-07

Aekor

这个人很懒,什么都没留下

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

使用AI教程

  • API报错解决方案
  • API 基础知识
  • API Key 获取

分类

  • Blog
  • TradingAgents-CN
  • 使用教程

COPYRIGHT © 2026 Aekor. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang