简介:这是一款专为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_uri |
source/plugin/qqlogin/include/qqapi.class.php | API封装 | 封装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?id=qqlogin:ajax&act=qr生成动态二维码 |
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_type=refresh_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 ID(10位纯数字,如1012345678)APP Key(32位字母数字组合,如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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;验证方式:登录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?id=qqlogin: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?id=xxx重写为/plugin-xxx.html,但改进版2.4的扫码接口plugin.php?id=qqlogin:ajax&act=qr未被规则捕获,导致Nginx/Apache返回404。
解决:
- Apache用户:编辑
.htaccess,在RewriteRule规则末尾添加:RewriteCond %{QUERY_STRING} ^id=qqlogin:ajax&act=qr$ [NC] RewriteRule ^plugin\.php$ plugin.php?%{QUERY_STRING} [L] - Nginx用户:在站点配置的
location ~ \.php$块内,添加:if ($args ~* "^id=qqlogin:ajax&act=qr$") { 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?id=qqlogin: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服务器配置中强制添加头:
- Nginx:
proxy_set_header X-Forwarded-For $remote_addr; - Apache:
RequestHeader set X-Forwarded-For "%{REMOTE_ADDR}e"
- Nginx:
5. 进阶验证与灰度发布:用三组测试用例覆盖95%的线上异常场景
上线前不做验证,等于把炸弹埋进生产环境。我一般会用以下三组测试用例,覆盖Token失效、网络抖动、用户信息变更等高发故障,每组测试均在独立浏览器无痕窗口执行,确保状态隔离。
5.1 测试用例设计:聚焦Token生命周期与用户状态一致性
| 测试目标 | 操作步骤 | 预期结果 | 验证方式 |
|---|---|---|---|
| Token自动续期 | 1. 用手机QQ扫码登录成功 2. 等待 access_token过期(默认3600秒)3. 不退出Discuz!,直接刷新个人中心页面 | 头像/昵称正常显示,无重新扫码提示 | 查看pre_common_member_qq表,expires_in时间戳应更新为当前时间+3600,refresh_expires不变 |
| UnionID去重 | 1. 用A手机QQ扫码登录,记下Discuz! UID(如1001) 2. 用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?id=qqlogin: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放大器。希望帮到你。
本文还有配套的精品资源,点击获取