微信官方你真的懂OAuth2?Spring Security OAuth2整合企業(yè)微信掃碼登錄

企業(yè)微信掃碼登錄DEMO參見文末。

現(xiàn)在很多企業(yè)都接入了企業(yè)微信,作為私域社群工具,企業(yè)微信開放了很多API,可以打通很多自有的應(yīng)用。既然是應(yīng)用,那肯定需要做登錄。正好企業(yè)微信提供了企業(yè)微信掃碼授權(quán)登錄功能,而且號(hào)稱使用了OAuth,正好拿這個(gè)檢驗(yàn)一下Spring Security OAuth2專欄的威力。

正當(dāng)我興致勃勃打開文檔學(xué)習(xí)的時(shí)候,臉上笑容逐漸消失,這確定是OAuth的嗎?

參數(shù)都變了,跟OAuth(不管是1.0還是2.0)規(guī)定不一樣,然而這還不是最離譜的。按正常OAuth2的要求,拿到code之后就可以換access_token了是吧?企業(yè)微信的access_token居然和上面掃碼獲取code這一步完全無關(guān),甚至獲取access_token才是第一步!


?
而且這個(gè)access_token接口,你還不能頻繁調(diào)用,要緩存起來公用。

那費(fèi)了半天勁兒去拿code有啥用呢?


居然這個(gè)code是拿用戶信息的,不得不說,我服了!這也就算了,命名上能不能走點(diǎn)心,一會(huì)兒下劃線,一會(huì)兒駝峰:

{
   "errcode": 0,
   "errmsg": "ok",
   "OpenId":"OPENID",
   "DeviceId":"DEVICEID",
   "external_userid":"EXTERNAL_USERID"
}
這個(gè)JSON風(fēng)格,果然是大廠,講究人,一個(gè)JSON要三個(gè)人來寫才體面圖片!反序列化的時(shí)候我還得給你寫一個(gè)兼容,這是要拉滿我的KPI是吧?算了,忍忍吧,老板就要這個(gè)功能,它就是一坨翔,做開發(fā)的也得含淚吃下去,干圖片!

環(huán)境準(zhǔn)備
準(zhǔn)備一個(gè)內(nèi)網(wǎng)穿透
開發(fā)微信相關(guān)的應(yīng)用都需要搞一個(gè)內(nèi)網(wǎng)穿透,在我往期的文章都有介紹。搞一個(gè)映射域名出來,就像下面這樣:

http://invybj.natappfree.cc -> 127.0.0.1:8082
invybj.natappfree.cc會(huì)映射到我本地的8082端口,也就是我本地要開發(fā)應(yīng)用的端口。

創(chuàng)建應(yīng)用
首先去企業(yè)微信管理后臺(tái)創(chuàng)建一個(gè)應(yīng)用,如圖:


?
圖里的參數(shù)AgentId和Secret要記下來備用。

還有一個(gè)企業(yè)微信的corpid,你可以從下面這個(gè)位置拿到,也要記下來備用。


配置內(nèi)網(wǎng)穿透域名
在創(chuàng)建應(yīng)用這一頁(yè)往下拉到頁(yè)面底端,你會(huì)看到:


點(diǎn)擊已啟用進(jìn)入下面這個(gè)頁(yè)面:


這里配置你授權(quán)登錄應(yīng)用生產(chǎn)的正式域名或者上面內(nèi)網(wǎng)穿透的域名,注意只配置域名,而且不能使用localhost。

?
其實(shí)我感覺改寫hosts文件也能用啊,你可以試一試。

到這里環(huán)境就搞定了,接下來就開始寫Spring Security兼容代碼吧。

Spring Security兼容企業(yè)微信掃碼登錄
寫起來太惡心了,不過對(duì)比文檔和OAuth2的流程之后其實(shí)也沒那么麻煩。我先放出我調(diào)試好的配置:

spring:
  security:
    oauth2:
      client:
        registration:
          work-wechat-scan:
            # client-id為企業(yè)微信 的企業(yè)ID
            # 下面client-id是假的,你用你自己的企業(yè)ID
            client-id: wwaxxxxxx
            # client-secret企業(yè)微信對(duì)應(yīng)應(yīng)用的secret,
            # 每個(gè)企業(yè)微信應(yīng)用都有獨(dú)立的secret,不要搞錯(cuò)
            # 下面client-secret假的,你用你自己創(chuàng)建的企業(yè)微信應(yīng)用secret
            client-secret:  nvzGI4Alp3zxxxxxxxKbnfTEets5W8
            authorization-grant-type: authorization_code
            redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
        provider:
          work-wechat-scan:
            authorization-uri: https://open.work.weixin.qq.com/wwopen/sso/qrConnect
            token-uri: https://qyapi.weixin.qq.com/cgi-bin/gettoken
            user-info-uri: https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo
這里client-id使用你企業(yè)微信的企業(yè)ID,client-secret使用上面創(chuàng)建應(yīng)用的secret值。

?
這里的work-wechat-scan是客戶端的registrationId

封裝企業(yè)微信拉起二維碼URL
我們期望的是保持Spring Security OAuth2的風(fēng)格,當(dāng)我訪問:

http://invybj.natappfree.cc/oauth2/authorization/work-wechat-scan
會(huì)重定向到企業(yè)微信掃碼登錄鏈接,格式為:

https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=CORPID&agentid=AGENTID&redirect_uri=REDIRECT_URI&state=STATE
這個(gè)和以前胖哥實(shí)現(xiàn)微信網(wǎng)頁(yè)授權(quán)的原理差不多,都是通過改造OAuth2AuthorizationRequestResolver接口來實(shí)現(xiàn),只需要實(shí)現(xiàn)一個(gè)Consumer<OAuth2AuthorizationRequest.Builder>就行了。

邏輯是:把client_id替換為appid,增加一個(gè)agentid參數(shù),連帶redirect_uri和state四個(gè)參數(shù)之外的其它OAuth2參數(shù)全干掉,拼接成上面的URL。

這么寫:


把這個(gè)Consumer配置到DefaultOAuth2AuthorizationRequestResolver就行了。

適配OAuth2獲取access_token
經(jīng)過這一步掃碼拿到code就不成問題了,按照OAuth2該拿access_token了,需要自定義一個(gè)函數(shù)式接口:

Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>>
也就是利用OAuth2AuthorizationCodeGrantRequest生成RestTemplate需要的請(qǐng)求對(duì)象RequestEntity<?>。按照企業(yè)微信獲取access_token的文檔,這樣自定義:


把這個(gè)配置到DefaultAuthorizationCodeTokenResponseClient就行了。

access_token的緩存,我放在了下一步進(jìn)行解決。

適配獲取用戶信息
code和access_token都拿到了,最后一步獲取用戶的信息。這里是比較麻煩的因?yàn)楂@取access_token后并沒有直接提供將code傳遞給OAuth2UserService的方法。最后發(fā)現(xiàn)OAuth2AccessTokenResponse的additionalParameters屬性可以傳遞到OAuth2UserService,于是就利用代理模式改造了OAuth2AccessTokenResponseClient來實(shí)現(xiàn):


自定義企業(yè)微信OAuth2UserService
這個(gè)和微信網(wǎng)頁(yè)授權(quán)我封裝的差不多,改下參數(shù)封裝成URI交給RestTemplate請(qǐng)求企業(yè)微信API。惡心的是要反序列化兼容三個(gè)微信研發(fā)工程師寫的一個(gè)JSON:

@Data
public class WorkWechatOAuth2User implements OAuth2User {
    private Set<GrantedAuthority> authorities;
    private Integer errcode;
    private String errmsg;
    @JsonAlias("OpenId")
    private String openId;
    @JsonAlias("UserId")
    private String userId;
}
收尾
拿到用戶信息后,就結(jié)束了,你實(shí)現(xiàn)一個(gè)AuthenticationSuccessHandler來保證登錄憑證和你平臺(tái)一致,無論是cookie還是JWT,最后把它配置到這里:

httpSecurity.oauth2Login()
    .successHandler(AuthenticationSuccessHandler successHandler)
試一下效果
?
務(wù)必使用域名進(jìn)行訪問,不要使用localhost或者IP。

訪問http://invybj.natappfree.cc/login,這里是內(nèi)網(wǎng)穿透域名,出現(xiàn):


企業(yè)微信掃碼登錄的地址其實(shí)就是http://invybj.natappfree.cc/oauth2/authorization/work-wechat-scan。點(diǎn)擊跳轉(zhuǎn)到掃碼頁(yè)面:


然后用你對(duì)應(yīng)的企業(yè)微信APP掃碼,企業(yè)和用戶要和申請(qǐng)應(yīng)用的一致。掃碼后:


這個(gè)就是Spring Security 封裝的用戶認(rèn)證信息Authentication對(duì)象,是真正的登錄,這里我沒有注入權(quán)限,你需要在企業(yè)微信的OAuth2UserService實(shí)現(xiàn)中注入權(quán)限和更多的信息。

總結(jié)
沒有實(shí)現(xiàn)不了的,只要把原理和流程搞清楚就行。不過如果上游微信把代碼寫規(guī)范一些,下游何必寫這么多冗余的代碼。完整DEMO通過公眾號(hào) 碼農(nóng)小胖哥 回復(fù) wwopen 獲取。

作者:碼農(nóng)小胖哥



歡迎關(guān)注:碼農(nóng)小胖哥