
简介这份“Dsdiss Verification”插件是一款用于WordPress内容隐藏与公众号引流的轻量工具适合希望在博文中定向展示部分内容、借此吸引访客关注微信公众号的站点运营者。插件通过短代码标记隐藏区域访客输入管理员设置的验证密码后可查看并用Cookie记录验证状态操作门槛低基本不改变原有写作流程。资源包体积仅4KB共3个文件包括核心PHP主文件、前端样式表与交互脚本结构十分精简可快速部署到插件目录或对照源码二次开发。已有48人学习下载适合具备一定WordPress使用经验的用户参考。通过该资源可掌握插件后台的二维码设置逻辑、密码与提示信息配置方法以及前后端交互和Cookie验证的实现思路便于根据自身公众号运营需求灵活调整隐藏与引流流程。1. 隐藏内容引流到微信公众号WordPress 插件真正要解决的不是“藏”而是“验证”一个常见的 WordPress 站点场景文章列表页正常展示点进详情页前半段可读后半段被一个带锁的容器挡住旁边是“关注公众号解锁全文”的按钮。用户点了按钮跳到公众号主页关注之后回到浏览器内容依然是锁住的——因为浏览器根本没有收到“已关注”的信号。这个插件的核心工作是把“用户是否已关注公众号”变成一个可验证的状态再把内容渲染权限跟这个状态绑定。它适合有稳定内容输出、想用公众号沉淀粉丝的站长也适合做付费课程引流的个人开发者。注意一个关键边界真正的“隐藏内容引流”依赖微信公众平台的服务号能力个人订阅号只能做到“展示引导块”没法自动判断关注状态这点在选型时必须先想清楚。下面直接从服务号网页授权这条链路讲起。2. 微信公众号网页授权链路先用 snsapi_base 换 openid再查关注状态2.1 为什么“显示隐藏内容”按钮不是答案很多现成插件只做一件事隐藏内容区里放一个链接用户点击后新窗口打开公众号主页页面本身不做任何状态判断。这种做法的问题是它不闭环——用户回不回来、有没有真的关注插件一概不知道。微信公众号网页授权可以补上这个闭环用户在 WordPress 站点上点击解锁按钮后被重定向到微信开放平台授权页授权完成后带着 code 回到站点站点用 code 换取该用户的 openid再调用用户信息接口查询 subscribe 字段subscribe 值为 1 就说明已关注此时才把隐藏内容渲染给用户。整个过程用户只需要在微信里完成一次确认无需输入任何表单信息。这条路线的约束是必须使用认证过的服务号并且服务器需要能访问微信 API。个人订阅号没有网页授权能力这种情况下只能降级为“点击后跳转公众号主页”的弱引导做不到自动验证。还有个容易被忽略的点网页授权回调域名只能填一个如果你的站点同时有 http 和 https 两种入口必须统一到 https否则授权回调会报 redirect_uri 参数错误。2.2 获取并缓存全局 Access Token先实现最底层的一环Access Token 的获取与缓存。公众号全局 Access Token 有效期 7200 秒同一时间只能有一个有效 token频繁请求会导致旧 token 失效所以必须用 WordPress 的 transient 做本地缓存。function wx_gate_get_access_token() { $cached get_transient(wx_gate_access_token); if ($cached) { return $cached; } $app_id get_option(wx_gate_app_id); $secret get_option(wx_gate_app_secret); $api_url https://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credentialappid . urlencode($app_id) . secret . urlencode($secret); $response wp_remote_get($api_url, array(timeout 10)); if (is_wp_error($response)) { return new WP_Error(wx_gate_http_error, $response-get_error_message()); } $body json_decode(wp_remote_retrieve_body($response), true); if (isset($body[errcode])) { return new WP_Error(wx_gate_api_error, $body[errmsg]); } // 实际过期时间为7200秒这里留300秒余量避免边界请求时token刚好失效 set_transient(wx_gate_access_token, $body[access_token], 6900); return $body[access_token]; }这里的关键参数是 transient 过期时间 6900 秒。如果设成 7200可能在 token 刚好过期的那一瞬间取到已失效的值设得太短又会频繁请求微信接口。另外wp_remote_get 的 timeout 建议不低于 10 秒在一些出口网络不稳定的机器上5 秒超时会导致解锁流程频繁失败。代码返回 WP_Error 而不是直接抛异常是为了让上层调用能统一做降级处理。2.3 构建网页授权跳转链接与回调处理网页授权的第一步是把用户带到微信的授权页。这里注意 scope 的选择。function wx_gate_build_auth_url($redirect_uri, $state ) { $app_id get_option(wx_gate_app_id); $encoded_redirect urlencode($redirect_uri); if (empty($state)) { $state wp_generate_password(16, false); set_transient(wx_gate_state_ . $state, $redirect_uri, 300); } return https://open.weixin.qq.com/connect/oauth2/authorize?appid . urlencode($app_id) . redirect_uri . $encoded_redirect . response_typecodescopesnsapi_base . state . $state . #wechat_redirect; }为什么要用 snsapi_base 而不是 snsapi_userinfo网页授权有两种 scopesnsapi_base 静默授权用户无感知只返回 openidsnsapi_userinfo 会弹确认框返回用户头像昵称等资料。本插件的场景只关心关注状态openid 就足够判断用 snsapi_base 能减少用户操作步骤授权通过率更高。state 参数是为了防 CSRF这里把 state 存在 transient 里并绑定原始回调地址回调时再做比对防止微信回调地址被恶意拼接。回调处理的核心是拿 code 换 openidfunction wx_gate_handle_oauth_callback() { if (!isset($_GET[wx_gate_oauth]) || !isset($_GET[code])) { return; } $state isset($_GET[state]) ? sanitize_text_field(wp_unslash($_GET[state])) : ; $redirect_uri get_transient(wx_gate_state_ . $state); if (empty($redirect_uri)) { wp_die(授权状态校验失败请返回文章页重试, 授权失败, array(response 400)); } $code sanitize_text_field(wp_unslash($_GET[code])); $app_id get_option(wx_gate_app_id); $secret get_option(wx_gate_app_secret); $token_url https://api.weixin.qq.com/sns/oauth2/access_token?appid . urlencode($app_id) . secret . urlencode($secret) . code . urlencode($code) . grant_typeauthorization_code; $response wp_remote_get($token_url, array(timeout 10)); $body json_decode(wp_remote_retrieve_body($response), true); if (isset($body[errcode])) { wp_die(获取用户身份失败 . esc_html($body[errmsg]), 授权失败, array(response 400)); } $openid sanitize_text_field($body[openid]); setcookie(wx_gate_openid, $openid, time() 30 * DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true); wp_safe_redirect($redirect_uri); exit; } add_action(init, wx_gate_handle_oauth_callback);code 只能使用一次重复使用会返回 errcode 40029。所以每次解锁流程都必须重新走一遍授权不能缓存 code。openid 写入 cookie 时设置了 HttpOnly这样前端 JS 读不到 openid可以防止内容解锁逻辑被绕过——后面内容解密时也不会依赖前端传入的 openid。2.4 查询关注状态subscribe 字段的判断逻辑拿到 openid 后还需要确认这个用户当前是否处于关注状态。微信用户信息接口会返回 subscribe 字段0 表示未关注1 表示已关注。function wx_gate_check_followed($openid) { $access_token wx_gate_get_access_token(); if (is_wp_error($access_token)) { return $access_token; } $api_url https://api.weixin.qq.com/cgi-bin/user/info?access_token . urlencode($access_token) . openid . urlencode($openid) . langzh_CN; $response wp_remote_get($api_url, array(timeout 10)); $body json_decode(wp_remote_retrieve_body($response), true); return isset($body[subscribe]) $body[subscribe] 1; }调用这个接口的限制是公众号必须在认证有效期内且 openid 必须是该公众号下用户的 openid。跨公众号或未认证的账号都会返回错误码。这里有个容易被忽视的边界用户关注后立刻点击解锁接口偶尔会因为数据同步延迟返回 subscribe0常见做法是在首次查询失败时不直接拒绝而是提示用户稍等几秒重试或者延迟 2 秒做一次重查。3. 在 WordPress 编辑器与前台渲染层做内容保护短代码加解密与 AJAX 解锁3.1 用短代码标记受保护内容区块内容保护的第一层是让编辑者在发布文章时能直观地标记“哪一段是隐藏的”。WordPress 的短代码机制在这里比 Gutenberg 自定义块更轻量复制粘贴不会丢格式也更容易被已有主题兼容。注册方式如下function wx_gate_register_shortcode($atts, $content null) { if (is_null($content)) { return ; } $atts shortcode_atts(array( tip 关注公众号解锁全文, ), $atts, wx_gated); // 输出缓冲捕获内部HTML避免短代码嵌套时被重复解析 $raw_content do_shortcode($content); $cipher wx_gate_encrypt_content($raw_content); $nonce wp_create_nonce(wx_gate_unlock_ . get_the_ID()); return div classwx-gate-block>function wx_gate_encrypt_content($html) { $key get_option(wx_gate_cipher_key); if (empty($key)) { $key wp_generate_password(32, false); update_option(wx_gate_cipher_key, $key); } $iv openssl_random_pseudo_bytes(16); $encrypted openssl_encrypt($html, AES-128-CBC, $key, 0, $iv); return base64_encode($iv . :: . $encrypted); } function wx_gate_decrypt_content($cipher_text) { $key get_option(wx_gate_cipher_key); $parts explode(::, base64_decode($cipher_text)); if (count($parts) ! 2) { return ; } return openssl_decrypt($parts[1], AES-128-CBC, $key, 0, $parts[0]); }AES-128-CBC 的密钥长度是 16 字节wp_generate_password(32, false) 生成的是 32 位字符截取前 16 位即可。解密失败时要返回空字符串而不是解密后的乱码避免把错误内容渲染给用户。密钥字段只存在数据库里绝不输出到前端。这样即使有人抓取整个 HTML拿到的也只是一段密文和一段 JS 壳无法直接还原原文。3.3 前台 AJAX 解锁关注后自动拉取内容当用户完成关注流程返回页面时前端脚本需要做两件事读取 cookie 中的 openid或通过服务端判断判断已关注后发起 AJAX 请求拉取解密内容并渲染。jQuery(function($) { $(.wx-gate-block).each(function() { var $block $(this); $block.find(.wx-gate-unlock-btn).on(click, function(e) { e.preventDefault(); var postId $block.data(post-id); var nonce $block.data(nonce); $.post(wxGateVars.ajaxUrl, { action: wx_gate_unlock, post_id: postId, nonce: nonce }, function(response) { if (response.success) { $block.html(response.data.html); } else { $block.find(.wx-gate-tip) .after(p stylecolor:red; response.data.message /p); } }); }); }); });对应的服务端 AJAX 处理器function wx_gate_ajax_unlock() { $post_id isset($_POST[post_id]) ? intval($_POST[post_id]) : 0; $nonce isset($_POST[nonce]) ? sanitize_text_field(wp_unslash($_POST[nonce])) : ; if (!wp_verify_nonce($nonce, wx_gate_unlock_ . $post_id)) { wp_send_json_error(array(message 安全校验失败请刷新页面重试)); } $openid isset($_COOKIE[wx_gate_openid]) ? sanitize_text_field(wp_unslash($_COOKIE[wx_gate_openid])) : ; if (empty($openid)) { $auth_url wx_gate_build_auth_url(wx_gate_current_url()); wp_send_json_error(array(message 请先完成微信授权, auth_url $auth_url)); } $followed wx_gate_check_followed($openid); if (is_wp_error($followed) || !$followed) { $auth_url wx_gate_get_mp_profile_url(); wp_send_json_error(array(message 请先关注公众号后刷新页面再次解锁, mp_url $auth_url)); } // 已关注用户记录解锁状态返回解密内容 wx_gate_record_unlock($post_id, $openid); $content get_post_field(post_content, $post_id); // 提取短代码中的密文这里简单通过正则取第一个wx_gated标签的data-cipher属性 // 生产环境建议在渲染shortcode时用global变量暂存密文避免二次解析 if (preg_match(/data-cipher([^])/, $content, $m)) { $html wx_gate_decrypt_content($m[1]); wp_send_json_success(array(html $html)); } else { wp_send_json_error(array(message 内容格式异常)); } } add_action(wp_ajax_wx_gate_unlock, wx_gate_ajax_unlock); add_action(wp_ajax_nopriv_wx_gate_unlock, wx_gate_ajax_unlock);需要留意的是上面在 post_content 里正则在全文范围内搜索>function wx_gate_localize_with_nonce() { $ajax_url admin_url(admin-ajax.php); wp_add_inline_script( wx-gate-frontend, var wxGateVars . wp_json_encode(array( ajaxUrl $ajax_url, )) . ;, before ); wp_script_add_data(wx-gate-frontend, nonce, wp_create_nonce(wx_gate_csp)); } add_action(wp_enqueue_scripts, wx_gate_localize_with_nonce, 20);开启 CSP 的站点需要把 script-src 中加上 nonce-xxx 并让 WordPress 自动输出对应 nonce 属性否则上面这段本地化脚本也会被浏览器拦截。缓存插件方面含有隐藏内容区块的页面不能被整页缓存成静态 HTML否则后到的用户拿到的是别人已经解锁过的渲染结果内容保护失去意义。一般做法是在 WP Super Cache 或 LiteSpeed Cache 中把包含 wx-gate-block 的 URL 加入排除名单或者给登录用户和管理员关闭缓存。4. 插件设置面板AppID、回调域名、解锁文案与缓存时长怎么配4.1 设置页需要哪些字段配置面板看起来只有几个输入框但字段设计直接影响插件能不能跑通。至少要包含以下几项配置字段键名说明AppIDwx_gate_app_id服务号应用唯一标识AppSecretwx_gate_app_secret服务号密钥只写入不回显回调域名wx_gate_redirect_domain必须和微信公众平台配置的域名完全一致解锁按钮文案wx_gate_btn_text默认“关注公众号解锁全文”关注后跳转地址wx_gate_mp_profile未关注用户最终跳转的公众号主页 URLToken 缓存时间wx_gate_token_ttl默认 6900 秒解锁记录保留天数wx_gate_keep_days白名单记录清理周期默认 180 天这里的“微信公众号主页链接怎么生成”有个常见误区——它不是一个需要代码生成的动态链接而是公众号后台“账号信息”里固定的 profile URL。既可以在微信内通过公众号搜索进入也可以直接复制 https://mp.weixin.qq.com/profile/ 后的那串参数。做法是把完整 URL 填到 wx_gate_mp_profile 字段里前端未关注弹层直接使用这个链接。4.2 register_setting 与字段安全处理注册设置项时要注意 secret 字段不能回显到页面。正常做法是单独存一个标志值判断是否已填写过function wx_gate_register_settings() { register_setting(wx_gate_options, wx_gate_app_secret, array( sanitize_callback sanitize_text_field, default , )); register_setting(wx_gate_options, wx_gate_btn_text, array( sanitize_callback sanitize_text_field, default 关注公众号解锁全文, )); register_setting(wx_gate_options, wx_gate_token_ttl, array( sanitize_callback absint, default 6900, )); } function wx_gate_render_secret_field() { $value get_option(wx_gate_app_secret); $has_secret get_option(wx_gate_has_secret); echo input typepassword namewx_gate_app_secret value . ($has_secret ? : ) . classregular-text autocompleteoff /; if ($has_secret empty($_POST)) { echo p classdescription已保存留空表示不修改/p; } } add_action(admin_init, wx_gate_register_settings);Token 缓存时间用 absint 做清洗防止负数和字符串注入。sanitize_callback 只在 options.php 保存时触发直接调用 update_option 的代码会绕过它所以只有配置页能改这些字段插件内部逻辑不要主动写这些 option。4.3 参数间的联动什么时候短代码失效配置面板还有三个开关需要明确其逻辑关系未填 AppID 或 AppSecret 时短代码在前台应渲染为普通内容或显示错误提示不能渲染成不可用按钮回调域名填写错误时授权跳转会失败此时应给出明确错误而不是无限重定向管理员登录状态下默认绕过解锁限制方便编辑预览文章但在配置面板里要有一个开关控制是否允许管理员绕过防止误操作后线上文章对管理员和访客展示不一致。这几个开关的联动建议统一放在一个 wx_gate_is_configured() 函数里判断所有入口都调用它避免散落各处出现状态不一致。配置完成后用微信开发者工具扫码登录公众号后台核对回调域名是否包含 https 前缀。5. 解锁白名单与降级策略二次访问免验证、取消关注重锁定5.1 建表记录已解锁用户每次都调微信用户信息接口不仅慢还可能触发接口频率限制。常规做法是建一张解锁记录表用户第一次验证通过后写入后续访问直接查表。数据库表结构如下CREATE TABLE wp_wx_gate_records ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, post_id BIGINT UNSIGNED NOT NULL, openid VARCHAR(64) NOT NULL, unlocked_at DATETIME NOT NULL, expires_at DATETIME NOT NULL, PRIMARY KEY (id), KEY idx_post_openid (post_id, openid), KEY idx_expires (expires_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;expires_at 字段是白名单的过期时间。和公众号“取关后再关注”的场景对应如果用户取关后马上重新关注openid 不会变白名单记录依旧有效一般不用做硬性过期。但为了账号安全和数据清理建议把有效期设置为 180 天过期后需要重新验证。5.2 查询与写入逻辑function wx_gate_is_unlocked($post_id, $openid) { global $wpdb; $table $wpdb-prefix . wx_gate_records; $row $wpdb-get_var($wpdb-prepare( SELECT id FROM {$table} WHERE post_id %d AND openid %s AND expires_at NOW() LIMIT 1, $post_id, $openid )); return (bool) $row; } function wx_gate_record_unlock($post_id, $openid) { global $wpdb; $table $wpdb-prefix . wx_gate_records; $exists $wpdb-get_var($wpdb-prepare( SELECT id FROM {$table} WHERE post_id %d AND openid %s LIMIT 1, $post_id, $openid )); if ($exists) { $wpdb-update( $table, array(unlocked_at current_time(mysql), expires_at gmdate(Y-m-d H:i:s, time() 180 * DAY_IN_SECONDS)), array(id $exists) ); } else { $wpdb-insert($table, array( post_id $post_id, openid $openid, unlocked_at current_time(mysql), expires_at gmdate(Y-m-d H:i:s, time() 180 * DAY_IN_SECONDS), )); } }注意前缀使用 $wpdb-prefix 而不是硬编码 wp_因为多站点环境下每个子站可能使用不同前缀。openid 是微信体系内的稳定标识跨设备不失效但涉及隐私建议记录时可存储 openid 的 SHA256 哈希值而非明文查询时用哈希值匹配不过注意微信用户信息接口的入参必须是明文 openid所以这个选项要根据合规要求权衡。5.3 降级策略Token 失败与取消关注场景一Access Token 获取失败。最常见原因是 AppSecret 被重置或公众号接口调用达到频率上限。此时解锁按钮的 AJAX 请求会拿到服务端返回的 WP_Error前端应显示“服务暂时不可用”而不是一直转圈同时再给用户提供一个“跳到公众号主页”的兜底按钮至少保证引流入口不失效。场景二用户已关注但接口查询返回 subscribe0。数据同步延迟是真实存在的尤其是用户刚关注完立刻回来。处理方式是对首次查询失败的 openid 延迟 2 秒自动重查一次仍失败才拒绝。重查逻辑要放在服务端不能让用户手动刷新后前端反复请求。场景三用户首次解锁成功随后取关。下次访问时前端会带 openid 访问文章页服务端先查白名单命中则直接展示内容。因为白名单判断优先于实时接口查询取关用户依旧能看已解锁的文章。要收紧这个策略的话可以把白名单过期时间调短或对 CC 流量较高的站点每个会话做一次实时 subscribe 校验普通站点建议保留宽松策略减少微信接口压力。场景四文章被转载到其他平台。短代码内容加密后只有本站密钥能解开其他人粘贴 HTML 也拿不到明文。密钥管理上换域名或迁移服务器时要注意把 wx_gate_cipher_key 一并导出否则迁移后所有历史文章无法解密。5.4 定时清理过期记录过期白名单记录如果一直堆积会拖慢查询。用 WP-Cron 每天清理一次function wx_gate_schedule_cleanup() { if (!wp_next_scheduled(wx_gate_daily_cleanup)) { wp_schedule_event(time(), daily, wx_gate_daily_cleanup); } } add_action(init, wx_gate_schedule_cleanup); function wx_gate_cleanup_records() { global $wpdb; $table $wpdb-prefix . wx_gate_records; $wpdb-query(DELETE FROM {$table} WHERE expires_at NOW()); } add_action(wx_gate_daily_cleanup, wx_gate_cleanup_records);这个定时清理不需要 real-time cron每日执行一次足够。如果站点有多个负载均衡节点注意只让一个节点注册 cron 事件否则会出现重复清理虽然 DELETE 本身是无害的但会徒增数据库负载。6. 验证方法五分钟检查清单与双环境隔离参数先给出一份可以直接复制到项目文档的验收清单验证场景操作预期结果未关注首次访问无痕窗口打开文章页点击解锁按钮跳转微信授权页授权后回到文章页内容解锁展示已关注二次访问清除浏览器缓存后重新打开文章内容直接展示无需再次授权取关后访问公众号后台取消关注再回文章页内容重新锁定白名单过期后密钥错误修改 wx_gate_cipher_key 后刷新页面提示解密失败不输出乱码AppSecret 错误保存错误密钥后点击解锁AJAX 返回友好错误不白屏调试时先看 Network 面板里三条关键请求第一跳是 open.weixin.qq.com 的 302第二跳是微信授权页返回的 code 回调第三跳是 /sns/oauth2/access_token 返回 JSON。code 只要出现一次第二次再用就会报 invalid code这是正常现象不用怀疑插件逻辑。双环境隔离的做法是把配置项放进 wp-config.php 而不是数据库// wp-config.php define(WX_GATE_APP_ID, wx1234567890abcdef); define(WX_GATE_APP_SECRET, your-secret);插件读取时优先取常量取不到再回退到后台配置$app_id defined(WX_GATE_APP_ID) ? WX_GATE_APP_ID : get_option(wx_gate_app_id);这样本地开发环境配一套测试号线上配正式号代码仓库里不会出现密钥。另外微信开发者工具登录时提示“未绑定公众号”通常是工具本身未绑定服务号开发者权限与插件无关在公众号后台的开发者设置里添加对应微信号即可。“微信公众号定时自动读取”如果要在插件里实现类似能力可以做一件事用同样的 Access Token 缓存机制结合 WP-Cron 在后台定时拉取公众号的文章列表再把文章标题和链接回写到 WordPress 对应栏目中。这条链路共用本插件的 token 获取逻辑不需要再写一套 API 客户端。把上面这份清单贴进项目文档下次换域名、换微信号或者排查线上解锁异常时能省下一小时。本文还有配套的精品资源点击获取