ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Discuz! QQ登录插件OAuth2.0授权回调与账号绑定实战

Discuz! QQ登录插件OAuth2.0授权回调与账号绑定实战 简介面向Discuz!论坛站长与社区运营者的QQ快捷登录插件改进版2.4由北岸团队对原版登录流程进行优化主要解决论坛注册步骤繁琐、QQ用户转化率偏低以及登录入口兼容性不足等问题。压缩包共61个文件整体约1MB内含24个php文件承载授权回调、账号绑定、安装卸载与移动端登录逻辑12个jpg和11个png用于界面与按钮素材8个htm模板配合4个xml插件配置另有gif、html等辅助文件覆盖PC与移动端接入场景。已有430人学习下载在Discuz!插件使用者中具备一定参考度。资料提供多编码插件配置、QQ回调与会员绑定处理、移动端登录类及帮助页模板便于管理员按QQ互联要求填入IDKEY后完成对接也方便开发者排查授权回调、账号绑定和页面展示问题减少从零改造登录模块的时间适合需要快速上线QQ登录、减少重复开发的站长与二次开发者参考。1. 按钮点下去白屏问题多半不在插件本身上周帮人看一个 DZ 论坛前台 QQ 登录按钮点下去跳授权页、点同意、回来一片白。站长说换过三套插件都一样最后发现是回调地址末尾多了一个斜杠。这套 DZ QQ 登录插件改进版 2.4就是在反复踩这类坑之后攒出来的东西授权、回调、绑定、解绑四条链路收在一个插件目录里后台填好 appid 和回调地址就能跑适合还在用 Discuz! X3.4/X3.5 搭垂直社区、又不想让新用户卡在邮箱验证这一步的站长和二次开发者。下面按“链路怎么走—文件怎么放—参数怎么填—数据怎么写—坑在哪”拆开讲能抄的部分直接抄抄完把域名换成自己的就行。2. OAuth2.0 授权码流程与插件目录先把链路和文件看明白2.1 授权码模式在这套插件里是怎么跑通的QQ 互联走的是标准授权码模式authorization code四步握手跳授权页拿code、用code换access_token、拿access_token换openid、拿openid和 token 拉用户资料。这四步里只有第一步是用户在浏览器里完成的后三步全部在服务端用 curl 走所以插件真正容易出错的地方全在服务端而不是前端按钮。第一步的 URL 由插件主类拼出来逻辑大致是这样?php // source/plugin/qqlogin/qqlogin.class.php 片段生成授权地址 $state substr(md5($_G[uid] . TIMESTAMP . random(8)), 0, 16); // state 同时落 cookie 和插件表回调时两边都要对得上 dsetcookie(qqlogin_state, $state, 600); C::t(#qqlogin#qqlogin_state)-insert(array( state $state, uid (int)$_G[uid], dateline TIMESTAMP, ), false, true); $params array( response_type code, client_id $this-appid, // 开放平台分配的应用 ID纯数字 redirect_uri $this-callback, // 必须与开放平台后台登记的一字不差 state $state, // 防 CSRF也用来区分是哪个入口发起的 scope get_user_info, ); $authorize_url https://graph.qq.com/oauth2.0/authorize? . http_build_query($params);state这一段是整个流程里最容易被偷懒省掉的部分。省掉之后功能也能跑但你会丢掉两样东西一是 CSRF 防护二是“这次登录是从 PC 端还是手机端发起的”这个上下文。改进版 2.4 把 state 存成了独立表pre_plugin_qqlogin_state而不是塞进$_SESSION原因是 DZ 的 session 在跨域回调回来时经常已经换了 session id尤其是站点开了 CDN 又没做 cookie 透传的时候读 session 读到空的概率相当高。第二步换 token 的请求在callback.inc.php里参数顺序不重要但redirect_uri必须和第一步完全一致连 URL 编码方式都要一致。第三步取 openid 返回的是一段callback( {client_id:xxx,openid:yyy} );的包裹文本插件里统一加了fmtjson省掉正则解析这个细节后面第 3 章会用 curl 单独验证。2.2 解包看目录哪些文件在干活把压缩包解开核心就是一个source/plugin/qqlogin/目录外面还有一段后台配置用的 XML。文件清单和职责对应关系如下文件/目录作用二次开发改动频率discuz_plugin_qqlogin.xml安装描述文件定义插件变量、模块、钩子挂载点低除非要加新配置项qqlogin.class.php主类继承plugin_qqlogin管钩子和入口分发高callback.inc.php回调入口处理 code、换 token、写绑定高bind.inc.php首次登录的绑定页逻辑处理用户名冲突中unbind.inc.php解绑清common_member_connect对应记录低table/table_qqlogin_state.phpstate 表的 DB 层封装低template/按钮模板、绑定页模板、移动端模板中static/js/qqlogin.js弹窗与跳转控制移动端 touch 绑定也在这里中看清这个结构有个实际好处出问题的时候能立刻定位是“配置层”还是“代码层”。比如授权页能打开但回调 500问题一定在callback.inc.php之后按钮点了没反应问题在static/js或模板跟 OAuth 一点关系都没有。我一般排查前先做这个二分能省掉一半时间。discuz_plugin_qqlogin.xml里的var节点定义了后台可见的配置项常见的就四个appid、appkey、callback、allownewuser。其中allownewuser控制“没绑定过的 QQ 是否允许自动创建新账号”社区类站点一般设 1内部论坛建议设 0只允许已有账号绑定防止外人随便注册进来。2.3 安装与后台配置的落地步骤装之前先备份source/plugin/和数据库这一步别省。DZ 插件装坏了会导致后台应用列表白屏恢复起来比重装还麻烦。上传和权限给到位# 在论坛根目录执行假设包已解压到 /tmp/qqlogin cp -r /tmp/qqlogin/source/plugin/qqlogin ./source/plugin/ # 插件目录给 web 用户可读可执行不要 777 chown -R www:www ./source/plugin/qqlogin find ./source/plugin/qqlogin -type d -exec chmod 755 {} \; find ./source/plugin/qqlogin -type f -exec chmod 644 {} \; # 清一遍模板与缓存目录避免旧缓存打架 rm -rf ./data/template/* ./data/cache/*命令本身没什么玄学重点是最后那两行。DZ 的模板缓存是按插件标识缓存的第二次装同标识插件时如果不清理前台很可能还是旧按钮的 HTML你会误以为是插件没生效。chown也别图省事用 777插件目录里有 PHP 文件777 等于给人留了写 shell 的口子。上传完成后进后台「应用 → 插件 → 未安装插件」找到 QQ 登录点安装。安装过程会建两张表pre_plugin_qqlogin_state和绑定关系表。装完在插件设置里填四个参数参数填什么常见填错appid开放平台应用详情页的 APP ID纯数字误填成 APP Keyappkey应用密钥字母数字混合不要带空格复制时带进了换行callback完整回调地址含协议和域名少了https://或末尾多了/allownewuser1 允许自动注册0 仅允许绑定与站点注册策略冲突填完必须点一次「更新缓存」DZ 的插件变量是缓存在pre_common_cache里的不更新缓存你改十遍后台前台读到的还是旧值。这个坑我在第 5 章还会再提一次因为它出现频率实在太高。最后一步是跑到前台实际点一次。别用管理员账号测管理员登录会被 DZ 的connect逻辑特殊处理测出来的结果和普通用户不一致。开一个浏览器无痕窗口用没注册过的普通身份走一遍完整流程能看到绑定页、能填用户名、能登录成功才算真装好了。3. 回调地址与 appid 配置三个参数决定成败3.1 redirect_uri 的三处一致性校验回调地址是这个插件里最容易翻车的参数没有之一。它同时存在于三个地方开放平台后台登记的授权回调域、DZ 插件后台配置的callback、以及callback.inc.php里拼给 token 接口的那个值。这三处必须完全一致任何一处不同都会在换 token 那一步拿到error100010之类的返回。“一致”的判定比多数人想的严格。http和https算两个地址带www和不带www算两个末尾有没有斜杠算两个带不带非标准端口比如:8080算两个。测试环境从http://test.xxx.com换到https://www.xxx.com三处都得改漏一处就白屏。我的做法是把这个地址做成唯一来源插件后台填一次代码里全部读配置不硬编码?php // callback.inc.php 开头回调地址统一从插件变量取不做任何拼接 $this-callback $_G[cache][plugin][qqlogin][callback]; if (empty($this-callback)) { showmessage(qqlogin_callback_missing, , array(), array(handle false)); } // 换 token 时原样带上不 urldecode、不重新拼参数 $token_url https://graph.qq.com/oauth2.0/token?grant_typeauthorization_code . client_id . $this-appid . client_secret . $this-appkey . code . urlencode($code) . redirect_uri . urlencode($this-callback) . fmtjson;注意urlencode只加在参数值上整个 URL 不能再编一次否则https://里的冒号和斜杠会被编成%3A%2F%2F服务端解出来是个坏地址。这个错误的表现很迷惑授权页能正常打开因为第一步浏览器会自动处理编码只有回调那一步失败很多人会误判成 appkey 错了。3.2 appid/appkey 与签名参数appid和appkey一个是对外的身份标识一个是对内的密钥性质完全不同。appid出现在前端跳转 URL 里泄露了无所谓appkey只在服务端换 token 时用绝不能出现在任何前端可见的地方。我见过有模板把appkey写进 JS 变量方便“调试”这等于把应用密钥公开了。state的校验逻辑也在这层。改进版 2.4 用“cookie 数据表”双份比对?php // 回调第一件事校验 state两个来源必须同时命中 $state isset($_GET[state]) ? trim($_GET[state]) : ; $cookie_state isset($_COOKIE[qqlogin_state]) ? $_COOKIE[qqlogin_state] : ; if (empty($state) || empty($cookie_state) || !hash_equals($cookie_state, $state)) { showmessage(qqlogin_state_invalid, , array(), array(handle false)); } // 命中后立刻删掉state 一次性使用 C::t(#qqlogin#qqlogin_state)-delete_by_state($state); dsetcookie(qqlogin_state, , -1);用hash_equals而不是是防止时序攻击虽然在这个场景下风险很低但成本几乎为零没理由不用。删除也必须在校验通过后立刻做否则同一个 state 可以被重放攻击者拿到一次回调 URL 就能反复触发绑定动作。access_token的缓存策略也值得说一句。token 默认有效期 90 天refresh_token30 天内可以续期。插件把 token 存在common_member_connect.conuintoken里每次登录前先查有没有可用 token没有才重新走授权。这样用户第二次点登录时是静默的不会反复弹授权页。但要注意用户在开放平台那边“取消授权”之后本地缓存的 token 会失效插件必须能识别error100016这类返回并清掉本地记录否则用户会卡在“点了没反应”的状态里出不来。3.3 用 curl 先跑通授权链路再回插件排查这类问题我习惯先绕开 DZ 和浏览器用 curl 把手动流程走一遍。这样能明确区分是“平台侧配置问题”还是“插件代码问题”。# 变量先准备好值从插件后台和开放平台后台抄过来 APPID你的appid APPKEY你的appkey REDIRECT_URIhttps://bbs.example.com/plugin.php?idqqlogin:callback CODE浏览器回调地址里 code 后面的那串 # 第 2 步code 换 token curl -s https://graph.qq.com/oauth2.0/token?grant_typeauthorization_codeclient_id${APPID}client_secret${APPKEY}code${CODE}redirect_uri${REDIRECT_URI}fmtjson # 第 3 步token 换 openid curl -s https://graph.qq.com/oauth2.0/me?access_token${ACCESS_TOKEN}fmtjson # 第 4 步拉用户资料看 nickname 能不能取到 curl -s https://graph.qq.com/user/get_user_info?access_token${ACCESS_TOKEN}oauth_consumer_key${APPID}openid${OPENID}第二步返回里如果带error基本可以断定是三处redirect_uri不一致把返回的error_description读一遍比在插件里瞎猜快得多。第三步返回openid说明 appkey 是对的。第四步返回的nickname里可能带表情符号DZ 的老表结构是utf8不是utf8mb4直接入库会把表情截断甚至报错插件里做了strip_emoji过滤这个在第 4 章会讲到。code是一次性的有效期很短通常几分钟。如果第二步报code expired回浏览器重新点一次登录拿新 code 就行别对着旧 code 反复试。curl 验证通过之后再回插件点一次如果插件还是失败而 curl 成功问题就一定在插件的代码路径上重点看callback.inc.php有没有对参数做了多余处理。4. 账号绑定与用户表写入从 openid 到 uid 的落地逻辑4.1 connect 表与 common_member 的对应关系OAuth 链路走通只是拿到身份真正把“这个 QQ 是谁”落到“这个人是论坛的哪个 uid”靠的是common_member_connect这张表。它和common_member是一对一关系uid做主键conopenid上必须有唯一索引CREATE TABLE IF NOT EXISTS pre_common_member_connect ( uid int(10) unsigned NOT NULL, conisbind tinyint(1) unsigned NOT NULL DEFAULT 0, conopenid char(32) NOT NULL DEFAULT , conuin char(40) NOT NULL DEFAULT , conuintoken char(32) NOT NULL DEFAULT , conqqnick varchar(100) NOT NULL DEFAULT , condeletetime int(10) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (uid), UNIQUE KEY conopenid (conopenid), KEY conisbind (conisbind) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;conopenid上的唯一索引不是可选项。缺了它并发情况下同一个 QQ 可能绑出两个 uid用户下次登录随机命中其中一个表现为“我的帖子怎么少了一半”。conuintoken存的是 access_token注意长度给够别用char(16)这种拍脑袋的长度。condeletetime是软删除标记用户解绑时不物理删除记录只置这个字段。这么设计的原因是防止“解绑后马上重绑”触发唯一索引冲突同时也留了一条审计痕迹。真要彻底清理由定时任务处理超过一定天数的记录。4.2 绑定、解绑与重名冲突的处理代码首次登录的判断顺序很关键必须“先查 openid再决定是登录还是注册”?php // callback.inc.php拿到 openid 之后的核心分发逻辑 $openid daddslashes($openid); $bind C::t(#qqlogin#qqlogin_bind)-fetch_by_openid($openid); if ($bind $bind[uid]) { // 已绑定直接写登录态更新 token C::t(#qqlogin#qqlogin_bind)-update_token($bind[uid], $access_token); setloginstatus(array(uid $bind[uid], username $bind[username]), 2592000); showmessage(qqlogin_succeed, dreferer(), array(), array(handle false)); } // 未绑定allownewuser 为 0 时直接拒绝 if (!$_G[cache][plugin][qqlogin][allownewuser]) { showmessage(qqlogin_not_bind, , array(), array(handle false)); } // 允许注册则跳到绑定页由用户确认用户名 include template(qqlogin:bind);setloginstatus的第三个参数是 cookie 有效期单位秒2592000是 30 天。这个值别设太长社区类站点一般 7 到 30 天比较合适。重名冲突发生在自动注册那一步。QQ 昵称里有大量重复直接拿昵称当用户名注册第二次就会撞common_member的用户名唯一索引。插件的做法是加后缀递增?php // bind.inc.php用户名去重最多试 20 次 $base preg_replace(/[^\x{4e00}-\x{9fa5}a-zA-Z0-9_]/u, , $nickname); $base $base ? qquser : mb_substr($base, 0, 12, UTF-8); $username $base; for ($i 1; $i 20; $i) { if (!C::t(common_member)-fetch_by_username($username)) { break; } $username $base . _ . $i; } if ($i 20) { showmessage(qqlogin_username_conflict, , array(), array(handle false)); }preg_replace那行把昵称里的表情和标点全部滤掉只留中文、字母、数字、下划线。不过滤的话utf8表直接报Incorrect string value。mb_substr限长 12 是照顾 DZ 默认用户名长度限制改过站点配置的按自己的来。4.3 老用户迁移把历史账号挂到 openid 上站点从别的方式切过来时会有一批“已经在论坛注册过、但没绑定过 QQ”的老用户。这批人第一次点 QQ 登录会走到注册分支结果多出一个新账号帖子还在老账号上用户会直接骂人。处理办法是在绑定页给一个“已有账号”入口让用户输入用户名密码验证一次验证通过后把 openid 挂到老 uid 上-- 迁移脚本把已知对应关系的用户批量补绑定记录 INSERT INTO pre_common_member_connect (uid, conisbind, conopenid, conuin, conuintoken, conqqnick, condeletetime) SELECT m.uid, 1, t.openid, , , t.nickname, 0 FROM pre_common_member m INNER JOIN pre_plugin_qqlogin_migrate t ON t.username m.username WHERE m.uid 0 ON DUPLICATE KEY UPDATE conisbind 1, conqqnick VALUES(conqqnick);ON DUPLICATE KEY UPDATE让脚本可以重复跑跑第二遍不会报主键冲突也不会把已有记录改坏。pre_plugin_qqlogin_migrate是临时对照表跑完就删。执行前先在从库或者备份上跑一遍SELECT版本确认行数和预期对得上血泪经验是这类脚本一定要先看结果再落库。5. 避坑与排查五条踩坑记录现象一回调后整页白屏日志里没有任何 DZ 报错。原因通常是 PHP 致命错误被display_errors关掉了或者插件文件带 BOM 头导致headers already sent。解决临时在config/config_global.php里开$_config[debug] 1去data/log/下看当天的xxx_log.phpBOM 用grep -rl $\xEF\xBB\xBF source/plugin/qqlogin/一次就能找出来批量转成无 BOM 的 UTF-8。现象二授权页能打开点同意后回到站点提示参数错误。原因是三处redirect_uri不一致或者callback参数被二次 URL 编码。解决按第 3 章的办法先用 curl 单独验证看返回的error_description。改完配置记得更新缓存插件变量读的是缓存不是数据库。现象三用户每次点登录都变成新注册账号越积越多。原因是common_member_connect缺conopenid唯一索引或者判断逻辑写反了先查 uid 再查 openid。解决先SHOW INDEX FROM pre_common_member_connect确认唯一索引在再检查callback.inc.php的分支顺序必须是 openid 优先。已经产生的重复账号要按 openid 分组人工合并别写脚本自动删。现象四PC 端正常手机端点了完全没反应。原因多半是模板里按钮绑的是click事件而移动端浏览器在某些情况下会吞掉这个事件另一种是本站已全站 HTTPS但插件模板里硬编码了http://的跳转地址被浏览器按混合内容拦掉。解决事件改成touchstart和click都绑检查模板里所有 URL 是否走$_G[siteurl]。用手机浏览器远程调试看一眼 Console有没有 blocked 提示一目了然。现象五换了域名或上了 HTTPS 之后所有老用户全部掉登录。原因是 cookie 域、后台站点 URL、开放平台回调三处没同步导致 state 校验失败或者 token 换不出来。解决换域名时把这三处列成清单逐项改改完让用户重新授权一次。这一步没有后悔药唯一的办法是换域名前就做好清单。6. 移动端与模板兼容的进阶调优一次回归验证清单改完模板和绑定逻辑之后别急着放开给用户用。我一般跑一遍固定清单把六种组合都点一遍任一项不对就不上线场景预期结果重点检查PC 未登录点按钮跳授权页回来进绑定页state cookie 是否写入成功PC 已绑定用户点按钮静默登录不弹授权页conuintoken是否被复用手机浏览器点按钮跳转或唤起授权回来正常按钮事件绑定、siteurl协议微信内置浏览器打开至少给出明确提示不白屏UA 判断分支是否存在已绑定用户解绑再绑能正常解绑重绑不报冲突软删除字段是否生效后台关闭自动注册新用户看到提示而非报错allownewuser分支模板这块改动最小、收益最大的一个点是按钮的降级。移动端很多时候唤起授权会失败如果没有兜底用户看到的就是一个死按钮!-- template/qqlogin/button.htm移动端同时绑 touchstart 和 click防止事件被吞 -- a href{$qqlogin_url} classqqlogin-btn idqqloginBtn relnofollow span classqqlogin-icon/spanQQ 登录 /a script typetext/javascript var btn document.getElementById(qqloginBtn); var jumping false; function go(e) { if (jumping) { return; } jumping true; if (e e.preventDefault) { e.preventDefault(); } window.location.href btn.getAttribute(href); } btn.addEventListener(touchstart, go, {passive: false}); btn.addEventListener(click, go, false); /scriptjumping这个标志位防的是移动端同时触发touchstart和click导致连跳两次第二次带上来的 state 已经失效用户看到的就是一次莫名其妙的白屏。{passive: false}是为了在touchstart里能调preventDefault不加的话部分浏览器会忽略这个调用。还有一个容易被忽略的细节relnofollow。登录入口被搜索引擎爬到会产生大量无效的授权页索引加上这个属性省事。最后说个习惯。这套插件我从第一版跟到现在每次升级前都会做同一件事把当前source/plugin/qqlogin/整个目录打包留一份命名为qqlogin_日期_版本.rar放到独立目录然后才动手覆盖。因为插件升级失败时后台应用列表会直接白屏进不去后台就没法回滚只能靠文件覆盖回去。这个习惯让我至少两次在十分钟内恢复了一个正在做活动的论坛。从那以后我每次动插件目录都强制走一遍“打包—记录改动点—再覆盖”的流程希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表