使用文档

时光授权系统文档

从快速开始到 API 对接,全面了解授权系统的使用与集成

设计思路

本章深入解析授权系统的核心设计原理,包括授权验证流程、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.com
  • WWW.EXAMPLE.COM
  • example.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 天软缓存平衡两者
  • 用户体验:域名标准化避免"看似不同实则相同"的混乱
  • 可追溯性:所有关键操作都有审计日志
  • 扩展性:产品独立配置价格、有效期、授权类型