webview 復(fù)用微信小程序獲取用戶(hù)信息的解決方案
簡(jiǎn)介
詳細(xì)討論如何通過(guò)迭代演進(jìn),最終實(shí)現(xiàn)一個(gè)以微信小程序作為認(rèn)證媒介的 OAuth 2 驗(yàn)證流程,在保證安全的前提下,為網(wǎng)頁(yè)提供獲取系統(tǒng)用戶(hù)信息的能力。
本文最終探討了如何將微信小程序作為系統(tǒng)的 OAuth 認(rèn)證媒介,而不是講述如何在微信小程序中對(duì)接微信登錄。
前提
整篇討論都限于通過(guò)企業(yè)認(rèn)證的微信小程序,個(gè)人版的微信小程序不適用!
企業(yè)的微信小程序和微信的對(duì)接已經(jīng)完成。
背景
企業(yè)基于微信小程序開(kāi)發(fā)了完整的登錄注冊(cè)、用戶(hù)檔案等功能。隨后,企業(yè)的運(yùn)營(yíng)方不斷地在營(yíng)銷(xiāo)活動(dòng)中復(fù)用微信小程序的登錄注冊(cè)和用戶(hù)信息獲取的能力,這種營(yíng)銷(xiāo)活動(dòng)頁(yè)面,通常外包給第三方開(kāi)發(fā),以 webview 的形式嵌入在微信小程序中,本質(zhì)上是一個(gè) html 網(wǎng)頁(yè)。
復(fù)用價(jià)值
節(jié)省開(kāi)發(fā)成本
這種活動(dòng)頁(yè)面成百上千,由不同的外包服務(wù)商獨(dú)立開(kāi)發(fā),每次都要獨(dú)立開(kāi)發(fā)登錄注冊(cè)的話,成本太大。從 DRY 原則上說(shuō),這些活動(dòng)頁(yè)面只需要關(guān)注每次活動(dòng)的具體變化的內(nèi)容即可(通常是一個(gè)不同的網(wǎng)頁(yè)小游戲)。
提升用戶(hù)體驗(yàn)
登錄注冊(cè)時(shí),要求用戶(hù)提供手機(jī)號(hào),這十分普遍。但是,在網(wǎng)頁(yè)中實(shí)現(xiàn)這個(gè)功能,一般都是通過(guò)用戶(hù)填寫(xiě)手機(jī)號(hào),接收驗(yàn)證碼,再填入驗(yàn)證碼由服務(wù)器驗(yàn)證。這個(gè)過(guò)程比較耗時(shí),需要用戶(hù)的多次輸入。相反,微信小程序可以利用微信的能力,只需要用戶(hù)點(diǎn)擊一次授權(quán)按鈕,即可獲得手機(jī)號(hào)。
通過(guò)重用微信小程序的用戶(hù)登錄注冊(cè)功能,用戶(hù)感受更方便快捷。
需求分析
這些活動(dòng)頁(yè)面,最終需要的用戶(hù)信息,多數(shù)情況下,只有一個(gè),那就是用戶(hù)在系統(tǒng)中的唯一標(biāo)識(shí)。這樣,在隨后的互動(dòng)中,就可以區(qū)分不同的用戶(hù)來(lái)。比如,在游戲互動(dòng)中,用戶(hù)中獎(jiǎng)了,就需要記錄是誰(shuí)中了什么獎(jiǎng)。因?yàn)檫@些互動(dòng)方式都是第三方開(kāi)發(fā)的,由他們來(lái)存儲(chǔ)這些中獎(jiǎng)信息很自然。但最終要關(guān)聯(lián)到內(nèi)部系統(tǒng)的用戶(hù),就需要這個(gè)用戶(hù)身份標(biāo)識(shí)。
所以,終極問(wèn)題是,微信小程序的 webview 里的網(wǎng)頁(yè),如何獲取到微信小程序的用戶(hù)唯一身份標(biāo)識(shí)?
方案零(客戶(hù)端明文傳輸用戶(hù)身份標(biāo)識(shí),可以立即否決)
在 webview 里的互動(dòng)進(jìn)行到某個(gè)時(shí)機(jī),網(wǎng)頁(yè)調(diào)起微信小程序的登錄注冊(cè)功能,用戶(hù)完成登錄注冊(cè)之后,微信小程序通過(guò) url 回調(diào)網(wǎng)頁(yè),在 query string 中,帶上 userId。
安全性
不安全!
開(kāi)發(fā)周期
最短
修改范圍
微信小程序客戶(hù)端,回調(diào)網(wǎng)頁(yè) url 功能
網(wǎng)頁(yè)端,接收回調(diào)并讀取 userId 參數(shù)的功能
解決了什么?
webview 中的網(wǎng)頁(yè)獲取不到 userId 的問(wèn)題
不能解決什么?
沒(méi)有安全性可言。如果通過(guò)偽造 userId 直接調(diào)用網(wǎng)頁(yè)的 url,在該網(wǎng)頁(yè)的域中,用戶(hù)身份信息就被偽造了。
補(bǔ)救措施
一旦發(fā)現(xiàn)這種行為,網(wǎng)頁(yè)端能做的是改版,不展示任何用戶(hù)信息;但是數(shù)據(jù)庫(kù)里存放的錯(cuò)誤映射關(guān)系,沒(méi)有辦法恢復(fù)了,不能再被使用。
流程

結(jié)論
不能使用!
方案一(jwt,不推薦)
由于前端 url 的傳遞參數(shù)容易修改,所以需要避開(kāi)使用前端在不同系統(tǒng)間直接明文傳遞 userId 的方式。一個(gè)很自然的想法就是使用 jwt,將 userId 包裝在 jwt 里,由小程序傳給 webview。網(wǎng)頁(yè)端接收到 jwt 后,需要驗(yàn)證 jwt 是否有效。由于 jwt 一旦被篡改,就通不過(guò)校驗(yàn),從而可以防止任意偽造 userId 的情況。
安全性
較不安全
開(kāi)發(fā)周期
很短
修改范圍
微信小程序前端,直接傳遞小程序后端的 jwt 給到 webview
網(wǎng)頁(yè)端后端,驗(yàn)證 jwt
解決了什么
方案零的安全問(wèn)題。在方案零中,任何人都可以通過(guò)猜測(cè) userId,遍歷所有網(wǎng)頁(yè)端保存過(guò)的用戶(hù)信息。采用這個(gè)方案,猜測(cè)的 userId 會(huì)通不過(guò)網(wǎng)頁(yè)端后端的驗(yàn)證。
不能解決什么
被捕獲的 jwt,在有效期內(nèi),可以被重放攻擊。用戶(hù) Bob 仍然可以通過(guò)拿到用戶(hù) Alice 的 jwt,打開(kāi)網(wǎng)頁(yè) url,看到 Alice 的信息。
補(bǔ)救措施
同方案零
流程

前提
這個(gè)方案由于使用了 jwt,并且要求網(wǎng)頁(yè)服務(wù)器端做驗(yàn)證,所以需要微信小程序后端和網(wǎng)頁(yè)后端共享頒發(fā) jwt 的密鑰。
結(jié)論
不推薦!
因?yàn)楹茈y把控第三方開(kāi)發(fā)們,一定做了驗(yàn)證操作。而且,要共享密鑰,這不可行。
方案二(OAuth 2 客戶(hù)端憑據(jù)許可模式)
由于方案零和方案一,本質(zhì)上都是通過(guò)客戶(hù)端傳遞 userId,只是一個(gè)通過(guò)明文,一個(gè)通過(guò) jwt。綜上所述,這都不安全,所以可以再進(jìn)一步,去掉客戶(hù)端交流用戶(hù)信息的渠道,而改為服務(wù)器端來(lái)進(jìn)行 userId 的查詢(xún)。即讓網(wǎng)頁(yè)端的服務(wù)器端來(lái)向小程序服務(wù)器端查詢(xún) userId,但這要求網(wǎng)頁(yè)服務(wù)器端在查詢(xún)時(shí)帶上用戶(hù)的另一信息。考慮到都是在微信生態(tài),可以讓網(wǎng)頁(yè)服務(wù)器端查詢(xún)系統(tǒng)的 userId 時(shí),帶上unionId。
安全性
較安全
開(kāi)發(fā)周期
較短
修改范圍
微信小程序后端,OAuth 2 client_credential 授權(quán)功能;以及改造原有微信小程序的登錄注冊(cè)流程,在原有的登錄注冊(cè)流程之后,保存一下 unionId, userId 的映射關(guān)系,并對(duì)受信任的客戶(hù)端(對(duì)微信小程序后端是客戶(hù)端,但實(shí)現(xiàn)上通常是服務(wù)器端)開(kāi)放查詢(xún)接口。
網(wǎng)頁(yè)后端,這個(gè)方案需要服務(wù)器和服務(wù)器間的溝通;另外網(wǎng)頁(yè)后端要對(duì)接微信服務(wù),實(shí)現(xiàn) unionid 的獲取功能
解決了什么
由于 url 上不用帶上明文或者加密的 userId,用戶(hù) Bob 沒(méi)有機(jī)會(huì)偽造用戶(hù) Alice 的身份,更不能通過(guò)猜測(cè) userId 遍歷系統(tǒng)用戶(hù)了。
不能解決什么
一旦網(wǎng)頁(yè)端服務(wù)器獲取到了服務(wù)器端信任的令牌,就可以從微信小程序后端查詢(xún)?nèi)我庥脩?hù)的 userId(不需要用戶(hù)授權(quán))。所以這個(gè)方案并不能防止網(wǎng)頁(yè)端服務(wù)器來(lái)遍歷會(huì)員信息。
補(bǔ)救措施
一旦發(fā)現(xiàn)網(wǎng)頁(yè)端本身存在不安全的行為,可以立即吊銷(xiāo)用于建立服務(wù)器端信任的 client id 和 secret,使得網(wǎng)頁(yè)端服務(wù)器不能再使用 userId 的查詢(xún)接口。
流程
網(wǎng)頁(yè)授權(quán)拿到了用戶(hù)的 unionId
網(wǎng)頁(yè)服務(wù)器端用 unionId,向小程序服務(wù)器查詢(xún)userId (server to server talk 的方式)
查到直接做后續(xù)操作
沒(méi)查到拉起小程序登錄注冊(cè),然后小程序回調(diào)網(wǎng)頁(yè)(webview 方式)
網(wǎng)頁(yè)的邏輯回到 2,再次查詢(xún)。得到 userId,進(jìn)行后續(xù)操作

前提
網(wǎng)頁(yè)端使用和小程序在相同的開(kāi)發(fā)平臺(tái)里綁定到同一公眾號(hào)主體的賬號(hào),授權(quán)獲取 unionid。
結(jié)論
在工期緊急的情況下,推薦使用
方案三(OAuth 2 授權(quán)碼許可模式)
方案二要求系統(tǒng)信任網(wǎng)頁(yè)端,從而開(kāi)放 userId 查詢(xún)權(quán)限給到網(wǎng)頁(yè)端。注意當(dāng)把用戶(hù)的 userId 給到網(wǎng)頁(yè)端時(shí),不需要經(jīng)過(guò)用戶(hù)的同意,因?yàn)椴捎昧丝蛻?hù)端憑據(jù)許可模式。
本方案更進(jìn)一步,網(wǎng)頁(yè)端要獲取用戶(hù)的 userId,需要經(jīng)過(guò)用戶(hù)顯式同意,即采用 OAuth 2 的授權(quán)碼許可模式。
安全性
更安全
開(kāi)發(fā)周期
最長(zhǎng)
修改范圍
微信小程序后端采用 OAuth 2 的授權(quán)碼許可模式
微信小程序前端增加用戶(hù)授權(quán)第三方查看其信息的頁(yè)面
網(wǎng)頁(yè)端服務(wù)器對(duì)接 OAuth 2 的流程
解決了什么
這個(gè)方案解決了方案二不能防止網(wǎng)頁(yè)端遍歷系統(tǒng)用戶(hù)的問(wèn)題,因?yàn)橹挥杏脩?hù)顯式授權(quán),它才能拿到用戶(hù)信息。
不能解決什么
一旦用戶(hù)同意,userId 就“泄露”給了網(wǎng)頁(yè)端。這個(gè)網(wǎng)頁(yè)應(yīng)用自己保存的(userId, 中獎(jiǎng)信息)這個(gè)映射關(guān)系,由這個(gè)網(wǎng)頁(yè)應(yīng)用自己負(fù)責(zé)保證安全。
補(bǔ)救措施
如果發(fā)現(xiàn)網(wǎng)頁(yè)端應(yīng)用本身存在不安全的行為,可以吊銷(xiāo)其 clientid/secret,使得該網(wǎng)頁(yè)端不能再訪問(wèn)微信小程序后端接口。
流程
1. 網(wǎng)頁(yè)端的已有流程(方案二中的網(wǎng)頁(yè)端要求用戶(hù)授權(quán),拿用戶(hù)的 unionid,這一步可以去掉)
2. 網(wǎng)頁(yè)端拉起小程序,用戶(hù)登錄/注冊(cè)和小程序自己內(nèi)部的登錄/注冊(cè)要多一步,即展示一個(gè)頁(yè)面,明確告知用戶(hù):該網(wǎng)頁(yè)應(yīng)用想使用你的憑據(jù),以獲取你的用戶(hù)信息。措詞可以相應(yīng)修改
3. 用戶(hù)拒絕登錄/注冊(cè),流程結(jié)束
4. 用戶(hù)如果同意,在小程序完成了登錄注冊(cè)流程,那么小程序回調(diào)webview,在 url query string 里帶上一個(gè)后端頒發(fā)的臨時(shí) code,有效期 5 分鐘(防止重放,有效期必須很短,而且限制只能被使用一次)
5. 網(wǎng)頁(yè)端服務(wù)器向小程序后端請(qǐng)求授權(quán)(client_credential)
6. 授權(quán)失敗,流程結(jié)束
7. 授權(quán)成功,使用server trust access_token 作為憑據(jù),用 code 換取用戶(hù)的 access_token,scope: userinfo
8. 換取失敗,流程結(jié)束
9. 換取成功,使用該用戶(hù) access_token,向小程序后端查詢(xún)用戶(hù)信息(userId 等等)

結(jié)論
在時(shí)間允許的情況下,最推薦使用這個(gè)方案!
總結(jié)
本文列舉了在webview中復(fù)用微信小程序的登錄注冊(cè)功能以換取用戶(hù)信息的 4 種方案,今天就寫(xiě)到這里。如果有考慮不周的地方,后面再發(fā)文修補(bǔ),再有時(shí)間甚至可以分享關(guān)鍵代碼,或者做一個(gè)開(kāi)源代碼庫(kù)。
