回調(diào)與賬號(hào)綁定實(shí)戰(zhàn))
簡(jiǎn)介面向Discuz!論壇站長(zhǎng)與社區(qū)運(yùn)營(yíng)者的QQ快捷登錄插件改進(jìn)版2.4由北岸團(tuán)隊(duì)對(duì)原版登錄流程進(jìn)行優(yōu)化主要解決論壇注冊(cè)步驟繁瑣、QQ用戶轉(zhuǎn)化率偏低以及登錄入口兼容性不足等問(wèn)題。壓縮包共61個(gè)文件整體約1MB內(nèi)含24個(gè)php文件承載授權(quán)回調(diào)、賬號(hào)綁定、安裝卸載與移動(dòng)端登錄邏輯12個(gè)jpg和11個(gè)png用于界面與按鈕素材8個(gè)htm模板配合4個(gè)xml插件配置另有g(shù)if、html等輔助文件覆蓋PC與移動(dòng)端接入場(chǎng)景。已有430人學(xué)習(xí)下載在Discuz!插件使用者中具備一定參考度。資料提供多編碼插件配置、QQ回調(diào)與會(huì)員綁定處理、移動(dòng)端登錄類及幫助頁(yè)模板便于管理員按QQ互聯(lián)要求填入IDKEY后完成對(duì)接也方便開(kāi)發(fā)者排查授權(quán)回調(diào)、賬號(hào)綁定和頁(yè)面展示問(wèn)題減少?gòu)牧愀脑斓卿浤K的時(shí)間適合需要快速上線QQ登錄、減少重復(fù)開(kāi)發(fā)的站長(zhǎng)與二次開(kāi)發(fā)者參考。1. 按鈕點(diǎn)下去白屏問(wèn)題多半不在插件本身上周幫人看一個(gè) DZ 論壇前臺(tái) QQ 登錄按鈕點(diǎn)下去跳授權(quán)頁(yè)、點(diǎn)同意、回來(lái)一片白。站長(zhǎng)說(shuō)換過(guò)三套插件都一樣最后發(fā)現(xiàn)是回調(diào)地址末尾多了一個(gè)斜杠。這套 DZ QQ 登錄插件改進(jìn)版 2.4就是在反復(fù)踩這類坑之后攢出來(lái)的東西授權(quán)、回調(diào)、綁定、解綁四條鏈路收在一個(gè)插件目錄里后臺(tái)填好 appid 和回調(diào)地址就能跑適合還在用 Discuz! X3.4/X3.5 搭垂直社區(qū)、又不想讓新用戶卡在郵箱驗(yàn)證這一步的站長(zhǎng)和二次開(kāi)發(fā)者。下面按“鏈路怎么走—文件怎么放—參數(shù)怎么填—數(shù)據(jù)怎么寫(xiě)—坑在哪”拆開(kāi)講能抄的部分直接抄抄完把域名換成自己的就行。2. OAuth2.0 授權(quán)碼流程與插件目錄先把鏈路和文件看明白2.1 授權(quán)碼模式在這套插件里是怎么跑通的QQ 互聯(lián)走的是標(biāo)準(zhǔn)授權(quán)碼模式authorization code四步握手跳授權(quán)頁(yè)拿code、用code換access_token、拿access_token換openid、拿openid和 token 拉用戶資料。這四步里只有第一步是用戶在瀏覽器里完成的后三步全部在服務(wù)端用 curl 走所以插件真正容易出錯(cuò)的地方全在服務(wù)端而不是前端按鈕。第一步的 URL 由插件主類拼出來(lái)邏輯大致是這樣?php // source/plugin/qqlogin/qqlogin.class.php 片段生成授權(quán)地址 $state substr(md5($_G[uid] . TIMESTAMP . random(8)), 0, 16); // state 同時(shí)落 cookie 和插件表回調(diào)時(shí)兩邊都要對(duì)得上 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, // 開(kāi)放平臺(tái)分配的應(yīng)用 ID純數(shù)字 redirect_uri $this-callback, // 必須與開(kāi)放平臺(tái)后臺(tái)登記的一字不差 state $state, // 防 CSRF也用來(lái)區(qū)分是哪個(gè)入口發(fā)起的 scope get_user_info, ); $authorize_url https://graph.qq.com/oauth2.0/authorize? . http_build_query($params);state這一段是整個(gè)流程里最容易被偷懶省掉的部分。省掉之后功能也能跑但你會(huì)丟掉兩樣?xùn)|西一是 CSRF 防護(hù)二是“這次登錄是從 PC 端還是手機(jī)端發(fā)起的”這個(gè)上下文。改進(jìn)版 2.4 把 state 存成了獨(dú)立表pre_plugin_qqlogin_state而不是塞進(jìn)$_SESSION原因是 DZ 的 session 在跨域回調(diào)回來(lái)時(shí)經(jīng)常已經(jīng)換了 session id尤其是站點(diǎn)開(kāi)了 CDN 又沒(méi)做 cookie 透?jìng)鞯臅r(shí)候讀 session 讀到空的概率相當(dāng)高。第二步換 token 的請(qǐng)求在callback.inc.php里參數(shù)順序不重要但redirect_uri必須和第一步完全一致連 URL 編碼方式都要一致。第三步取 openid 返回的是一段callback( {client_id:xxx,openid:yyy} );的包裹文本插件里統(tǒng)一加了fmtjson省掉正則解析這個(gè)細(xì)節(jié)后面第 3 章會(huì)用 curl 單獨(dú)驗(yàn)證。2.2 解包看目錄哪些文件在干活把壓縮包解開(kāi)核心就是一個(gè)source/plugin/qqlogin/目錄外面還有一段后臺(tái)配置用的 XML。文件清單和職責(zé)對(duì)應(yīng)關(guān)系如下文件/目錄作用二次開(kāi)發(fā)改動(dòng)頻率discuz_plugin_qqlogin.xml安裝描述文件定義插件變量、模塊、鉤子掛載點(diǎn)低除非要加新配置項(xiàng)qqlogin.class.php主類繼承plugin_qqlogin管鉤子和入口分發(fā)高callback.inc.php回調(diào)入口處理 code、換 token、寫(xiě)綁定高bind.inc.php首次登錄的綁定頁(yè)邏輯處理用戶名沖突中unbind.inc.php解綁清common_member_connect對(duì)應(yīng)記錄低table/table_qqlogin_state.phpstate 表的 DB 層封裝低template/按鈕模板、綁定頁(yè)模板、移動(dòng)端模板中static/js/qqlogin.js彈窗與跳轉(zhuǎn)控制移動(dòng)端 touch 綁定也在這里中看清這個(gè)結(jié)構(gòu)有個(gè)實(shí)際好處出問(wèn)題的時(shí)候能立刻定位是“配置層”還是“代碼層”。比如授權(quán)頁(yè)能打開(kāi)但回調(diào) 500問(wèn)題一定在callback.inc.php之后按鈕點(diǎn)了沒(méi)反應(yīng)問(wèn)題在static/js或模板跟 OAuth 一點(diǎn)關(guān)系都沒(méi)有。我一般排查前先做這個(gè)二分能省掉一半時(shí)間。discuz_plugin_qqlogin.xml里的var節(jié)點(diǎn)定義了后臺(tái)可見(jiàn)的配置項(xiàng)常見(jiàn)的就四個(gè)appid、appkey、callback、allownewuser。其中allownewuser控制“沒(méi)綁定過(guò)的 QQ 是否允許自動(dòng)創(chuàng)建新賬號(hào)”社區(qū)類站點(diǎn)一般設(shè) 1內(nèi)部論壇建議設(shè) 0只允許已有賬號(hào)綁定防止外人隨便注冊(cè)進(jìn)來(lái)。2.3 安裝與后臺(tái)配置的落地步驟裝之前先備份source/plugin/和數(shù)據(jù)庫(kù)這一步別省。DZ 插件裝壞了會(huì)導(dǎo)致后臺(tái)應(yīng)用列表白屏恢復(fù)起來(lái)比重裝還麻煩。上傳和權(quán)限給到位# 在論壇根目錄執(zhí)行假設(shè)包已解壓到 /tmp/qqlogin cp -r /tmp/qqlogin/source/plugin/qqlogin ./source/plugin/ # 插件目錄給 web 用戶可讀可執(zhí)行不要 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/*命令本身沒(méi)什么玄學(xué)重點(diǎn)是最后那兩行。DZ 的模板緩存是按插件標(biāo)識(shí)緩存的第二次裝同標(biāo)識(shí)插件時(shí)如果不清理前臺(tái)很可能還是舊按鈕的 HTML你會(huì)誤以為是插件沒(méi)生效。chown也別圖省事用 777插件目錄里有 PHP 文件777 等于給人留了寫(xiě) shell 的口子。上傳完成后進(jìn)后臺(tái)「應(yīng)用 → 插件 → 未安裝插件」找到 QQ 登錄點(diǎn)安裝。安裝過(guò)程會(huì)建兩張表pre_plugin_qqlogin_state和綁定關(guān)系表。裝完在插件設(shè)置里填四個(gè)參數(shù)參數(shù)填什么常見(jiàn)填錯(cuò)appid開(kāi)放平臺(tái)應(yīng)用詳情頁(yè)的 APP ID純數(shù)字誤填成 APP Keyappkey應(yīng)用密鑰字母數(shù)字混合不要帶空格復(fù)制時(shí)帶進(jìn)了換行callback完整回調(diào)地址含協(xié)議和域名少了https://或末尾多了/allownewuser1 允許自動(dòng)注冊(cè)0 僅允許綁定與站點(diǎn)注冊(cè)策略沖突填完必須點(diǎn)一次「更新緩存」DZ 的插件變量是緩存在pre_common_cache里的不更新緩存你改十遍后臺(tái)前臺(tái)讀到的還是舊值。這個(gè)坑我在第 5 章還會(huì)再提一次因?yàn)樗霈F(xiàn)頻率實(shí)在太高。最后一步是跑到前臺(tái)實(shí)際點(diǎn)一次。別用管理員賬號(hào)測(cè)管理員登錄會(huì)被 DZ 的connect邏輯特殊處理測(cè)出來(lái)的結(jié)果和普通用戶不一致。開(kāi)一個(gè)瀏覽器無(wú)痕窗口用沒(méi)注冊(cè)過(guò)的普通身份走一遍完整流程能看到綁定頁(yè)、能填用戶名、能登錄成功才算真裝好了。3. 回調(diào)地址與 appid 配置三個(gè)參數(shù)決定成敗3.1 redirect_uri 的三處一致性校驗(yàn)回調(diào)地址是這個(gè)插件里最容易翻車的參數(shù)沒(méi)有之一。它同時(shí)存在于三個(gè)地方開(kāi)放平臺(tái)后臺(tái)登記的授權(quán)回調(diào)域、DZ 插件后臺(tái)配置的callback、以及callback.inc.php里拼給 token 接口的那個(gè)值。這三處必須完全一致任何一處不同都會(huì)在換 token 那一步拿到error100010之類的返回?!耙恢隆钡呐卸ū榷鄶?shù)人想的嚴(yán)格。http和https算兩個(gè)地址帶www和不帶www算兩個(gè)末尾有沒(méi)有斜杠算兩個(gè)帶不帶非標(biāo)準(zhǔn)端口比如:8080算兩個(gè)。測(cè)試環(huán)境從http://test.xxx.com換到https://www.xxx.com三處都得改漏一處就白屏。我的做法是把這個(gè)地址做成唯一來(lái)源插件后臺(tái)填一次代碼里全部讀配置不硬編碼?php // callback.inc.php 開(kāi)頭回調(diào)地址統(tǒng)一從插件變量取不做任何拼接 $this-callback $_G[cache][plugin][qqlogin][callback]; if (empty($this-callback)) { showmessage(qqlogin_callback_missing, , array(), array(handle false)); } // 換 token 時(shí)原樣帶上不 urldecode、不重新拼參數(shù) $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只加在參數(shù)值上整個(gè) URL 不能再編一次否則https://里的冒號(hào)和斜杠會(huì)被編成%3A%2F%2F服務(wù)端解出來(lái)是個(gè)壞地址。這個(gè)錯(cuò)誤的表現(xiàn)很迷惑授權(quán)頁(yè)能正常打開(kāi)因?yàn)榈谝徊綖g覽器會(huì)自動(dòng)處理編碼只有回調(diào)那一步失敗很多人會(huì)誤判成 appkey 錯(cuò)了。3.2 appid/appkey 與簽名參數(shù)appid和appkey一個(gè)是對(duì)外的身份標(biāo)識(shí)一個(gè)是對(duì)內(nèi)的密鑰性質(zhì)完全不同。appid出現(xiàn)在前端跳轉(zhuǎn) URL 里泄露了無(wú)所謂appkey只在服務(wù)端換 token 時(shí)用絕不能出現(xiàn)在任何前端可見(jiàn)的地方。我見(jiàn)過(guò)有模板把a(bǔ)ppkey寫(xiě)進(jìn) JS 變量方便“調(diào)試”這等于把應(yīng)用密鑰公開(kāi)了。state的校驗(yàn)邏輯也在這層。改進(jìn)版 2.4 用“cookie 數(shù)據(jù)表”雙份比對(duì)?php // 回調(diào)第一件事校驗(yàn) state兩個(gè)來(lái)源必須同時(shí)命中 $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而不是是防止時(shí)序攻擊雖然在這個(gè)場(chǎng)景下風(fēng)險(xiǎn)很低但成本幾乎為零沒(méi)理由不用。刪除也必須在校驗(yàn)通過(guò)后立刻做否則同一個(gè) state 可以被重放攻擊者拿到一次回調(diào) URL 就能反復(fù)觸發(fā)綁定動(dòng)作。access_token的緩存策略也值得說(shuō)一句。token 默認(rèn)有效期 90 天refresh_token30 天內(nèi)可以續(xù)期。插件把 token 存在common_member_connect.conuintoken里每次登錄前先查有沒(méi)有可用 token沒(méi)有才重新走授權(quán)。這樣用戶第二次點(diǎn)登錄時(shí)是靜默的不會(huì)反復(fù)彈授權(quán)頁(yè)。但要注意用戶在開(kāi)放平臺(tái)那邊“取消授權(quán)”之后本地緩存的 token 會(huì)失效插件必須能識(shí)別error100016這類返回并清掉本地記錄否則用戶會(huì)卡在“點(diǎn)了沒(méi)反應(yīng)”的狀態(tài)里出不來(lái)。3.3 用 curl 先跑通授權(quán)鏈路再回插件排查這類問(wèn)題我習(xí)慣先繞開(kāi) DZ 和瀏覽器用 curl 把手動(dòng)流程走一遍。這樣能明確區(qū)分是“平臺(tái)側(cè)配置問(wèn)題”還是“插件代碼問(wèn)題”。# 變量先準(zhǔn)備好值從插件后臺(tái)和開(kāi)放平臺(tái)后臺(tái)抄過(guò)來(lái) APPID你的appid APPKEY你的appkey REDIRECT_URIhttps://bbs.example.com/plugin.php?idqqlogin:callback CODE瀏覽器回調(diào)地址里 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說(shuō)明 appkey 是對(duì)的。第四步返回的nickname里可能帶表情符號(hào)DZ 的老表結(jié)構(gòu)是utf8不是utf8mb4直接入庫(kù)會(huì)把表情截?cái)嗌踔翀?bào)錯(cuò)插件里做了strip_emoji過(guò)濾這個(gè)在第 4 章會(huì)講到。code是一次性的有效期很短通常幾分鐘。如果第二步報(bào)code expired回瀏覽器重新點(diǎn)一次登錄拿新 code 就行別對(duì)著舊 code 反復(fù)試。curl 驗(yàn)證通過(guò)之后再回插件點(diǎn)一次如果插件還是失敗而 curl 成功問(wèn)題就一定在插件的代碼路徑上重點(diǎn)看callback.inc.php有沒(méi)有對(duì)參數(shù)做了多余處理。4. 賬號(hào)綁定與用戶表寫(xiě)入從 openid 到 uid 的落地邏輯4.1 connect 表與 common_member 的對(duì)應(yīng)關(guān)系OAuth 鏈路走通只是拿到身份真正把“這個(gè) QQ 是誰(shuí)”落到“這個(gè)人是論壇的哪個(gè) uid”靠的是common_member_connect這張表。它和common_member是一對(duì)一關(guān)系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上的唯一索引不是可選項(xiàng)。缺了它并發(fā)情況下同一個(gè) QQ 可能綁出兩個(gè) uid用戶下次登錄隨機(jī)命中其中一個(gè)表現(xiàn)為“我的帖子怎么少了一半”。conuintoken存的是 access_token注意長(zhǎng)度給夠別用char(16)這種拍腦袋的長(zhǎng)度。condeletetime是軟刪除標(biāo)記用戶解綁時(shí)不物理刪除記錄只置這個(gè)字段。這么設(shè)計(jì)的原因是防止“解綁后馬上重綁”觸發(fā)唯一索引沖突同時(shí)也留了一條審計(jì)痕跡。真要徹底清理由定時(shí)任務(wù)處理超過(guò)一定天數(shù)的記錄。4.2 綁定、解綁與重名沖突的處理代碼首次登錄的判斷順序很關(guān)鍵必須“先查 openid再?zèng)Q定是登錄還是注冊(cè)”?php // callback.inc.php拿到 openid 之后的核心分發(fā)邏輯 $openid daddslashes($openid); $bind C::t(#qqlogin#qqlogin_bind)-fetch_by_openid($openid); if ($bind $bind[uid]) { // 已綁定直接寫(xiě)登錄態(tài)更新 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 時(shí)直接拒絕 if (!$_G[cache][plugin][qqlogin][allownewuser]) { showmessage(qqlogin_not_bind, , array(), array(handle false)); } // 允許注冊(cè)則跳到綁定頁(yè)由用戶確認(rèn)用戶名 include template(qqlogin:bind);setloginstatus的第三個(gè)參數(shù)是 cookie 有效期單位秒2592000是 30 天。這個(gè)值別設(shè)太長(zhǎng)社區(qū)類站點(diǎn)一般 7 到 30 天比較合適。重名沖突發(fā)生在自動(dòng)注冊(cè)那一步。QQ 昵稱里有大量重復(fù)直接拿昵稱當(dāng)用戶名注冊(cè)第二次就會(huì)撞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那行把昵稱里的表情和標(biāo)點(diǎn)全部濾掉只留中文、字母、數(shù)字、下劃線。不過(guò)濾的話utf8表直接報(bào)Incorrect string value。mb_substr限長(zhǎng) 12 是照顧 DZ 默認(rèn)用戶名長(zhǎng)度限制改過(guò)站點(diǎn)配置的按自己的來(lái)。4.3 老用戶遷移把歷史賬號(hào)掛到 openid 上站點(diǎn)從別的方式切過(guò)來(lái)時(shí)會(huì)有一批“已經(jīng)在論壇注冊(cè)過(guò)、但沒(méi)綁定過(guò) QQ”的老用戶。這批人第一次點(diǎn) QQ 登錄會(huì)走到注冊(cè)分支結(jié)果多出一個(gè)新賬號(hào)帖子還在老賬號(hào)上用戶會(huì)直接罵人。處理辦法是在綁定頁(yè)給一個(gè)“已有賬號(hào)”入口讓用戶輸入用戶名密碼驗(yàn)證一次驗(yàn)證通過(guò)后把 openid 掛到老 uid 上-- 遷移腳本把已知對(duì)應(yīng)關(guān)系的用戶批量補(bǔ)綁定記錄 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讓腳本可以重復(fù)跑跑第二遍不會(huì)報(bào)主鍵沖突也不會(huì)把已有記錄改壞。pre_plugin_qqlogin_migrate是臨時(shí)對(duì)照表跑完就刪。執(zhí)行前先在從庫(kù)或者備份上跑一遍SELECT版本確認(rèn)行數(shù)和預(yù)期對(duì)得上血淚經(jīng)驗(yàn)是這類腳本一定要先看結(jié)果再落庫(kù)。5. 避坑與排查五條踩坑記錄現(xiàn)象一回調(diào)后整頁(yè)白屏日志里沒(méi)有任何 DZ 報(bào)錯(cuò)。原因通常是 PHP 致命錯(cuò)誤被display_errors關(guān)掉了或者插件文件帶 BOM 頭導(dǎo)致headers already sent。解決臨時(shí)在config/config_global.php里開(kāi)$_config[debug] 1去data/log/下看當(dāng)天的xxx_log.phpBOM 用grep -rl $\xEF\xBB\xBF source/plugin/qqlogin/一次就能找出來(lái)批量轉(zhuǎn)成無(wú) BOM 的 UTF-8?,F(xiàn)象二授權(quán)頁(yè)能打開(kāi)點(diǎn)同意后回到站點(diǎn)提示參數(shù)錯(cuò)誤。原因是三處redirect_uri不一致或者callback參數(shù)被二次 URL 編碼。解決按第 3 章的辦法先用 curl 單獨(dú)驗(yàn)證看返回的error_description。改完配置記得更新緩存插件變量讀的是緩存不是數(shù)據(jù)庫(kù)?,F(xiàn)象三用戶每次點(diǎn)登錄都變成新注冊(cè)賬號(hào)越積越多。原因是common_member_connect缺conopenid唯一索引或者判斷邏輯寫(xiě)反了先查 uid 再查 openid。解決先SHOW INDEX FROM pre_common_member_connect確認(rèn)唯一索引在再檢查callback.inc.php的分支順序必須是 openid 優(yōu)先。已經(jīng)產(chǎn)生的重復(fù)賬號(hào)要按 openid 分組人工合并別寫(xiě)腳本自動(dòng)刪?,F(xiàn)象四PC 端正常手機(jī)端點(diǎn)了完全沒(méi)反應(yīng)。原因多半是模板里按鈕綁的是click事件而移動(dòng)端瀏覽器在某些情況下會(huì)吞掉這個(gè)事件另一種是本站已全站 HTTPS但插件模板里硬編碼了http://的跳轉(zhuǎn)地址被瀏覽器按混合內(nèi)容攔掉。解決事件改成touchstart和click都綁檢查模板里所有 URL 是否走$_G[siteurl]。用手機(jī)瀏覽器遠(yuǎn)程調(diào)試看一眼 Console有沒(méi)有 blocked 提示一目了然。現(xiàn)象五換了域名或上了 HTTPS 之后所有老用戶全部掉登錄。原因是 cookie 域、后臺(tái)站點(diǎn) URL、開(kāi)放平臺(tái)回調(diào)三處沒(méi)同步導(dǎo)致 state 校驗(yàn)失敗或者 token 換不出來(lái)。解決換域名時(shí)把這三處列成清單逐項(xiàng)改改完讓用戶重新授權(quán)一次。這一步?jīng)]有后悔藥唯一的辦法是換域名前就做好清單。6. 移動(dòng)端與模板兼容的進(jìn)階調(diào)優(yōu)一次回歸驗(yàn)證清單改完模板和綁定邏輯之后別急著放開(kāi)給用戶用。我一般跑一遍固定清單把六種組合都點(diǎn)一遍任一項(xiàng)不對(duì)就不上線場(chǎng)景預(yù)期結(jié)果重點(diǎn)檢查PC 未登錄點(diǎn)按鈕跳授權(quán)頁(yè)回來(lái)進(jìn)綁定頁(yè)state cookie 是否寫(xiě)入成功PC 已綁定用戶點(diǎn)按鈕靜默登錄不彈授權(quán)頁(yè)conuintoken是否被復(fù)用手機(jī)瀏覽器點(diǎn)按鈕跳轉(zhuǎn)或喚起授權(quán)回來(lái)正常按鈕事件綁定、siteurl協(xié)議微信內(nèi)置瀏覽器打開(kāi)至少給出明確提示不白屏UA 判斷分支是否存在已綁定用戶解綁再綁能正常解綁重綁不報(bào)沖突軟刪除字段是否生效后臺(tái)關(guān)閉自動(dòng)注冊(cè)新用戶看到提示而非報(bào)錯(cuò)allownewuser分支模板這塊改動(dòng)最小、收益最大的一個(gè)點(diǎn)是按鈕的降級(jí)。移動(dòng)端很多時(shí)候喚起授權(quán)會(huì)失敗如果沒(méi)有兜底用戶看到的就是一個(gè)死按鈕!-- template/qqlogin/button.htm移動(dòng)端同時(shí)綁 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這個(gè)標(biāo)志位防的是移動(dòng)端同時(shí)觸發(fā)touchstart和click導(dǎo)致連跳兩次第二次帶上來(lái)的 state 已經(jīng)失效用戶看到的就是一次莫名其妙的白屏。{passive: false}是為了在touchstart里能調(diào)preventDefault不加的話部分瀏覽器會(huì)忽略這個(gè)調(diào)用。還有一個(gè)容易被忽略的細(xì)節(jié)relnofollow。登錄入口被搜索引擎爬到會(huì)產(chǎn)生大量無(wú)效的授權(quán)頁(yè)索引加上這個(gè)屬性省事。最后說(shuō)個(gè)習(xí)慣。這套插件我從第一版跟到現(xiàn)在每次升級(jí)前都會(huì)做同一件事把當(dāng)前source/plugin/qqlogin/整個(gè)目錄打包留一份命名為qqlogin_日期_版本.rar放到獨(dú)立目錄然后才動(dòng)手覆蓋。因?yàn)椴寮?jí)失敗時(shí)后臺(tái)應(yīng)用列表會(huì)直接白屏進(jìn)不去后臺(tái)就沒(méi)法回滾只能靠文件覆蓋回去。這個(gè)習(xí)慣讓我至少兩次在十分鐘內(nèi)恢復(fù)了一個(gè)正在做活動(dòng)的論壇。從那以后我每次動(dòng)插件目錄都強(qiáng)制走一遍“打包—記錄改動(dòng)點(diǎn)—再覆蓋”的流程希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取