SpringSecurityOAuth2實(shí)現(xiàn)微信授權(quán)登錄
繼上一篇走了下登錄的流程后,已經(jīng)熟悉了不少,這一篇就來(lái)嘗試下微信的授權(quán)登錄實(shí)現(xiàn),之所以學(xué)下微信,是因?yàn)槲⑿旁诋?dāng)前的地位還是無(wú)人可及的,而且也是因?yàn)槲⑿诺腛Auth2比較不同,挺多需要自定義的,因此來(lái)搞下微信授權(quán)登錄,會(huì)了這個(gè),相信別的第三方都可以輕松應(yīng)對(duì)。
一. 準(zhǔn)備工作
工程建立與之前一樣
配置OAuth應(yīng)用
對(duì)比之前的Github和Gitee,咱們都在他們那創(chuàng)建了自己的OAuth應(yīng)用,那么對(duì)于微信來(lái)說,也是需要的,只是微信有些特殊,微信平臺(tái)限制只有微信公眾號(hào)服務(wù)號(hào)才能使用授權(quán)登錄。那我們這種普通使用者是不是沒法搞了?
實(shí)際上,微信還是提供了一個(gè)測(cè)試平臺(tái)來(lái)供我們模擬服務(wù)號(hào)進(jìn)行功能測(cè)試,我們可以到微信公眾平臺(tái)接口申請(qǐng)測(cè)試賬號(hào)
通過掃碼登錄后,會(huì)顯示如下頁(yè)面:
微信的不是叫ClientID,而是appid
你以為這樣就OK啦?當(dāng)然不是!看到了那個(gè)接口配置信息了沒,微信需要我們配置一個(gè)接口,然后在提交時(shí)他會(huì)去請(qǐng)求我們的接口,做一次校驗(yàn),我們需要在自己的服務(wù)器提供這樣的接口,并且按微信的要求正確返回,他才認(rèn)為我們的服務(wù)器是正常的。
具體的要求可以看他的文檔:消息接口使用指南其中最關(guān)鍵的就是這個(gè):
其實(shí)這個(gè)也好辦,咱們寫個(gè)程序就可以了,但是這里又會(huì)有另一問題需要解決,我們自己在電腦寫的應(yīng)用,電腦的網(wǎng)絡(luò)大概率是內(nèi)網(wǎng)(除非你在有公網(wǎng)的服務(wù)器開發(fā)),那微信的服務(wù)器要怎么請(qǐng)求到我們內(nèi)網(wǎng)的電腦?
這就需要我們?nèi)ジ阋粋€(gè)內(nèi)網(wǎng)穿透了。
- 內(nèi)網(wǎng)穿透配置
推薦一款免費(fèi)的工具:cpolar
要注意的是好像24h還是多長(zhǎng)時(shí)間,這個(gè)域名會(huì)自動(dòng)刷新的,所以也僅僅是適合我們測(cè)試用用
這里我配置了幾個(gè)隧道,分別映射本地的80端口和8844端口
80端口是為了給微信服務(wù)器能用http請(qǐng)求我們接口
8844是應(yīng)用程序開啟的端口
- 回到第二步配置接口url和Token
搞定內(nèi)網(wǎng)穿透后,將80端口對(duì)應(yīng)的http的接口填入微信配置中:
token可以隨便填,但需要和接口代碼中的token保持一樣。
這里點(diǎn)擊提交顯示配置失敗,是因?yàn)槲覀兊慕涌谶€沒寫,微信服務(wù)器請(qǐng)求不到正確響應(yīng)導(dǎo)致。這里我用golang來(lái)快速的提供下這個(gè)接口:
package main import ( "crypto/sha1" "encoding/hex" "net/http" "sort" "github.com/gin-gonic/gin" ) type ByAlphabet []string func (a ByAlphabet) Len() int { return len(a) } func (a ByAlphabet) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByAlphabet) Less(i, j int) bool { return a[i] < a[j] } func SHA1(s string) string { hash := sha1.New() hash.Write([]byte(s)) return hex.EncodeToString(hash.Sum(nil)) } func main() { engine := gin.Default() engine.GET("/", func(ctx *gin.Context) { signature := ctx.Query("signature") timestamp := ctx.Query("timestamp") nonce := ctx.Query("nonce") echostr := ctx.Query("echostr") token := "lucas" tmpSlice := []string{nonce, timestamp, token} // 1.按字典序排序 sort.Sort(ByAlphabet(tmpSlice)) // 2.三個(gè)字段拼接為str str := tmpSlice[0] + tmpSlice[1] + tmpSlice[2] // 3. 計(jì)算str的sha1加密的字符串 sha1Str := SHA1(str) // 4.比較sha1Str和signature,相同則返回echostr if sha1Str == signature { ctx.String(http.StatusOK, echostr) return } else { ctx.String(http.StatusOK, "") return } }) engine.Run(":80") }
啟動(dòng)應(yīng)用,然后再在網(wǎng)頁(yè)上提交,就可以成功了。
- 到了這一步,離成功也不遠(yuǎn)了
在上面這些操作,實(shí)際就是類似之前在gitee中新建一個(gè)OAuth app,但是不知道是否還記得,當(dāng)時(shí)我們需要填寫一個(gè)授權(quán)成功后的回調(diào)url的,接著我們就來(lái)微信這配置。
還是微信公眾平臺(tái)測(cè)試號(hào)管理這個(gè)頁(yè)面,往下拉,會(huì)看到一個(gè)體驗(yàn)接口權(quán)限表,沒錯(cuò),我們需要獲取用戶信息,就在這個(gè)里面:
點(diǎn)擊修改,會(huì)展示如下:
在這里填入我們的域名,注意不需要協(xié)議頭,只要域名即可,也就是內(nèi)網(wǎng)穿透給我們的那個(gè):7453dd4b.r15.cpolar.top
注意這里不需要配置端口,只需要域名即可
好了,到了這一步,環(huán)境準(zhǔn)備就完成了。
二. 開始編碼
閱讀官方文檔
首先,先看看微信的官方接口文檔說明在這文檔里,我們可以了解到各個(gè)接口的請(qǐng)求路徑以及參數(shù),這在接下來(lái)配置中需要用到。
另外,我們也可以看到,微信使用appid
而不是clientid
,這也是我們需要自定義的地方。配置文件
根據(jù)文檔,將相關(guān)的配置項(xiàng)寫入
spring: security: oauth2: client: registration: github: clientId: xxxx # 填入自己應(yīng)用的clientId clientSecret: xxxxx # 填入自己應(yīng)用的clientSecret redirectUri: http://localhost:8844/login/oauth2/code/github gitee: clientId: xxxx # 填入自己應(yīng)用的clientId clientSecret: xxxx # 填入自己應(yīng)用的clientSecret redirectUri: http://localhost:8844/login/oauth2/code/gitee authorizationGrantType: authorization_code wechat: clientId: xxxx # 填入自己應(yīng)用的appID clientSecret: xxxx # 填入自己應(yīng)用的appsecret redirectUri: http://347b2d93.r8.cpolar.top/login/oauth2/code/wechat authorizationGrantType: authorization_code scope: - snsapi_userinfo clientName: tencent-wechat provider: gitee: authorizationUri: https://gitee.com/oauth/authorize tokenUri: https://gitee.com/oauth/token userInfoUri: https://gitee.com/api/v5/user userNameAttribute: name wechat: authorizationUri: https://open.weixin.qq.com/connect/oauth2/authorize tokenUri: https://api.weixin.qq.com/sns/oauth2/access_token userInfoUri: https://api.weixin.qq.com/sns/userinfo userNameAttribute: nickname
- 自定義配置
關(guān)于自定義配置這塊,我們按照oauth2授權(quán)碼的流程,結(jié)合官方文檔接口,一步步看哪些是需要自定義配置的,然后給他定制上。
- 第一步是去申請(qǐng)授權(quán)碼
可以看到這里就需要自定義了,因?yàn)閰?shù)變?yōu)榱?code>appid以及需要加一個(gè)錨點(diǎn)#wechat_redirect
- 回顧下之前走過的登錄流程分析,我們已經(jīng)配置好了微信的Provider,在訪問受限制的接口時(shí)會(huì)跳轉(zhuǎn)到登錄頁(yè)面,點(diǎn)擊wechat,就會(huì)被OAuth2AuthorizationRequestRedirectFilter過濾器過濾處理,因此我們要自定義參數(shù),需要到這個(gè)過濾器中去查找可自定義的地方。
- 之前也分析過,在默認(rèn)的實(shí)現(xiàn)類
DefaultOAuth2AuthorizationRequestResolver
解析請(qǐng)求時(shí)預(yù)留了一個(gè)this.authorizationRequestCustomizer.accept(builder)
,而這個(gè)builder就是構(gòu)建請(qǐng)求的 - 因此我們可以實(shí)現(xiàn)這個(gè)
authorizationRequestCustomizer
,再將它set進(jìn)去:
private final static String WECHAT_APPID = "appid"; private final static String WECHAT_SECRET = "secret"; private final static String WECHAT_FRAGMENT = "wechat_redirect"; /** * 1. 自定義微信獲取授權(quán)碼的uri * https://open.weixin.qq.com/connect/oauth2/authorize? * appid=wx807d86fb6b3d4fd2 * &redirect_uri=http%3A%2F%2Fdevelopers.weixin.qq.com * &response_type=code * &scope=snsapi_userinfo * &state=STATE 非必須 * #wechat_redirect * 微信比較特殊,比如不是clientid,而是appid,還強(qiáng)制需要一個(gè)錨點(diǎn)#wechat+redirect * @return */ public OAuth2AuthorizationRequestResolver customOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) { // 定義一個(gè)默認(rèn)的oauth2請(qǐng)求解析器 DefaultOAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI); // 進(jìn)行自定義 Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer = (builder) -> { builder.attributes(attributeConsumer -> { // 判斷registrationId是否為wechat String registrationId = (String) attributeConsumer.get(OAuth2ParameterNames.REGISTRATION_ID); if ("wechat".equals(registrationId)) { // 替換參數(shù)名稱 builder.parameters(this::replaceWechatUriParamter); // 增加錨點(diǎn),需要在uri構(gòu)建中添加 builder.authorizationRequestUri((uriBuilder) -> { uriBuilder.fragment(WECHAT_FRAGMENT); return uriBuilder.build(); }); } }); }; // 設(shè)置authorizationRequestCustomizer oAuth2AuthorizationRequestResolver.setAuthorizationRequestCustomizer(authorizationRequestCustomizer); return oAuth2AuthorizationRequestResolver; } /** * 替換Uri參數(shù),parameterMap是保存的請(qǐng)求的各個(gè)參數(shù) * @param parameterMap */ private void replaceWechatUriParamter(Map<String, Object> parameterMap) { Map<String, Object> linkedHashMap = new LinkedHashMap<>(); // 遍歷所有參數(shù),有序的,替換掉clientId為appid parameterMap.forEach((k, v) -> { if (OAuth2ParameterNames.CLIENT_ID.equals(k)) { linkedHashMap.put(WECHAT_APPID, v); } else { linkedHashMap.put(k, v); } }); // 清空原始的paramterMap parameterMap.clear(); // 將新的linkedHashMap存入paramterMap parameterMap.putAll(linkedHashMap); }
- 至于這內(nèi)部替換參數(shù)的做法,可以先看看builder的實(shí)現(xiàn),它在構(gòu)建時(shí)已經(jīng)創(chuàng)建了所有默認(rèn)的參數(shù),并且在
attributes
中放入了registration_id
,因此可以先拿到registration_id
,再將參數(shù)全部拿出來(lái),再進(jìn)行遍歷
在DefaultOAuth2AuthorizationRequestResolver
中:
而參數(shù)部分,在構(gòu)建uri時(shí)已經(jīng)getParameters()將參數(shù)全部拿出來(lái),并且設(shè)置到了this.parametersConsumer
:
調(diào)用builder.parameters
的用途就是重新處理參數(shù):
這一塊可能比較亂,我只是想告訴你們?cè)趺磳懗瞿莻€(gè)自定義的代碼的,結(jié)合這些應(yīng)該是可以理解的。
- 第二步是通過code獲取access_token
可以看到這里的請(qǐng)求參數(shù)也是需要做下變更的。
按照流程,這一步會(huì)被
OAuth2LoginAuthenticationFilter
過濾處理,然后會(huì)交給AuthenticationManager
,最終會(huì)委托給ProviderManager
處理,再找到合適的Provider處理,這里是OAuth2LoginAuthenticationProvider
,它又讓OAuth2AuthorizationCodeAuthenticationProvider
幫忙處理了。直接來(lái)到
OAuth2AuthorizationCodeAuthenticationProvider
的authenticate()方法,它是交給了accessTokenResponseClient去請(qǐng)求獲取access_token的:找到
OAuth2AccessTokenResponseClient
的實(shí)現(xiàn)類:DefaultAuthorizationCodeTokenResponseClient
,看到他的getTokenResponse
方法,存在一個(gè)requestEntityConverter
,請(qǐng)求實(shí)體轉(zhuǎn)換器,并且提供了set方法,這就是說明我們可以自定義替換默認(rèn)實(shí)現(xiàn)接著進(jìn)去它的實(shí)現(xiàn)類看看做了什么:
一眼看穿,實(shí)際就是在構(gòu)造請(qǐng)求參數(shù),那么我們只需要來(lái)實(shí)現(xiàn)自己的requestEntityConverter
就可以在請(qǐng)求參數(shù)上為所欲為了。
5. 參考代碼如下:
private final static String WECHAT_APPID = "appid"; private final static String WECHAT_SECRET = "secret"; private final static String WECHAT_FRAGMENT = "wechat_redirect"; /** * 2. 自定義請(qǐng)求access_token時(shí)的請(qǐng)求體轉(zhuǎn)換器 * 獲取access_token * https://api.weixin.qq.com/sns/oauth2/access_token? * appid=APPID * &secret=SECRET * &code=CODE 從上一個(gè)請(qǐng)求響應(yīng)中獲取 * &grant_type=authorization_code 框架幫忙填寫了 */ public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> customOAuth2AccessTokenResponseClient() { // 定義默認(rèn)的Token響應(yīng)客戶端 DefaultAuthorizationCodeTokenResponseClient oAuth2AccessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient(); // 定義默認(rèn)的轉(zhuǎn)換器 OAuth2AuthorizationCodeGrantRequestEntityConverter oAuth2AuthorizationCodeGrantRequestEntityConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter(); // 自定義參數(shù)轉(zhuǎn)換器 Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> customParameterConverter = (authorizationCodeGrantRequest) -> { ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration(); OAuth2AuthorizationExchange authorizationExchange = authorizationCodeGrantRequest.getAuthorizationExchange(); MultiValueMap<String, String> parameters = new LinkedMultiValueMap(); parameters.add("grant_type", authorizationCodeGrantRequest.getGrantType().getValue()); parameters.add("code", authorizationExchange.getAuthorizationResponse().getCode()); String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri(); String codeVerifier = (String)authorizationExchange.getAuthorizationRequest().getAttribute("code_verifier"); if (redirectUri != null) { parameters.add("redirect_uri", redirectUri); } parameters.add(WECHAT_APPID, clientRegistration.getClientId()); parameters.add(WECHAT_SECRET, clientRegistration.getClientSecret()); if (codeVerifier != null) { parameters.add("code_verifier", codeVerifier); } return parameters; }; // 設(shè)置自定義參數(shù)轉(zhuǎn)換器 oAuth2AuthorizationCodeGrantRequestEntityConverter.setParametersConverter(customParameterConverter); // 自定義RestTemplate處理響應(yīng)content-type為“text/plain” OAuth2AccessTokenResponseHttpMessageConverter oAuth2AccessTokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); oAuth2AccessTokenResponseHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON)); // 處理TOKEN_TYPE為null的問題,自定義accessTokenResponseParametersConverter,給TOKEN_TYPE賦值 // 因?yàn)橐呀?jīng)有默認(rèn)的處理了,只是需要給token_type賦值 Converter<Map<String, Object>, OAuth2AccessTokenResponse> setAccessTokenResponseConverter = (paramMap) -> { DefaultMapOAuth2AccessTokenResponseConverter defaultMapOAuth2AccessTokenResponseConverter = new DefaultMapOAuth2AccessTokenResponseConverter(); paramMap.put(OAuth2ParameterNames.TOKEN_TYPE, OAuth2AccessToken.TokenType.BEARER.getValue()); return defaultMapOAuth2AccessTokenResponseConverter.convert(paramMap); }; // 設(shè)置這個(gè)轉(zhuǎn)換器 oAuth2AccessTokenResponseHttpMessageConverter.setAccessTokenResponseConverter(setAccessTokenResponseConverter); RestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(), oAuth2AccessTokenResponseHttpMessageConverter)); restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); // 設(shè)置自定義轉(zhuǎn)換器 oAuth2AccessTokenResponseClient.setRequestEntityConverter(oAuth2AuthorizationCodeGrantRequestEntityConverter); // 設(shè)置自定義RestTemplate oAuth2AccessTokenResponseClient.setRestOperations(restTemplate); return oAuth2AccessTokenResponseClient; }
注意看上面代碼,除了參數(shù)轉(zhuǎn)換這一部分的自定義外,還多做了一些處理響應(yīng)的操作,主要原因是微信接口返回的是json字符串,但他的content-type卻不是application/json,而是text/plain!!!,因此在這里會(huì)踩坑,沒有做處理的話,可能你會(huì)遇到這樣的報(bào)錯(cuò):
既然返回的是
text/plain
,那我們也只能做處理去兼容,注意下DefaultAuthorizationCodeTokenResponseClient
類不止是提供了我們自定義請(qǐng)求實(shí)體轉(zhuǎn)換,他發(fā)起請(qǐng)求的RestOperations也提供了set方法,也就是我們也可以自定義RestOperations來(lái)將text/plain
給支持進(jìn)去。
- 我們可以先看看官方中是怎么設(shè)置這個(gè)
RestOperations
,他在構(gòu)造方法中初始化:
在初始化RestTemplate
(RestOperations的實(shí)現(xiàn)類)時(shí)傳入了轉(zhuǎn)換器OAuth2AccessTokenResponseHttpMessageConverter
,進(jìn)去看看:
這就是官方自己定義的一個(gè)轉(zhuǎn)換器,用來(lái)處理請(qǐng)求access_token響應(yīng)的消息轉(zhuǎn)換器,其實(shí)我們自定義就可以照貓畫瓢,照抄這個(gè)轉(zhuǎn)換器,再改改適配我們需要的。
但是看到這個(gè)轉(zhuǎn)換器也提供了一些自定義的接口:accessTokenResponseConverter
和accessTokenResponseParametersConverter
,那我們也可以直接就自定義這部分。
- 接著看看這個(gè)
OAuth2AccessTokenResponseHttpMessageConverter
繼承了AbstractHttpMessageConverter<T> implements HttpMessageConverter<T>
,該父類內(nèi)有一個(gè)方法可以設(shè)置MediaType:
因此我們要想支持text/plain
,那我們可以直接調(diào)用這個(gè)方法,進(jìn)行設(shè)置,因此有了以下代碼:
OAuth2AccessTokenResponseHttpMessageConverter oAuth2AccessTokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); oAuth2AccessTokenResponseHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON));
- 到這里,好像都沒問題了,但是一運(yùn)行起來(lái),又會(huì)報(bào)錯(cuò),這次的坑是:springsecurity默認(rèn)token響應(yīng)對(duì)象
OAuth2AccessTokenResponse
中的OAuth2AccessToken
對(duì)象在構(gòu)造時(shí)必須有TokenType這個(gè)屬性,否則會(huì)報(bào)錯(cuò):
但是我們請(qǐng)求接口時(shí)響應(yīng)數(shù)據(jù)里沒有TokenType,因此我們這里需要再處理下,給他填個(gè)值,這里就要用到OAuth2AccessTokenResponseHttpMessageConverter
提供的自定義接口accessTokenResponseConverter
了,在將參數(shù)轉(zhuǎn)為OAuth2AccessTokenResponse
對(duì)象時(shí)給他的OAuth2AccessToken
設(shè)置一個(gè)TokenType:
// 處理TOKEN_TYPE為null的問題,自定義accessTokenResponseParametersConverter,給TOKEN_TYPE賦值 // 因?yàn)橐呀?jīng)有默認(rèn)的處理了,只是需要給token_type賦值 Converter<Map<String, Object>, OAuth2AccessTokenResponse> setAccessTokenResponseConverter = (paramMap) -> { DefaultMapOAuth2AccessTokenResponseConverter defaultMapOAuth2AccessTokenResponseConverter = new DefaultMapOAuth2AccessTokenResponseConverter(); paramMap.put(OAuth2ParameterNames.TOKEN_TYPE, OAuth2AccessToken.TokenType.BEARER.getValue()); return defaultMapOAuth2AccessTokenResponseConverter.convert(paramMap); };
- 到這里,第二步的自定義才結(jié)束,這里挺繁瑣的,有兩個(gè)坑需要埋,因此嘮叨比較長(zhǎng)。
- 第三步通過access_token獲取用戶信息
在這里依然是需要自定義一些操作,首先就是請(qǐng)求了,然后響應(yīng)也是需要處理,因?yàn)槲⑿彭憫?yīng)的用戶信息的實(shí)體是不同的,自然也是需要自定義了。
- 根據(jù)之前的流程分析,我們回到
OAuth2LoginAuthenticationProvider
的authenticate
方法中,在獲取到access_token后,緊接著就是獲取用戶信息了:
這里調(diào)用了一個(gè)userService的loadUser方法,并且返回了一個(gè)OAuth2User,這個(gè)OAuth2User是一個(gè)接口,因此我們自定義的用戶實(shí)體只要實(shí)現(xiàn)它即可作為返回值返回了,在這里先定義出來(lái):
@Data public class WeChatEntity implements OAuth2User { // 用戶的唯一標(biāo)識(shí) private String openid; // 用戶昵稱 private String nickname; // 用戶的性別,值為1表示男,值為2表示女,值為0表示未知 private Integer sex; // 用戶個(gè)人資料填寫的省份 private String province; // 普通用戶個(gè)人資料填寫的城市 private String city; // 國(guó)家,如中國(guó)為CN private String country; // 用戶頭像,最后一個(gè)數(shù)值代表正方形頭像大?。ㄓ?、46、64、96、132數(shù)值可選,0代表640*640正方形頭像), // 用戶沒有頭像時(shí)該項(xiàng)為空。若用戶更換頭像,原有頭像URL將失效。 private String headimgurl; // 用戶特權(quán)信息 private List<String> privilege; // 只有在用戶將公眾號(hào)綁定到微信開放平臺(tái)帳號(hào)后,才會(huì)出現(xiàn)該字段。 private String unionid; @Override public Map<String, Object> getAttributes() { return null; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } /** 不可以返回null,在構(gòu)建實(shí)體時(shí)會(huì)有斷言 **/ @Override public String getName() { return nickname; } }
這里需要注意的就是
getName()
方法不返回null,因?yàn)樵?code>OAuth2AuthorizedClient構(gòu)造中斷言它不為空
- 接著便是要看看這個(gè)
loadUser
做了什么了,找到默認(rèn)的實(shí)現(xiàn)類DefaultOAuth2UserService
:
雖然這里也是提供了自定義接口,但是微信獲取用戶信息的接口參數(shù)是query參數(shù),需要拼接在請(qǐng)求url上,獲取的類型也是我們自定義的實(shí)體,因此這里不采用直接實(shí)現(xiàn)提供的自定義接口的方式,而是直接實(shí)現(xiàn)一個(gè)我們自己的UserService。
3. 實(shí)現(xiàn)代碼
- 首先我們要實(shí)現(xiàn)自己的UserService,最好的方法就是直接參考默認(rèn)實(shí)現(xiàn)的,先整個(gè)復(fù)制,再改成適合我們自己的
- 第一個(gè)要改的地方就是
getResponse
方法,我們需要自己構(gòu)造請(qǐng)求url:
private ResponseEntity<WeChatEntity> getResponse(OAuth2UserRequest userRequest) { OAuth2Error oauth2Error; try { // 發(fā)起Get請(qǐng)求,請(qǐng)求參數(shù)是query參數(shù),需要自己拼接 MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(); queryParams.add("access_token", userRequest.getAccessToken().getTokenValue()); // 獲取access token時(shí),其他參數(shù)被存儲(chǔ)在了userRequest中,從里面把openid拿出來(lái) queryParams.add("openid", (String) userRequest.getAdditionalParameters().get("openid")); queryParams.add("lang", "zh_CN"); URI uri = UriComponentsBuilder.fromUriString(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri()).queryParams(queryParams).build().toUri(); ResponseEntity<WeChatEntity> retData = this.restOperations.exchange(uri, HttpMethod.GET, null, PARAMETERIZED_RESPONSE_TYPE); return retData; } catch (OAuth2AuthorizationException var6) { oauth2Error = var6.getError(); StringBuilder errorDetails = new StringBuilder(); errorDetails.append("Error details: ["); errorDetails.append("UserInfo Uri: ").append(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri()); errorDetails.append(", Error Code: ").append(oauth2Error.getErrorCode()); if (oauth2Error.getDescription() != null) { errorDetails.append(", Error Description: ").append(oauth2Error.getDescription()); } errorDetails.append("]"); oauth2Error = new OAuth2Error("invalid_user_info_response", "An error occurred while attempting to retrieve the UserInfo Resource: " + errorDetails.toString(), (String)null); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), var6); } catch (UnknownContentTypeException var7) { String errorMessage = "An error occurred while attempting to retrieve the UserInfo Resource from '" + userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri() + "': response contains invalid content type '" + var7.getContentType().toString() + "'. The UserInfo Response should return a JSON object (content type 'application/json') that contains a collection of name and value pairs of the claims about the authenticated End-User. Please ensure the UserInfo Uri in UserInfoEndpoint for Client Registration '" + userRequest.getClientRegistration().getRegistrationId() + "' conforms to the UserInfo Endpoint, as defined in OpenID Connect 1.0: 'https://openid.net/specs/openid-connect-core-1_0.html#UserInfo'"; oauth2Error = new OAuth2Error("invalid_user_info_response", errorMessage, (String)null); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), var7); } catch (RestClientException var8) { oauth2Error = new OAuth2Error("invalid_user_info_response", "An error occurred while attempting to retrieve the UserInfo Resource: " + var8.getMessage(), (String)null); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), var8); } }
- 因?yàn)槲覀兊膮?shù)是自己拼接的,因此這個(gè)
requestEntityConverter
轉(zhuǎn)換器就不需要了,可以直接刪除 - 然后就是
loadUser
處調(diào)用getResponse
:
@Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { Assert.notNull(userRequest, "userRequest cannot be null"); if (!StringUtils.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) { OAuth2Error oauth2Error = new OAuth2Error("missing_user_info_uri", "Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: " + userRequest.getClientRegistration().getRegistrationId(), (String)null); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } else { String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); if (!StringUtils.hasText(userNameAttributeName)) { OAuth2Error oauth2Error = new OAuth2Error("missing_user_name_attribute", "Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: " + userRequest.getClientRegistration().getRegistrationId(), (String)null); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } else { ResponseEntity<WeChatEntity> response = this.getResponse(userRequest); // 直接返回最終的實(shí)體 WeChatEntity userAttributes = (WeChatEntity)response.getBody(); return userAttributes; } } }
- 最后還要處理響應(yīng)體的轉(zhuǎn)換,將我們獲取到的數(shù)據(jù)轉(zhuǎn)換為
WeChatEntity
,這就需要Spring的HttpMessageConverter了,而且在微信獲取用戶信息中返回的還是JSON字符串,text/plain
的,因此我們還需要再處理這些問題,有了上面的處理經(jīng)驗(yàn),我們知道是從RestTemplate
入手,我們可以參考SpringSecurity官方實(shí)現(xiàn)的這個(gè)OAuth2AccessTokenResponseHttpMessageConverter
,還是照抄,再改寫:
public class WeChatUserHttpMessageConverter extends AbstractHttpMessageConverter<WeChatEntity> { private static final ParameterizedTypeReference<WeChatEntity> STRING_OBJECT_MAP; private static final Charset DEFAULT_CHARSET; private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter(); static { DEFAULT_CHARSET = StandardCharsets.UTF_8; STRING_OBJECT_MAP = new ParameterizedTypeReference<WeChatEntity>() { }; } public WeChatUserHttpMessageConverter() { super(DEFAULT_CHARSET, MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); } @Override protected boolean supports(Class<?> clazz) { return WeChatEntity.class.isAssignableFrom(clazz); } @Override protected WeChatEntity readInternal(Class<? extends WeChatEntity> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { try { WeChatEntity weChatEntity = (WeChatEntity)this.jsonMessageConverter.read(STRING_OBJECT_MAP.getType(), (Class)null, inputMessage); return weChatEntity; } catch (Exception var5) { throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Access Token Response: " + var5.getMessage(), var5, inputMessage); } } @Override protected void writeInternal(WeChatEntity weChatEntity, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { } }
- 最后的最后,配置SpringSecurity,以上的自定義,沒有配置到SpringSecurity的filterChain中,是不可能生效的。
@Bean public SecurityFilterChain filterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository) throws Exception { http .authorizeHttpRequests() .anyRequest() .authenticated().and() .oauth2Login(oauth2LoginCustomizer -> { // 授權(quán)端點(diǎn)配置 oauth2LoginCustomizer.authorizationEndpoint().authorizationRequestResolver(customOAuth2AuthorizationRequestResolver(clientRegistrationRepository)); // 獲取token端點(diǎn)配置 oauth2LoginCustomizer.tokenEndpoint().accessTokenResponseClient(customOAuth2AccessTokenResponseClient()); // 獲取用戶信息端點(diǎn)配置 oauth2LoginCustomizer.userInfoEndpoint().userService(new WeChatUserService()); }); return http.build(); }
到了這里就真的大功告成…
接著準(zhǔn)備測(cè)試…
三. 測(cè)試驗(yàn)證
有了以上的自定義改造后,剩下的就是測(cè)試驗(yàn)證了,對(duì)于微信,因?yàn)槲覀冎皇菧y(cè)試,沒有接入網(wǎng)站應(yīng)用,因此我們也沒法使用那種二維碼掃碼登錄的方式來(lái)測(cè)試了。。
但我們可以使用微信開發(fā)者工具來(lái)發(fā)起請(qǐng)求,微信開發(fā)者工具需要先使用微信賬號(hào)登錄,這樣你發(fā)起請(qǐng)求就相當(dāng)于是用這個(gè)賬號(hào)來(lái)申請(qǐng)微信的權(quán)限。
打開后登錄后如下界面:
啟動(dòng)我們的應(yīng)用,然后在微信開發(fā)者工具中訪問
http://347b2d93.r8.cpolar.top/hello
或http://347b2d93.r8.cpolar.top/user
:點(diǎn)擊
tencent-wechat
,同意授權(quán):最后訪問到資源:
注意:關(guān)于獲取用戶信息,性別和地區(qū)等字段是空的問題,不要慌,是因?yàn)槲⑿潘辉俜祷剡@些字段的值了。
具體可以查看這個(gè):微信公眾平臺(tái)用戶信息相關(guān)接口調(diào)整公告
四. 總結(jié)
這一篇主要是介紹了對(duì)于微信的第三方登錄自定義,講的可能比較亂,還是得結(jié)合源碼理解理解,我只想把思路和為什么盡量都分享清楚,當(dāng)然這只是測(cè)試,真正的支持微信第三方還得需要在微信登記公眾號(hào)等操作,那些是需要認(rèn)證啥的,我們當(dāng)前學(xué)習(xí)的話目前的已經(jīng)足夠了。
到此這篇關(guān)于SpringSecurityOAuth2實(shí)現(xiàn)微信授權(quán)登錄的文章就介紹到這了,更多相關(guān)SpringSecurityOAuth2微信授權(quán)登錄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringSecurity實(shí)現(xiàn)權(quán)限認(rèn)證與授權(quán)的使用示例
- SpringSecurity進(jìn)行認(rèn)證與授權(quán)的示例代碼
- springSecurity用戶認(rèn)證和授權(quán)的實(shí)現(xiàn)
- SpringBoot整合SpringSecurity認(rèn)證與授權(quán)
- 深入淺析springsecurity入門登錄授權(quán)
- SpringBoot+SpringSecurity實(shí)現(xiàn)基于真實(shí)數(shù)據(jù)的授權(quán)認(rèn)證
- springsecurity第三方授權(quán)認(rèn)證的項(xiàng)目實(shí)踐
- SpringSecurity數(shù)據(jù)庫(kù)進(jìn)行認(rèn)證和授權(quán)的使用
- SpringSecurity授權(quán)機(jī)制的實(shí)現(xiàn)(AccessDecisionManager與投票決策)
相關(guān)文章
Java?nacos動(dòng)態(tài)配置實(shí)現(xiàn)流程詳解
使用動(dòng)態(tài)配置的原因是properties和yaml是寫到項(xiàng)目中的,好多時(shí)候有些配置需要修改,每次修改就要重新啟動(dòng)項(xiàng)目,不僅增加了系統(tǒng)的不穩(wěn)定性,也大大提高了維護(hù)成本,非常麻煩,且耗費(fèi)時(shí)間2022-09-09一文帶你了解Java創(chuàng)建型設(shè)計(jì)模式之原型模式
原型模式其實(shí)就是從一個(gè)對(duì)象在創(chuàng)建另外一個(gè)可定制的對(duì)象,不需要知道任何創(chuàng)建的細(xì)節(jié)。本文就來(lái)通過示例為大家詳細(xì)聊聊原型模式,需要的可以參考一下2022-09-09Java編程Webservice指定超時(shí)時(shí)間代碼詳解
這篇文章主要介紹了Java編程Webservice指定超時(shí)時(shí)間代碼詳解,簡(jiǎn)單介紹了webservice,然后分享了通過使用JDK對(duì)Webservice的支持進(jìn)行Webservice調(diào)用實(shí)現(xiàn)指定超時(shí)時(shí)間完整示例,具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-11-11Java的MyBatis框架中實(shí)現(xiàn)多表連接查詢和查詢結(jié)果分頁(yè)
這篇文章主要介紹了Java的MyBatis框架中實(shí)現(xiàn)多表連接查詢和查詢結(jié)果分頁(yè),借助MyBatis框架中帶有的動(dòng)態(tài)SQL查詢功能可以比普通SQL查詢做到更多,需要的朋友可以參考下2016-04-04Nacos?版本不一致報(bào)錯(cuò)Request?nacos?server?failed解決
這篇文章主要為大家介紹了Nacos?版本不一致報(bào)錯(cuò)Request?nacos?server?failed的解決方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11