上篇分享了自建 WordPress 插件授权中心(服务器端),很多朋友问:服务器搭好了,插件客户端怎么对接?今天就手把手把客户端完整代码贴出来,复制即用,零门槛接入。
先说结论:整个对接只需要一个 PHP 类(约 300 行),搞定激活、停用、心跳复核、自动更新、秒级锁定全部功能。
一、对接原理(先看流程再抄代码)
客户端与服务器之间就 4 个动作,理解这张图就够了:
① 激活 设置页填授权码 ──activate──▶ 服务器记录「码 + 站点 + 插件」② 心跳 每小时 ──verify──▶ 服务器复核授权,无效则本地锁定③ 更新 WordPress 检查更新 ──check_update──▶ 返回新版本 + 下载地址(带令牌)④ 锁定 服务器停用/删除授权 ──webhook 推送──▶ 插件本地立即失效
安全设计(重点理解):
服务器所有响应都带 HMAC-SHA256 签名,客户端收到后必须验签,防止中间人篡改、防止别人伪造服务器 更新包的下载地址带时效令牌(24 小时有效),无法被无限转用 服务器推送的锁定 webhook 带时间戳签名,防重放攻击
⚠️ 最容易踩的坑:验签时重新编码 JSON 必须用和服务器相同的参数
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
否则中文、斜杠被默认转义,签名永远对不上(代码里已处理)。
二、完整代码
第 1 步:客户端核心类
把下面的代码保存为 inc/license-client.php(放在你的插件目录下):
<?php/*** QM_License_Client —— WordPress 插件授权客户端* 对接「插件授权中心」服务器(activate / deactivate / check_update / verify / plugin_info / webhook)*/classQM_License_Client{private $slug; // 插件 slug,必须与服务器上传时一致private $server_url; // 服务器基础地址private $secret; // 与服务器 LS_SECRET 一致private $name; // 插件显示名private $version; // 当前版本号private $main_file; // 插件主文件(用于更新列表标识)private $opt_key; // 授权信息存储键public function__construct($slug, $args = array()) {$this->slug = $slug;$this->server_url = rtrim(isset($args['server_url']) ? $args['server_url'] : '', '/');$this->secret = isset($args['secret']) ? $args['secret'] : '';$this->name = isset($args['name']) ? $args['name'] : $slug;$this->version = isset($args['version']) ? $args['version'] : '';$this->main_file = $slug . '/' . $slug . '.php';$this->opt_key = 'qm_license_' . $slug;// 后台设置页add_action('admin_menu', array($this, 'add_settings_page'));add_action('admin_notices', array($this, 'show_notice'));// 激活 / 停用端点add_action('admin_post_qm_activate_' . $slug, array($this, 'handle_activate'));add_action('admin_post_qm_deactivate_' . $slug, array($this, 'handle_deactivate'));// 接收服务器推送的 webhook(授权被停用/删除 → 本地秒级锁定)add_action('admin_post_wppl_revoke_' . $slug, array($this, 'handle_revoke'));// 自动更新:更新检测 + 详情弹窗add_filter('pre_set_site_transient_update_plugins', array($this, 'check_update'));add_filter('plugins_api', array($this, 'plugin_info'), 10, 3);// 定时复核(每小时心跳,服务器判定失效则本地锁定)add_action('qm_license_heartbeat_' . $slug, array($this, 'heartbeat'));if (!wp_next_scheduled('qm_license_heartbeat_' . $slug)) {wp_schedule_event(time() + 3600, 'hourly', 'qm_license_heartbeat_' . $slug);}}// ============================================================ HTTP + 签名/*** 调用服务器 API,并校验响应 HMAC 签名。* 关键点:服务器用 json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)* 生成 data 字符串再签名;客户端必须用【相同选项】重新编码再比对。*/private functionapi_call($action, $params = array()) {$params['site_url'] = home_url();$params['plugin_slug'] = $this->slug;$resp = wp_remote_post($this->server_url . '/?action=' . $action, array('timeout' => 20,'body' => $params,));if (is_wp_error($resp)) {return array('success' => false, 'error' => 'network', 'message' => $resp->get_error_message());}$json = json_decode(wp_remote_retrieve_body($resp), true);if (!is_array($json) || !isset($json['data']) || !isset($json['sig'])) {return array('success' => false, 'error' => 'bad_response', 'message' => '服务器响应异常');}$expect = hash_hmac('sha256', json_encode($json['data'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), $this->secret);if (!hash_equals($expect, $json['sig'])) {return array('success' => false, 'error' => 'bad_sig', 'message' => '响应签名校验失败');}return $json['data'];}/** 服务器 reason 码 → 中文提示 */private functionerr_text($r) {$map = array('invalid' => '授权码无效','disabled' => '授权已被停用','expired' => '授权已过期','plugin_mismatch' => '授权码与当前插件不匹配','no_instance' => '该站点尚未激活此插件','instance_slug_mismatch' => '该授权码已绑定其它插件','limit_exceeded' => '激活次数已达上限','bad_sig' => '响应签名校验失败','network' => '无法连接授权服务器','bad_response' => '服务器响应异常',);$e = isset($r['error']) ? $r['error'] : '';return isset($map[$e]) ? $map[$e] : (isset($r['message']) ? $r['message'] : '未知错误');}// ============================================================ 授权状态/** 本地是否已激活(业务门控用) */public functionis_active() {$lic = get_option($this->opt_key);return !empty($lic['key']);}/** 获取本地保存的授权信息 */public functionget_license() {return get_option($this->opt_key, array());}// ============================================================ 激活 / 停用public functionhandle_activate() {check_admin_referer('qm_activate_' . $this->slug);$key = isset($_POST['license_key']) ? strtoupper(trim($_POST['license_key'])) : '';if ($key === '') {$this->notice('请填写授权码', 'error');wp_safe_redirect(admin_url('options-general.php?page=qm-license-' . $this->slug));exit;}$r = $this->api_call('activate', array('license_key' => $key));if (!empty($r['success'])) {update_option($this->opt_key, array('key' => $key,'activated_at' => current_time('mysql'),'expires_at' => isset($r['expires_at']) ? $r['expires_at'] : '',));$left = (isset($r['activations_left']) && $r['activations_left'] !== null)? $r['activations_left'] . ' 个' : '不限';$this->notice('激活成功!剩余可激活站点:' . $left);} else {$this->notice('激活失败:' . $this->err_text($r), 'error');}wp_safe_redirect(admin_url('options-general.php?page=qm-license-' . $this->slug));exit;}public functionhandle_deactivate() {check_admin_referer('qm_deactivate_' . $this->slug);$lic = get_option($this->opt_key);if (!empty($lic['key'])) {$this->api_call('deactivate', array('license_key' => $lic['key']));}delete_option($this->opt_key);$this->notice('已停用授权并解除本机绑定');wp_safe_redirect(admin_url('options-general.php?page=qm-license-' . $this->slug));exit;}// ============================================================ 复核心跳public functionheartbeat() {$lic = get_option($this->opt_key);if (empty($lic['key'])) {return;}$r = $this->api_call('verify', array('license_key' => $lic['key']));if (empty($r['license_valid'])) {delete_option($this->opt_key); // 服务器判定无效 → 本地锁定}}// ============================================================ webhook 秒级锁定public functionhandle_revoke() {$slug = isset($_POST['slug']) ? $_POST['slug'] : '';$key = isset($_POST['license_key']) ? strtoupper(trim($_POST['license_key'])) : '';$ts = isset($_POST['ts']) ? intval($_POST['ts']) : 0;$sig = isset($_POST['sig']) ? $_POST['sig'] : '';// 防重放:时间戳偏差超过 10 分钟直接拒绝if ($ts < time() - 600 || $ts > time() + 600) {wp_die('expired');}$expect = hash_hmac('sha256', $slug . '|' . $key . '|' . $ts, $this->secret);if (!hash_equals($expect, $sig)) {wp_die('bad sig');}$lic = get_option($this->opt_key);if (!empty($lic['key']) && strtoupper($lic['key']) === $key) {delete_option($this->opt_key);update_option($this->opt_key . '_locked', current_time('mysql'));}wp_die('ok');}// ============================================================ 自动更新public functioncheck_update($transient) {if (empty($transient) || !is_object($transient)) {return $transient;}$lic = get_option($this->opt_key);if (empty($lic['key'])) {return $transient; // 未激活不推送更新}$r = $this->api_call('check_update', array('license_key' => $lic['key'],'current_version' => $this->version,));if (empty($r['license_valid'])) {delete_option($this->opt_key); // 授权失效,不再提供更新return $transient;}if (!empty($r['update'])) {$u = $r['update'];$obj = new stdClass();$obj->slug = $u['slug'];$obj->plugin = $this->main_file;$obj->new_version = $u['version'];$obj->package = $u['package']; // 已带下载令牌,有效期 24h$obj->url = isset($u['url']) ? $u['url'] : '';$obj->requires = isset($u['requires']) ? $u['requires'] : '';$obj->requires_php = isset($u['requires_php']) ? $u['requires_php'] : '';$obj->tested = isset($u['tested']) ? $u['tested'] : '';$transient->response[$this->main_file] = $obj;}return $transient;}public functionplugin_info($res, $action, $args) {if ($action !== 'plugin_information' || empty($args->slug) || $args->slug !== $this->slug) {return $res;}$lic = get_option($this->opt_key);$r = $this->api_call('plugin_info', array('license_key' => isset($lic['key']) ? $lic['key'] : '',));if (empty($r['success'])) {return $res;}$sections = isset($r['sections']) ? $r['sections'] : array();return (object) array('slug' => $r['slug'],'name' => $r['name'],'version' => $r['version'],'requires' => $r['requires'],'requires_php' => $r['requires_php'],'tested' => $r['tested'],'homepage' => $r['homepage'],'sections' => array('changelog' => isset($sections['changelog']) ? $sections['changelog'] : '','description' => isset($sections['description']) ? $sections['description'] : '',),'download_link' => '',);}// ============================================================ 后台设置页public functionadd_settings_page() {add_options_page($this->name . ' 授权',$this->name . ' 授权','manage_options','qm-license-' . $this->slug,array($this, 'render_settings_page'));}public functionrender_settings_page() {$lic = get_option($this->opt_key);$active = !empty($lic['key']);echo '' . esc_html($this->name) . ' 授权管理';if ($active) {echo '授权状态:已激活'. '授权码:' . esc_html($lic['key']) . ''. (!empty($lic['expires_at']) ? ' | 到期时间:' . esc_html($lic['expires_at']) : ' | 永久有效'). ' | 激活时间:' . esc_html($lic['activated_at']) . ''. ''. 'slug) . '”>'. wp_nonce_field('qm_deactivate_' . $this->slug, '_wpnonce', true, false). '停用授权';} else {echo '当前未激活,填写授权码后即可正常使用插件并获得自动更新。'. ''. 'slug) . '”>'. wp_nonce_field('qm_activate_' . $this->slug, '_wpnonce', true, false). '授权码'. ''. ''. '激活授权';}echo '';}// ============================================================ 提示消息private functionnotice($msg, $type = 'success') {set_transient($this->opt_key . '_notice', array('m' => $msg, 't' => $type), 60);}public functionshow_notice() {$n = get_transient($this->opt_key . '_notice');if (!empty($n)) {echo '' . esc_html($n['m']) . '';}}}
第 2 步:主文件接入(只需 3 行 + 一个门控)
在你插件的主文件(如 my-plugin.php)里加上:
<?php/*** Plugin Name: 我的插件* Description: 带授权 + 自动更新* Version: 1.0.0*/require_once __DIR__ . '/inc/license-client.php';$GLOBALS['qm_license'] = new QM_License_Client('my-plugin', array('server_url' => 'https://your-domain.com/license-server', // ① 你的服务器地址'secret' => '与服务器 config.php 的 LS_SECRET 保持一致', // ② 签名密钥'name' => '我的插件','version' => '1.0.0', // ③ 当前版本号,务必与服务器上传的版本一致));// 业务门控:未激活时不加载核心功能$GLOBALS['qm_licensed'] = $GLOBALS['qm_license']->is_active();if (!$GLOBALS['qm_licensed']) {add_action('admin_notices', function () {echo '插件未激活,请到「设置 → 我的插件 授权」填写授权码。';});}// ====== 以下写你的插件核心功能,在功能入口判断 $GLOBALS['qm_licensed'] 即可 ======
三、接入三步走
放代码:把 license-client.php放到插件inc/目录,主文件里require+new QM_License_Client(...)对齐三项: server_url(服务器地址)、secret(与服务器config.php的LS_SECRET完全一致)、slug(与服务器上传插件时的 Slug 一致,注意大小写)测试闭环:后台「设置 → 我的插件 授权」输入授权码 → 激活成功 → 服务器发布新版本 → WordPress 后台出现更新 → 点击更新自动下载安装 ✅
四、常见问题排查
secret | ||
wp-admin/admin-post.php | ||
LS_SERVER_URL 是否为插件实际访问的地址 |
五、源码获取
本文完整源码(客户端类 + 接入示例 + 上期服务器端)已打包,关注公众号【公众号名称】,回复关键词「客户端代码」即可获取。
服务器端还没搭?看上一篇《自建WordPress插件授权中心,开源免费,全功能保姆级教程!》,回复「千猫授权中心」获取。
觉得有用,点赞、在看、转发给做 WordPress 的朋友,我们下期见!💪
文章首发于公众号【灵哥学社】,转载请联系授权。
夜雨聆风