基於IdentityServer的系統對接微信公眾號
業務需求
公司有兩個業務系統,A和B,AB使用者之間屬於多對一的關係,資料庫裡面也就是兩張表,A表有個外來鍵指向B。現在需要實現以下幾個功能。
- A使用者掃描B的二維碼,填寫相關的註冊資訊,註冊完成之後自動屬於B。也就是表A的外加欄位指向B。
- 老使用者和微信openid繫結。
- 使用者在公眾號裡面自動登入。
專案結構
公司專案基於.net core 2.1 + Vue,
後端有以下幾個子系統:
- 基於IdentityServer4 的asp.net mvc,簡稱account 專案,配了域名account.xxx.com
- 兩個業務系統api。A和B。分別域名配置aapi.xxx.com 和bapi.xxx.com
- 其他。。
前端有以下幾個系統,都是基於Vue的SPA:
- A業務系統,域名a.xxx.com
- B業務系統,域名b.xxx.com
- 其他。
登入這塊的邏輯實現方式是類似的。都是基於IdentityModel/oidc-client-js
簡單介紹一下IdentityServer這個東西。
使用者登入A或B系統,就是呼叫A和B對應的webapi,webapi配置了自己的驗證伺服器是account伺服器,account驗證未通過,前端就得到401狀態碼,通過oidc-client-js的內部方法引導使用者進行登入。跳轉account的頁面,使用者輸入使用者名稱密碼,登入成功,account伺服器判斷是A or B過來的登入請求,帶上token回跳到配置的對應頁面。業務系統再次用獲取到的token請求api,呼叫成功。用一個圖來說。
實現方法
用了盛派微信sdk ,特別感謝大佬的貢獻。
推薦一下微信沙箱環境 ,專案做完下來除了"無法在測試的公眾號裡面推送小程式訊息”無法實現之外(因為推送的需要公眾號和小程式有一個繫結關係),其他都ok。
因為版本的關係,account系統升級了asp.net core 2.2。
先實現上面第一個需求
這裡用到微信裡面生成帶引數的二維碼 功能。B系統建立了使用者之後,生成一個對應的guid,然後把這個guid作為引數,呼叫sdk就能得到二維碼的url。
//建立ticket var qrRstTicketRst = await QrCodeApi.CreateAsync(weixinSetting.WeixinAppId, 30, 100000, QrCode_ActionName.QR_LIMIT_STR_SCENE, sceneId); //通過ticket獲取二維碼對應的url var url = QrCodeApi.GetShowQrCodeUrl(qrRstTicketRst.ticket);
這裡我們專案中用到的是永久二維碼
,雖然這個二維碼上限10W個,我們業務系統B使用者不會超過那麼多。
B使用者展示二維碼給A使用者,A使用者掃描,根據文件:
如果使用者還未關注公眾號,則使用者可以關注公眾號,關注後微信會將帶場景值關注事件推送給開發者。
如果使用者已經關注公眾號,在使用者掃描後會自動進入會話,微信也會將帶場景值掃描事件推送給開發者。
觸發程式碼裡面分別對應的是OnEvent_SubscribeRequest
和OnEvent_ScanRequest
,兩個方法裡面的程式碼基本上是一樣的。RequestMessageEvent_Scan.EventKey
可以得到上面的guid值。Subscribe事件裡面得到的EventKey會比Scan的多一個qrscene
字首,處理的時候要注意一點。兩個方法引數都能通過FromUserName
獲取到掃描的使用者的openId,然後在這個方法裡面返回一個帶引數(A的openId,和B的guid)的註冊連結,A使用者註冊的時候就提交了這兩個引數,後臺就能拿到。
順道說一句,公眾號裡面使用者每次操作只能被動返回一條訊息。如要主動推送,需要用模板訊息的方式。
實現第二個需求
對於老使用者,這裡需要一個賬號繫結的功能。也就是業務系統的賬號和openId做一個關聯。繫結的關鍵在於這個如何獲取這個openId,這裡有兩種方式。
-
使用者點選公眾號的選單,後端獲取到這個事件,在
OnEvent_ClickRequest
中,判斷RequestMessageEvent_Click.EventKey==xxx
,返回一個帶openId的繫結頁面的連結給使用者。比如/bind?openId=xxx,使用者點選這個連結,系統引導使用者登入,然後點選繫結按鈕,實現繫結。 - 基於微信網頁授權 ,這個在自動登入裡面也用到了,所以下面解釋。
實現第三個需求
系統中使用者和微信的openId已經繫結,所以,只要知道每次訪問頁面的openId就應該能實現自動登入。openId是通過微信網頁授權的方式獲取到。流程可以看文件 。簡單來說,先拿code,再換token,同時拿到openId。實現步驟分以下幾步。
- 新增一個公眾號選單,type是view,也就是點了之後會開啟一個頁面,頁面地址直接用獲取code的url.
{ "type": "view", "name": "登入A", "url":"https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxb66259f2a353&redirect_uri=http%3A%2F%2Faccount.xxx.cn%2Fweixincallback%2Fcallback&response_type=code&scope=snsapi_base&state=spa.A#wechat_redirect" }
url中的state
引數會和code一起返回給設定的redirectUrl,這個可以用來給我們在account登入中心判斷是需要登入A還是B,以便最後回跳到對應的業務頁面。
這裡在沙箱配置跳轉域名的時候注意一下,只要寫域名就好。
- 開啟頁面,需要使用者點允許授權。通過之後瀏覽器會把code 和 state引數帶這get請求redirect_url
- callback頁面的邏輯
callback 接收code和state兩個引數。
public async Task<IActionResult> Callback(string code, string state){...}//方法簽名
用這個code呼叫sdk裡面的api獲取token,同時可以拿到openid。
var tokenResult = await OAuthApi.GetAccessTokenAsync(AppId, AppSecret, code); if (tokenResult.errcode != ReturnCode.請求成功) { throw new BizException("獲取微信使用者資訊失敗"); } var openId = tokenResult.openid;
通過open獲取使用者資訊。
var userInfo = await userService.GetByOpenId(openId);//userService是自己的業務service
然後呼叫HttpContext.SignInAsync
登入。
public static async Task SignInAsync(this HttpContext context, string subject, string name, AuthenticationProperties properties, params Claim[] claims) { var clock = context.GetClock(); var user = new IdentityServerUser(subject) { DisplayName = name, AdditionalClaims = claims, AuthenticationTime = clock.UtcNow.UtcDateTime }; await context.SignInAsync(user, properties); }
HttpContext是當前請求的上下文。
subject可以理解為使用者的標識。
name可以理解是使用者顯示的名字。
AuthenticationProperties是此次認證的一些配置,比如有效時長之類的。
Claim可以理解為這個subject帶的一些屬性。
await HttpContext.SignInAsync(userMobile, userName, props, claims);
呼叫完之後就登入成功。
然後通過帶來的state引數判斷需要跳轉的client。
var client = await clientStore.FindClientByIdAsync(state); return Redirect($"{client.PostLogoutRedirectUris.FirstOrDefault()}?logined=true");
這裡帶一個logined=true引數,用來給client做一些邏輯。
總結
首先要感謝的肯定是盛派微信sdk的contributors,沒有他們系統對接起來應該會慢很多。
然後我想說,IdentityServer是個好東西,現在公司.NET相關的系統都已經用這個實現統一的登入邏輯了,系統維護的代價小了許多。
說起來其實也是第一次對接微信公眾號相關的東西,在走通這條路之前走了不少彎路,不過好在走通了。希望對其他人有幫助。