Discuz! QQ登录插件改进版2.4:解决callback_url_mismatch与unionid去重
简介这是一款专为Discuz!论坛管理员与PHP二次开发者设计的QQ快捷登录增强插件解决传统注册流程繁琐、用户流失率高、社交登录兼容性差等实际问题尤其适用于面向年轻用户的社区类网站快速集成腾讯生态。资源包共61个文件涵盖24个核心PHP逻辑文件如qqoauth.inc.php、httqqlogin.class.php、回调与绑定模块、12张JPG/PNG图标与界面素材、8个HTML/HTM前端模板页以及XML插件配置文件和安装卸载脚本整体仅1MB轻量易部署。已有429人学习下载说明其在中小论坛运维场景中具备较强实用性与验证基础。用户可直接获取完整可运行的改进版2.4源码包含多编码版本适配GBK/UTF8/BIG5、移动端支持类、错误处理增强逻辑及详细安装说明无需从零开发即可完成QQ登录功能上线并基于现有结构进行安全加固或UI定制。1. DZ QQ登录插件北岸QQ登录改进版2.4不是“一键接入”而是Discuz! X3.2–X3.5环境下QQ第三方登录的稳定落地方案你是不是也试过在Discuz! X3.4后台上传原版QQ登录插件点启用就报“callback_url_mismatch”或直接白屏是不是在调试OAuth2回调时反复修改connect.php却始终卡在“未授权”页面连QQ开放平台的redirect_uri都对得眼花这不是你配置错了——是原生Discuz! QQ插件自2018年起就不再适配QQ开放平台v3接口的Token刷新机制与Scope字段校验逻辑。而北岸QQ登录改进版2.4正是某高校实验室在真实生产环境日活30万社区中为解决“登录成功但无法同步头像/昵称”“用户重复注册”“手机QQ扫码后无响应”三大顽疾逆向分析QQ OpenAPI v3.2.1文档、重写OAuth2状态管理与用户映射模块后发布的实战补丁包。它不替换Discuz!核心文件不依赖任何外部JS SDK所有逻辑收敛在source/plugin/qqlogin/目录下适配X3.2至X3.5全系列且已通过UTF-8编码兼容性压测。如果你正维护一个老Discuz!社区又不想推倒重做单点登录体系这份改进版就是你该停下来的最后一站。2. 插件结构与核心机制为什么必须重写qq_login.class.php而非打补丁Discuz!原生QQ插件的问题根源不在UI而在身份认证链路的三个断点OAuth2授权码未绑定Discuz! session、access_token刷新未持久化、用户信息拉取时忽略unionid去重逻辑。北岸改进版2.4通过重构qq_login.class.php的on_login()与on_callback()方法将认证流程拆解为可审计的四阶段预授权态生成 → 回调参数校验 → Token双存储 → 用户档案归一化。下面逐层解析其设计选择与代码实现。2.1 目录结构与文件职责划分拒绝“把所有逻辑塞进一个PHP文件”改进版2.4共12个文件按功能严格分层避免Discuz!插件常见的“上帝类”反模式文件路径类型核心职责关键改动点source/plugin/qqlogin/qq_login.class.php主控类实现discuz_plugin接口定义install()/uninstall()及global_header()钩子重写on_callback()引入state参数防CSRF移除硬编码redirect_urisource/plugin/qqlogin/include/qqapi.class.phpAPI封装封装QQ OpenAPI v3.2.1的/oauth2.0/token、/user/get_user_info等接口增加refresh_token自动续期逻辑失败时回退至access_token有效期检测source/plugin/qqlogin/template/qq_login.htm前端模板渲染QQ登录按钮与扫码弹窗移除jQuery依赖改用原生fetch()调用plugin.php?idqqlogin:ajaxactqr生成动态二维码source/plugin/qqlogin/install.php安装脚本创建pre_common_member_qq数据表添加qq_openid/qq_unionid字段新增unionid唯一索引强制qq_unionid非空约束解决多APP同一用户重复注册提示pre_common_member_qq表是本插件的核心状态表它不存储明文token只存access_token加密后、expires_in秒级时间戳、refresh_token加密后及unionid。所有敏感字段均经authcode()二次加密密钥取自Discuz!全局$_G[config][security][authkey]。2.2 OAuth2流程重写从“授权码换Token”到“Token续期闭环”原版插件在用户首次登录后仅缓存access_token3600秒过期即要求重新扫码。改进版2.4通过qqapi.class.php中的get_access_token()方法构建续期闭环// source/plugin/qqlogin/include/qqapi.class.php public function get_access_token($code, $state ) { // 1. 校验state参数防CSRF原版缺失 if (empty($_SESSION[qq_state]) || $_SESSION[qq_state] ! $state) { throw new Exception(Invalid state parameter); } // 2. 构造Token请求体关键显式声明grant_type $params [ grant_type authorization_code, client_id $this-appid, client_secret $this-appkey, code $code, redirect_uri rawurlencode($this-callback_url) ]; // 3. 调用QQ OpenAPI获取初始Token $response $this-http_post(https://graph.qq.com/oauth2.0/token, $params); parse_str($response, $token_data); // 4. 若含refresh_token则持久化存储原版从未保存 if (!empty($token_data[refresh_token])) { $encrypted_refresh authcode($token_data[refresh_token], ENCODE); C::t(common_member_qq)-update_by_openid($token_data[openid], [ refresh_token $encrypted_refresh, refresh_expires TIMESTAMP 30 * 86400 // 刷新Token有效期30天 ]); } return $token_data; }逻辑说明state参数校验是OAuth2安全基线原版完全缺失导致CSRF攻击面敞开改进版将其与Discuz! session绑定每次扫码前生成随机state并存入$_SESSION。refresh_token被加密后存入数据库而非仅存在内存。当access_token过期时插件会自动调用/oauth2.0/token?grant_typerefresh_token刷新无需用户再次扫码。refresh_expires字段设为30天符合QQ开放平台对Refresh Token的有效期策略实际值以QQ文档为准此处为保险起见设为30天。2.3 用户映射逻辑升级用unionid终结“同一人多个账号”乱象Discuz!原生插件仅用openid作为用户标识而openid在不同QQ APPID下完全不同。当用户先用A社区登录、再用B社区登录就会创建两个独立账号。改进版2.4强制启用unionid需在QQ开放平台后台开启“UnionID机制”并在qq_login.class.php的on_callback()中实现归一化// source/plugin/qqlogin/qq_login.class.php节选 public function on_callback() { // ... 前置校验省略 ... // 1. 获取用户基础信息含unionid $user_info $qqapi-get_user_info($access_token, $openid); // 2. 若返回unionid为空降级使用openid兼容未开通UnionID的旧应用 $bind_key !empty($user_info[unionid]) ? $user_info[unionid] : $user_info[openid]; // 3. 查询是否已有绑定记录 $member_qq C::t(common_member_qq)-fetch_by_bindkey($bind_key); if ($member_qq) { // 已存在直接登录更新token与最后登录时间 $this-login_existing_user($member_qq[uid]); } else { // 不存在创建新用户但用unionid作为主键绑定 $new_uid $this-create_new_user($user_info); C::t(common_member_qq)-insert([ uid $new_uid, bindkey $bind_key, // 此处存unionid或openid openid $user_info[openid], unionid $user_info[unionid], access_token authcode($access_token, ENCODE), expires_in TIMESTAMP $expires_in ]); } }参数说明$bind_key是归一化键优先取unionid若为空则回退openid。这保证了插件在未开通UnionID的场景下仍可运行但强烈建议在QQ开放平台后台为应用开启UnionID路径应用管理 → 应用设置 → 网站应用 → UnionID机制。common_member_qq表的bindkey字段建立唯一索引确保同一unionid只能绑定一个Discuz!账号。create_new_user()方法内部会校验用户名是否已存在若user_info[nickname]被占用则自动追加数字后缀如“张三2”避免注册失败。3. 部署与配置从QQ开放平台申请到Discuz!后台启用的六步实操部署不是上传ZIP解压完事而是涉及QQ开放平台配置、Discuz!数据库变更、插件参数校准的完整链路。以下步骤基于Discuz! X3.4 UTF-8环境实测每一步均有验证点和失败回滚方案。3.1 QQ开放平台应用创建与关键参数提取登录QQ开放平台open.qq.com进入“管理中心” → “创建网站应用”。填写应用名称如“XX社区QQ登录”、应用简介、官网地址填Discuz!根域名如https://bbs.example.com。在“基本资料”页必须勾选“启用UnionID机制”否则unionid字段恒为空。进入“开发设置”页配置合法回调域名填写Discuz!论坛的完整域名如bbs.example.com不要带http://或https://。在“网站应用”页记录以下三个参数后续必填APP ID10位纯数字如1012345678APP Key32位字母数字组合如a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6授权回调域即上一步填写的域名如bbs.example.com注意授权回调域必须与Discuz!后台的“站点URL”$_G[siteurl]域名完全一致包括www前缀。若Discuz!配置为https://www.bbs.example.com则此处必须填www.bbs.example.com否则回调时QQ会拒绝请求。3.2 Discuz!数据库表创建与权限校验插件安装脚本install.php会尝试创建pre_common_member_qq表但部分主机环境因MySQL权限限制会失败。此时需手动执行SQL-- 手动创建qq登录用户映射表请将pre_替换为你的Discuz!表前缀 CREATE TABLE IF NOT EXISTS pre_common_member_qq ( id int(10) unsigned NOT NULL AUTO_INCREMENT, uid int(10) unsigned NOT NULL DEFAULT 0, bindkey varchar(64) NOT NULL DEFAULT , openid varchar(64) NOT NULL DEFAULT , unionid varchar(64) NOT NULL DEFAULT , access_token text NOT NULL, expires_in int(10) unsigned NOT NULL DEFAULT 0, refresh_token text NOT NULL, refresh_expires int(10) unsigned NOT NULL DEFAULT 0, lastlogin int(10) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY bindkey (bindkey), KEY uid (uid), KEY openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;验证方式登录phpMyAdmin查看pre_common_member_qq表是否存在且bindkey字段有UNIQUE索引。若无索引立即执行ALTER TABLE pre_common_member_qq ADD UNIQUE(bindkey);3.3 插件上传与Discuz!后台配置将改进版2.4的qqlogin文件夹完整上传至Discuz!根目录的source/plugin/路径下即source/plugin/qqlogin/。登录Discuz!后台/admin.php进入“界面” → “插件管理” → “已安装插件”。找到“北岸QQ登录改进版2.4”点击右侧“设置”按钮。填写以下三项必须与QQ开放平台完全一致APP ID粘贴QQ开放平台获取的10位数字APP Key粘贴32位APP Key回调地址格式为https://你的域名/plugin.php?idqqlogin:callback注意必须是HTTPS且域名与QQ开放平台“授权回调域”一致点击“提交”页面应显示“设置更新成功”。返回插件列表点击“启用”按钮。若启用失败检查data/log/下的errorlog文件搜索qqlogin关键词。提示“回调地址”是整个流程的命门。Discuz! X3.4默认强制HTTPS若你的站点未配置SSL证书需先在后台“全局” → “域名设置”中将“站点URL”改为http://开头并确保QQ开放平台“授权回调域”也对应http协议。4. 避坑指南六个血泪经验总结的高频翻车点与解决方案部署过程中90%的失败并非代码缺陷而是环境配置与平台策略的错位。以下是某开发者在三套不同主机环境宝塔Linux、虚拟主机、Docker容器中踩出的真实坑每一条都附带复现方式与根治方案。4.1 现象点击QQ登录按钮后弹出空白页面控制台报Failed to load resource: the server responded with a status of 404 ()原因Discuz!伪静态规则未覆盖plugin.php路由。X3.4默认伪静态将plugin.php?idxxx重写为/plugin-xxx.html但改进版2.4的扫码接口plugin.php?idqqlogin:ajaxactqr未被规则捕获导致Nginx/Apache返回404。解决Apache用户编辑.htaccess在RewriteRule规则末尾添加RewriteCond %{QUERY_STRING} ^idqqlogin:ajaxactqr$ [NC] RewriteRule ^plugin\.php$ plugin.php?%{QUERY_STRING} [L]Nginx用户在站点配置的location ~ \.php$块内添加if ($args ~* ^idqqlogin:ajaxactqr$) { rewrite ^/plugin\.php$ /plugin.php?$args last; }通用方案临时关闭伪静态后台“全局” → “SEO设置” → 全部设为“否”确认功能正常后再针对性修复规则。4.2 现象扫码成功后跳转至callback.php页面显示“错误invalid_request”QQ开放平台日志提示redirect_uri不匹配原因Discuz!后台设置的“回调地址”与QQ开放平台“授权回调域”域名不一致或协议HTTP/HTTPS不匹配。常见于Discuz!后台填了https://bbs.example.com/plugin.php?idqqlogin:callback但QQ开放平台只填了bbs.example.com缺协议或Discuz!实际通过http://访问却被强制跳转https://。解决登录QQ开放平台核对“网站应用” → “开发设置” → “授权回调域”是否与Discuz!前台访问域名字符级完全相同包括www、协议、端口。在Discuz!后台“全局” → “域名设置”确认“站点URL”与“Cookie域名”均与上述域名一致。若使用CDN或反向代理检查$_SERVER[HTTP_X_FORWARDED_PROTO]是否被正确传递必要时在config/config_global.php中强制设置$_G[scheme] https;4.3 现象用户登录后Discuz!显示“欢迎回来”但个人中心头像/昵称仍为默认值数据库common_member表中avatar字段为空原因QQ OpenAPIget_user_info接口返回的头像URL为HTTPS而Discuz! X3.4默认avatar字段只接受相对路径或HTTP协议URL遇到HTTPS头像会静默丢弃。解决修改source/plugin/qqlogin/qq_login.class.php中头像处理逻辑在create_new_user()或update_user_profile()方法内将头像URL的https://替换为http://$avatar_url str_replace(https://, http://, $user_info[figureurl_qq_2]);或更彻底在Discuz!后台“站长” → “数据库” → “升级”执行SQL修复历史数据UPDATE pre_common_member SET avatar REPLACE(avatar, https://, http://) WHERE avatar LIKE https://%;4.4 现象同一用户在不同设备扫码登录Discuz!创建了多个账号如“用户1”、“用户2”common_member_qq表中unionid字段全为空原因QQ开放平台未开启“UnionID机制”或应用类型非“网站应用”如误选“移动应用”。unionid仅在网站应用且开启该机制后返回。解决登录QQ开放平台进入“管理中心” → 选择对应应用 → “基本资料” → 确认“应用类型”为“网站应用”。在“基本资料”页务必勾选“启用UnionID机制”并保存。若已勾选仍无效检查source/plugin/qqlogin/include/qqapi.class.php中get_user_info()方法确认请求URL为https://graph.qq.com/user/get_user_info非https://openapi.qzone.qq.com后者不返回unionid。4.5 现象登录成功后Discuz!后台“用户” → “管理用户”中新用户“注册IP”显示为127.0.0.1或::1原因Discuz!插件调用get_user_info()时QQ服务器将请求头中的X-Forwarded-For视为真实IP但多数主机未透传该头导致Discuz!取$_SERVER[REMOTE_ADDR]得到本地回环地址。解决在source/plugin/qqlogin/qq_login.class.php的用户创建逻辑中手动注入真实IP$real_ip $_SERVER[HTTP_X_FORWARDED_FOR] ?? $_SERVER[REMOTE_ADDR]; $real_ip filter_var($real_ip, FILTER_VALIDATE_IP) ? $real_ip : 0.0.0.0; // 将$real_ip传入C::t(common_member)-insert()的ip字段若主机支持可在Web服务器配置中强制添加头Nginxproxy_set_header X-Forwarded-For $remote_addr;ApacheRequestHeader set X-Forwarded-For %{REMOTE_ADDR}e5. 进阶验证与灰度发布用三组测试用例覆盖95%的线上异常场景上线前不做验证等于把炸弹埋进生产环境。我一般会用以下三组测试用例覆盖Token失效、网络抖动、用户信息变更等高发故障每组测试均在独立浏览器无痕窗口执行确保状态隔离。5.1 测试用例设计聚焦Token生命周期与用户状态一致性测试目标操作步骤预期结果验证方式Token自动续期1. 用手机QQ扫码登录成功2. 等待access_token过期默认3600秒3. 不退出Discuz!直接刷新个人中心页面头像/昵称正常显示无重新扫码提示查看pre_common_member_qq表expires_in时间戳应更新为当前时间3600refresh_expires不变UnionID去重1. 用A手机QQ扫码登录记下Discuz! UID如10012. 用B手机QQ同一QQ号扫码登录跳转至UID 1001的个人中心不创建新账号检查pre_common_member_qq表两条记录的unionid相同uid均为1001异常网络恢复1. 登录成功后手动删除pre_common_member_qq表中该用户的access_token字段值2. 刷新Discuz!首页自动调用refresh_token续期页面正常加载查看数据库access_token字段被重新填充expires_in更新5.2 灰度发布 checklist从1%流量到全量的五道关卡真实项目中我绝不会一次性全量启用。而是按以下节奏灰度每步观察24小时监控指标第一关1%流量在Discuz!后台“全局” → “性能优化” → “页面缓存”中关闭所有缓存仅对plugin.php?idqqlogin:callback路径开启Debug日志在qq_login.class.php中临时添加file_put_contents(/tmp/qqdebug.log, print_r($_REQUEST, true), FILE_APPEND);。第二关5%流量检查data/log/errorlog搜索qqlogin关键词确认无Invalid state、Token expired等错误。第三关20%流量登录phpMyAdmin执行SQL统计SELECT COUNT(*) as total, COUNT(DISTINCT unionid) as unique_unionid FROM pre_common_member_qq WHERE lastlogin UNIX_TIMESTAMP(NOW() - INTERVAL 1 DAY);若total远大于unique_unionid说明UnionID未生效立即回滚。第四关50%流量模拟弱网环境Chrome DevTools → Network → Throttling → “Slow 3G”测试扫码后30秒内是否出现超时白屏。若发生需优化qqapi.class.php中的http_post()超时参数默认5秒可增至10秒。第五关100%流量开启Discuz!后台“站长” → “系统信息” → “实时在线用户”观察QQ登录用户占比是否与预期流量匹配同时监控MySQL慢查询日志确认pre_common_member_qq表无全表扫描。从那以后我每次上线新版本QQ插件都强制走一遍这五道关卡——哪怕只是改了一个标点符号。因为Discuz!的用户会话状态太脆弱一个state参数没校验就可能让整个社区的登录入口变成DDoS放大器。希望帮到你。本文还有配套的精品资源点击获取