设计思路
本章深入解析授权系统的核心设计原理,包括授权验证流程、HMAC 签名机制、本地缓存策略、域名标准化、订单生命周期。这些设计决策直接影响系统的安全性、性能、可扩展性。
1. 授权验证全流程
从用户在 WordPress 插件后台点击"激活授权"开始,整个流程涉及 6 个步骤:
┌─────────┐ ① 输入授权码 ┌────────────────┐
│ 用户 │ ─────────────▶ │ 插件后台 │
│ │ ② 调用激活 API │ (WordPress) │
└─────────┘ └────────────────┘
│
│ ③ HTTPS POST
│ license_key + signature
▼
┌──────────────────────┐
│ License Server │
│ api/verify.php │
└──────────────────────┘
│
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
④ 验证签名 ⑤ 查询数据库 ⑥ 校验域名
(HMAC-SHA256) (lic_licenses) (标准化后比对)
│ │ │
└──────────────────────┼──────────────────────┘
▼
┌──────────────────────┐
│ 返回 JSON 结果 │
│ success / error │
└──────────────────────┘
│
▼
┌──────────────────────┐
│ 插件保存授权到 │
│ wp_options 表 │
│ 并显示"已激活" │
└──────────────────────┘
2. HMAC-SHA256 签名机制
为什么需要签名?
如果没有签名,攻击者可以:
- 伪造请求:模拟一个有效授权码,绕过授权验证
- 暴力穷举:批量枚举授权码
- 重放攻击:截获一次有效请求,多次发送
引入 HMAC-SHA256 后,只有持有 API_SECRET 的合法客户端才能生成有效签名。
签名生成算法
客户端和服务器端使用相同的算法计算签名:
sign_data = domain + '|' + license_key + '|' + product + '|' + serial
signature = HMAC-SHA256(sign_data, API_SECRET)
代码示例(PHP 客户端):
<?php
function generate_license_signature($domain, $license_key, $product, $serial = 1) {
$sign_data = $domain . '|' . $license_key . '|' . $product . '|' . $serial;
return hash_hmac('sha256', $sign_data, API_SECRET);
}
签名验证(服务器端)
<?php
function verify_license_signature($params) {
$sign_data = $params['domain'] . '|' . $params['license_key']
. '|' . $params['product'] . '|' . ($params['serial'] ?? 1);
$expected = hash_hmac('sha256', $sign_data, API_SECRET);
// hash_equals 防时间攻击
return hash_equals($expected, $params['signature'] ?? '');
}
关键细节:
- 使用
hash_equals()而不是===,防止时序攻击(timing attack) API_SECRET必须保密,建议 ≥ 32 字符随机字符串serial字段防止重放:每次验证后服务端递增,下次请求必须用新 serial
3. 本地缓存策略
如果每次 WordPress 后台加载都调用 API 验证授权,会导致:
- 性能问题:每个管理页面打开都要等 API 响应
- 服务器压力:高频请求占用 CPU、带宽
- 离线不可用:网络故障时插件不能工作
7 天缓存机制
系统采用软缓存方案:
<?php
function shiguang_quark_qrcode_check_license() {
$cached = get_option('shiguang_quark_qrcode_license', null);
if (!$cached) {
// 缓存不存在 → 在线验证
return verify_online();
}
// 检查缓存是否过期(7 天)
$last_check = strtotime($cached['last_check'] ?? '1970-01-01');
if (time() - $last_check > 7 * 86400) {
// 缓存过期 → 在线验证 + 更新缓存
$online = verify_online();
if ($online['status'] === 'success') {
update_option('shiguang_quark_qrcode_license', array_merge($online, [
'last_check' => date('Y-m-d H:i:s'),
]));
}
return $online;
}
// 缓存有效 → 直接返回
return $cached;
}
缓存的内容包括:
- 授权码(license_key)
- 授权类型(standard / pro / trial)
- 域名(用于本地校验)
- 过期时间(expires_at)
- 上次验证时间(last_check)
- 状态(active / suspended)
缓存失败时的容错:如果在线验证失败(如服务器故障),插件仍使用缓存授权,确保用户业务不中断。直到缓存完全过期才会强制要求在线验证。
4. 域名标准化
用户在购买时可能填写各种形式的域名:
https://www.example.com/example.comWWW.EXAMPLE.COMexample.com/path/to/post
如果不标准化,会出现"同一个站点了买了 2 次"的尴尬情况。系统统一处理为:
<?php
function normalize_domain($input) {
// 1. 转小写
$domain = strtolower(trim($input));
// 2. 去除协议头(http://, https://)
$domain = preg_replace('#^https?://#', '', $domain);
// 3. 去除路径部分(第一个 / 之后全部去掉)
if (($pos = strpos($domain, '/')) !== false) {
$domain = substr($domain, 0, $pos);
}
// 4. 去除端口
if (($pos = strpos($domain, ':')) !== false) {
$domain = substr($domain, 0, $pos);
}
// 5. 去除 www. 前缀(可选)
$domain = preg_replace('#^www\.#', '', $domain);
return $domain;
}
// 测试
echo normalize_domain('HTTPS://WWW.Example.COM/post/1'); // output: example.com
数据库里存的和 API 比对时,都是标准化后的结果。
5. 订单生命周期
创建订单 支付 完成 激活
│ │ │ │
▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌──────────┐ ┌──────────┐
│ pending│──────▶│ paid │─────────▶│ completed │───────▶│ active │
└────────┘ └────────┘ └──────────┘ └──────────┘
│ │
│ 超时 30min │
▼ │ 过期
┌────────┐ ▼
│ failed │ ┌──────────┐
└────────┘ │ expired │
└──────────┘
状态机详解
- pending:用户下单后未支付,30 分钟后自动转 failed
- paid:用户已支付,但插件还未激活(需要走完激活流程)
- completed:订单已生成授权码(在
lic_licenses表里) - active:用户已在插件后台输入授权码并激活
- failed:订单超时未支付 / 用户取消
- refunded:退款后状态(待实现)
- expired:超过
expires_at时间
关键函数
complete_order($order):订单支付成功后被调用,生成授权码:
<?php
function complete_order($order) {
$db = get_db();
// 1. 生成唯一授权码
$license_key = generate_license_key(); // 例如:LIC-9C20E-C6A64-B6DE1-D4F90
// 2. 计算过期时间(产品有效期决定)
$product = get_product($order['product_id']);
$expires_at = $product['validity_days'] > 0
? date('Y-m-d H:i:s', time() + $product['validity_days'] * 86400)
: null; // null 表示永久
// 3. 插入授权记录
$db->prepare("INSERT INTO " . table('licenses') . "
(license_key, user_id, product_id, domain, status, expires_at, created_at)
VALUES (?, ?, ?, ?, 'active', ?, NOW())")
->execute([$license_key, $order['user_id'], $order['product_id'],
$order['domain'], $expires_at]);
$license_id = $db->lastInsertId();
// 4. 关联订单
$db->prepare("UPDATE " . table('orders') . "
SET pay_status = 'completed', license_id = ?, completed_at = NOW()
WHERE id = ?")
->execute([$license_id, $order['id']]);
return $license_id;
}
6. 防止授权码冲突
授权码格式:LIC-XXXXX-XXXXX-XXXXX-XXXXX(5 段 5 字符的 Base36)。
为了避免重复,使用循环重试:
<?php
function generate_license_key() {
$db = get_db();
for ($i = 0; $i < 10; $i++) {
// 生成 20 个 Base36 字符,分成 5 段
$chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$key = 'LIC-';
for ($seg = 0; $seg < 4; $seg++) {
for ($j = 0; $j < 5; $j++) {
$key .= $chars[random_int(0, 35)];
}
$key .= '-';
}
$key = rtrim($key, '-');
// 检查数据库是否已存在
$stmt = $db->prepare("SELECT id FROM " . table('licenses') . " WHERE license_key = ?");
$stmt->execute([$key]);
if (!$stmt->fetch()) {
return $key;
}
}
throw new Exception('生成授权码失败,请重试');
}
理论上 36^20 ≈ 1.3 × 10^31 种组合,10 次循环内冲突概率 ≈ 0。
设计哲学总结:
- 性能 vs 安全:7 天软缓存平衡两者
- 用户体验:域名标准化避免"看似不同实则相同"的混乱
- 可追溯性:所有关键操作都有审计日志
- 扩展性:产品独立配置价格、有效期、授权类型