SpringSecurityOAuth2實現(xiàn)微信授權登錄
繼上一篇走了下登錄的流程后,已經(jīng)熟悉了不少,這一篇就來嘗試下微信的授權登錄實現(xiàn),之所以學下微信,是因為微信在當前的地位還是無人可及的,而且也是因為微信的OAuth2比較不同,挺多需要自定義的,因此來搞下微信授權登錄,會了這個,相信別的第三方都可以輕松應對。
一. 準備工作
工程建立與之前一樣
配置OAuth應用
對比之前的Github和Gitee,咱們都在他們那創(chuàng)建了自己的OAuth應用,那么對于微信來說,也是需要的,只是微信有些特殊,微信平臺限制只有微信公眾號服務號才能使用授權登錄。那我們這種普通使用者是不是沒法搞了?
實際上,微信還是提供了一個測試平臺來供我們模擬服務號進行功能測試,我們可以到微信公眾平臺接口申請測試賬號
通過掃碼登錄后,會顯示如下頁面:
微信的不是叫ClientID,而是appid
你以為這樣就OK啦?當然不是!看到了那個接口配置信息了沒,微信需要我們配置一個接口,然后在提交時他會去請求我們的接口,做一次校驗,我們需要在自己的服務器提供這樣的接口,并且按微信的要求正確返回,他才認為我們的服務器是正常的。
具體的要求可以看他的文檔:消息接口使用指南其中最關鍵的就是這個:
其實這個也好辦,咱們寫個程序就可以了,但是這里又會有另一問題需要解決,我們自己在電腦寫的應用,電腦的網(wǎng)絡大概率是內(nèi)網(wǎng)(除非你在有公網(wǎng)的服務器開發(fā)),那微信的服務器要怎么請求到我們內(nèi)網(wǎng)的電腦?
這就需要我們?nèi)ジ阋粋€內(nèi)網(wǎng)穿透了。
- 內(nèi)網(wǎng)穿透配置
推薦一款免費的工具:cpolar
要注意的是好像24h還是多長時間,這個域名會自動刷新的,所以也僅僅是適合我們測試用用
這里我配置了幾個隧道,分別映射本地的80端口和8844端口
80端口是為了給微信服務器能用http請求我們接口
8844是應用程序開啟的端口
- 回到第二步配置接口url和Token
搞定內(nèi)網(wǎng)穿透后,將80端口對應的http的接口填入微信配置中:
token可以隨便填,但需要和接口代碼中的token保持一樣。
這里點擊提交顯示配置失敗,是因為我們的接口還沒寫,微信服務器請求不到正確響應導致。這里我用golang來快速的提供下這個接口:
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.三個字段拼接為str str := tmpSlice[0] + tmpSlice[1] + tmpSlice[2] // 3. 計算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") }
啟動應用,然后再在網(wǎng)頁上提交,就可以成功了。
- 到了這一步,離成功也不遠了
在上面這些操作,實際就是類似之前在gitee中新建一個OAuth app,但是不知道是否還記得,當時我們需要填寫一個授權成功后的回調(diào)url的,接著我們就來微信這配置。
還是微信公眾平臺測試號管理這個頁面,往下拉,會看到一個體驗接口權限表,沒錯,我們需要獲取用戶信息,就在這個里面:
點擊修改,會展示如下:
在這里填入我們的域名,注意不需要協(xié)議頭,只要域名即可,也就是內(nèi)網(wǎng)穿透給我們的那個:7453dd4b.r15.cpolar.top
注意這里不需要配置端口,只需要域名即可
好了,到了這一步,環(huán)境準備就完成了。
二. 開始編碼
閱讀官方文檔
首先,先看看微信的官方接口文檔說明在這文檔里,我們可以了解到各個接口的請求路徑以及參數(shù),這在接下來配置中需要用到。
另外,我們也可以看到,微信使用appid
而不是clientid
,這也是我們需要自定義的地方。配置文件
根據(jù)文檔,將相關的配置項寫入
spring: security: oauth2: client: registration: github: clientId: xxxx # 填入自己應用的clientId clientSecret: xxxxx # 填入自己應用的clientSecret redirectUri: http://localhost:8844/login/oauth2/code/github gitee: clientId: xxxx # 填入自己應用的clientId clientSecret: xxxx # 填入自己應用的clientSecret redirectUri: http://localhost:8844/login/oauth2/code/gitee authorizationGrantType: authorization_code wechat: clientId: xxxx # 填入自己應用的appID clientSecret: xxxx # 填入自己應用的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
- 自定義配置
關于自定義配置這塊,我們按照oauth2授權碼的流程,結(jié)合官方文檔接口,一步步看哪些是需要自定義配置的,然后給他定制上。
- 第一步是去申請授權碼
可以看到這里就需要自定義了,因為參數(shù)變?yōu)榱?code>appid以及需要加一個錨點#wechat_redirect
- 回顧下之前走過的登錄流程分析,我們已經(jīng)配置好了微信的Provider,在訪問受限制的接口時會跳轉(zhuǎn)到登錄頁面,點擊wechat,就會被OAuth2AuthorizationRequestRedirectFilter過濾器過濾處理,因此我們要自定義參數(shù),需要到這個過濾器中去查找可自定義的地方。
- 之前也分析過,在默認的實現(xiàn)類
DefaultOAuth2AuthorizationRequestResolver
解析請求時預留了一個this.authorizationRequestCustomizer.accept(builder)
,而這個builder就是構(gòu)建請求的 - 因此我們可以實現(xiàn)這個
authorizationRequestCustomizer
,再將它set進去:
private final static String WECHAT_APPID = "appid"; private final static String WECHAT_SECRET = "secret"; private final static String WECHAT_FRAGMENT = "wechat_redirect"; /** * 1. 自定義微信獲取授權碼的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,還強制需要一個錨點#wechat+redirect * @return */ public OAuth2AuthorizationRequestResolver customOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) { // 定義一個默認的oauth2請求解析器 DefaultOAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI); // 進行自定義 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); // 增加錨點,需要在uri構(gòu)建中添加 builder.authorizationRequestUri((uriBuilder) -> { uriBuilder.fragment(WECHAT_FRAGMENT); return uriBuilder.build(); }); } }); }; // 設置authorizationRequestCustomizer oAuth2AuthorizationRequestResolver.setAuthorizationRequestCustomizer(authorizationRequestCustomizer); return oAuth2AuthorizationRequestResolver; } /** * 替換Uri參數(shù),parameterMap是保存的請求的各個參數(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的實現(xiàn),它在構(gòu)建時已經(jīng)創(chuàng)建了所有默認的參數(shù),并且在
attributes
中放入了registration_id
,因此可以先拿到registration_id
,再將參數(shù)全部拿出來,再進行遍歷
在DefaultOAuth2AuthorizationRequestResolver
中:
而參數(shù)部分,在構(gòu)建uri時已經(jīng)getParameters()將參數(shù)全部拿出來,并且設置到了this.parametersConsumer
:
調(diào)用builder.parameters
的用途就是重新處理參數(shù):
這一塊可能比較亂,我只是想告訴你們怎么寫出那個自定義的代碼的,結(jié)合這些應該是可以理解的。
- 第二步是通過code獲取access_token
可以看到這里的請求參數(shù)也是需要做下變更的。
按照流程,這一步會被
OAuth2LoginAuthenticationFilter
過濾處理,然后會交給AuthenticationManager
,最終會委托給ProviderManager
處理,再找到合適的Provider處理,這里是OAuth2LoginAuthenticationProvider
,它又讓OAuth2AuthorizationCodeAuthenticationProvider
幫忙處理了。直接來到
OAuth2AuthorizationCodeAuthenticationProvider
的authenticate()方法,它是交給了accessTokenResponseClient去請求獲取access_token的:找到
OAuth2AccessTokenResponseClient
的實現(xiàn)類:DefaultAuthorizationCodeTokenResponseClient
,看到他的getTokenResponse
方法,存在一個requestEntityConverter
,請求實體轉(zhuǎn)換器,并且提供了set方法,這就是說明我們可以自定義替換默認實現(xiàn)接著進去它的實現(xiàn)類看看做了什么:
一眼看穿,實際就是在構(gòu)造請求參數(shù),那么我們只需要來實現(xiàn)自己的requestEntityConverter
就可以在請求參數(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. 自定義請求access_token時的請求體轉(zhuǎn)換器 * 獲取access_token * https://api.weixin.qq.com/sns/oauth2/access_token? * appid=APPID * &secret=SECRET * &code=CODE 從上一個請求響應中獲取 * &grant_type=authorization_code 框架幫忙填寫了 */ public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> customOAuth2AccessTokenResponseClient() { // 定義默認的Token響應客戶端 DefaultAuthorizationCodeTokenResponseClient oAuth2AccessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient(); // 定義默認的轉(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ù)轉(zhuǎn)換器 oAuth2AuthorizationCodeGrantRequestEntityConverter.setParametersConverter(customParameterConverter); // 自定義RestTemplate處理響應content-type為“text/plain” OAuth2AccessTokenResponseHttpMessageConverter oAuth2AccessTokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); oAuth2AccessTokenResponseHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON)); // 處理TOKEN_TYPE為null的問題,自定義accessTokenResponseParametersConverter,給TOKEN_TYPE賦值 // 因為已經(jīng)有默認的處理了,只是需要給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); }; // 設置這個轉(zhuǎn)換器 oAuth2AccessTokenResponseHttpMessageConverter.setAccessTokenResponseConverter(setAccessTokenResponseConverter); RestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(), oAuth2AccessTokenResponseHttpMessageConverter)); restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); // 設置自定義轉(zhuǎn)換器 oAuth2AccessTokenResponseClient.setRequestEntityConverter(oAuth2AuthorizationCodeGrantRequestEntityConverter); // 設置自定義RestTemplate oAuth2AccessTokenResponseClient.setRestOperations(restTemplate); return oAuth2AccessTokenResponseClient; }
注意看上面代碼,除了參數(shù)轉(zhuǎn)換這一部分的自定義外,還多做了一些處理響應的操作,主要原因是微信接口返回的是json字符串,但他的content-type卻不是application/json,而是text/plain!!!,因此在這里會踩坑,沒有做處理的話,可能你會遇到這樣的報錯:
既然返回的是
text/plain
,那我們也只能做處理去兼容,注意下DefaultAuthorizationCodeTokenResponseClient
類不止是提供了我們自定義請求實體轉(zhuǎn)換,他發(fā)起請求的RestOperations也提供了set方法,也就是我們也可以自定義RestOperations來將text/plain
給支持進去。
- 我們可以先看看官方中是怎么設置這個
RestOperations
,他在構(gòu)造方法中初始化:
在初始化RestTemplate
(RestOperations的實現(xiàn)類)時傳入了轉(zhuǎn)換器OAuth2AccessTokenResponseHttpMessageConverter
,進去看看:
這就是官方自己定義的一個轉(zhuǎn)換器,用來處理請求access_token響應的消息轉(zhuǎn)換器,其實我們自定義就可以照貓畫瓢,照抄這個轉(zhuǎn)換器,再改改適配我們需要的。
但是看到這個轉(zhuǎn)換器也提供了一些自定義的接口:accessTokenResponseConverter
和accessTokenResponseParametersConverter
,那我們也可以直接就自定義這部分。
- 接著看看這個
OAuth2AccessTokenResponseHttpMessageConverter
繼承了AbstractHttpMessageConverter<T> implements HttpMessageConverter<T>
,該父類內(nèi)有一個方法可以設置MediaType:
因此我們要想支持text/plain
,那我們可以直接調(diào)用這個方法,進行設置,因此有了以下代碼:
OAuth2AccessTokenResponseHttpMessageConverter oAuth2AccessTokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); oAuth2AccessTokenResponseHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON));
- 到這里,好像都沒問題了,但是一運行起來,又會報錯,這次的坑是:springsecurity默認token響應對象
OAuth2AccessTokenResponse
中的OAuth2AccessToken
對象在構(gòu)造時必須有TokenType這個屬性,否則會報錯:
但是我們請求接口時響應數(shù)據(jù)里沒有TokenType,因此我們這里需要再處理下,給他填個值,這里就要用到OAuth2AccessTokenResponseHttpMessageConverter
提供的自定義接口accessTokenResponseConverter
了,在將參數(shù)轉(zhuǎn)為OAuth2AccessTokenResponse
對象時給他的OAuth2AccessToken
設置一個TokenType:
// 處理TOKEN_TYPE為null的問題,自定義accessTokenResponseParametersConverter,給TOKEN_TYPE賦值 // 因為已經(jīng)有默認的處理了,只是需要給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é)束,這里挺繁瑣的,有兩個坑需要埋,因此嘮叨比較長。
- 第三步通過access_token獲取用戶信息
在這里依然是需要自定義一些操作,首先就是請求了,然后響應也是需要處理,因為微信響應的用戶信息的實體是不同的,自然也是需要自定義了。
- 根據(jù)之前的流程分析,我們回到
OAuth2LoginAuthenticationProvider
的authenticate
方法中,在獲取到access_token后,緊接著就是獲取用戶信息了:
這里調(diào)用了一個userService的loadUser方法,并且返回了一個OAuth2User,這個OAuth2User是一個接口,因此我們自定義的用戶實體只要實現(xiàn)它即可作為返回值返回了,在這里先定義出來:
@Data public class WeChatEntity implements OAuth2User { // 用戶的唯一標識 private String openid; // 用戶昵稱 private String nickname; // 用戶的性別,值為1表示男,值為2表示女,值為0表示未知 private Integer sex; // 用戶個人資料填寫的省份 private String province; // 普通用戶個人資料填寫的城市 private String city; // 國家,如中國為CN private String country; // 用戶頭像,最后一個數(shù)值代表正方形頭像大?。ㄓ?、46、64、96、132數(shù)值可選,0代表640*640正方形頭像), // 用戶沒有頭像時該項為空。若用戶更換頭像,原有頭像URL將失效。 private String headimgurl; // 用戶特權信息 private List<String> privilege; // 只有在用戶將公眾號綁定到微信開放平臺帳號后,才會出現(xiàn)該字段。 private String unionid; @Override public Map<String, Object> getAttributes() { return null; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } /** 不可以返回null,在構(gòu)建實體時會有斷言 **/ @Override public String getName() { return nickname; } }
這里需要注意的就是
getName()
方法不返回null,因為在OAuth2AuthorizedClient
構(gòu)造中斷言它不為空
- 接著便是要看看這個
loadUser
做了什么了,找到默認的實現(xiàn)類DefaultOAuth2UserService
:
雖然這里也是提供了自定義接口,但是微信獲取用戶信息的接口參數(shù)是query參數(shù),需要拼接在請求url上,獲取的類型也是我們自定義的實體,因此這里不采用直接實現(xiàn)提供的自定義接口的方式,而是直接實現(xiàn)一個我們自己的UserService。
3. 實現(xiàn)代碼
- 首先我們要實現(xiàn)自己的UserService,最好的方法就是直接參考默認實現(xiàn)的,先整個復制,再改成適合我們自己的
- 第一個要改的地方就是
getResponse
方法,我們需要自己構(gòu)造請求url:
private ResponseEntity<WeChatEntity> getResponse(OAuth2UserRequest userRequest) { OAuth2Error oauth2Error; try { // 發(fā)起Get請求,請求參數(shù)是query參數(shù),需要自己拼接 MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(); queryParams.add("access_token", userRequest.getAccessToken().getTokenValue()); // 獲取access token時,其他參數(shù)被存儲在了userRequest中,從里面把openid拿出來 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); } }
- 因為我們的參數(shù)是自己拼接的,因此這個
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); // 直接返回最終的實體 WeChatEntity userAttributes = (WeChatEntity)response.getBody(); return userAttributes; } } }
- 最后還要處理響應體的轉(zhuǎn)換,將我們獲取到的數(shù)據(jù)轉(zhuǎn)換為
WeChatEntity
,這就需要Spring的HttpMessageConverter了,而且在微信獲取用戶信息中返回的還是JSON字符串,text/plain
的,因此我們還需要再處理這些問題,有了上面的處理經(jīng)驗,我們知道是從RestTemplate
入手,我們可以參考SpringSecurity官方實現(xiàn)的這個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 -> { // 授權端點配置 oauth2LoginCustomizer.authorizationEndpoint().authorizationRequestResolver(customOAuth2AuthorizationRequestResolver(clientRegistrationRepository)); // 獲取token端點配置 oauth2LoginCustomizer.tokenEndpoint().accessTokenResponseClient(customOAuth2AccessTokenResponseClient()); // 獲取用戶信息端點配置 oauth2LoginCustomizer.userInfoEndpoint().userService(new WeChatUserService()); }); return http.build(); }
到了這里就真的大功告成…
接著準備測試…
三. 測試驗證
有了以上的自定義改造后,剩下的就是測試驗證了,對于微信,因為我們只是測試,沒有接入網(wǎng)站應用,因此我們也沒法使用那種二維碼掃碼登錄的方式來測試了。。
但我們可以使用微信開發(fā)者工具來發(fā)起請求,微信開發(fā)者工具需要先使用微信賬號登錄,這樣你發(fā)起請求就相當于是用這個賬號來申請微信的權限。
打開后登錄后如下界面:
啟動我們的應用,然后在微信開發(fā)者工具中訪問
http://347b2d93.r8.cpolar.top/hello
或http://347b2d93.r8.cpolar.top/user
:點擊
tencent-wechat
,同意授權:最后訪問到資源:
注意:關于獲取用戶信息,性別和地區(qū)等字段是空的問題,不要慌,是因為微信他不再返回這些字段的值了。
具體可以查看這個:微信公眾平臺用戶信息相關接口調(diào)整公告
四. 總結(jié)
這一篇主要是介紹了對于微信的第三方登錄自定義,講的可能比較亂,還是得結(jié)合源碼理解理解,我只想把思路和為什么盡量都分享清楚,當然這只是測試,真正的支持微信第三方還得需要在微信登記公眾號等操作,那些是需要認證啥的,我們當前學習的話目前的已經(jīng)足夠了。
到此這篇關于SpringSecurityOAuth2實現(xiàn)微信授權登錄的文章就介紹到這了,更多相關SpringSecurityOAuth2微信授權登錄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
- SpringSecurity實現(xiàn)權限認證與授權的使用示例
- SpringSecurity進行認證與授權的示例代碼
- springSecurity用戶認證和授權的實現(xiàn)
- SpringBoot整合SpringSecurity認證與授權
- 深入淺析springsecurity入門登錄授權
- SpringBoot+SpringSecurity實現(xiàn)基于真實數(shù)據(jù)的授權認證
- springsecurity第三方授權認證的項目實踐
- SpringSecurity數(shù)據(jù)庫進行認證和授權的使用
- SpringSecurity授權機制的實現(xiàn)(AccessDecisionManager與投票決策)
相關文章
Java?nacos動態(tài)配置實現(xiàn)流程詳解
使用動態(tài)配置的原因是properties和yaml是寫到項目中的,好多時候有些配置需要修改,每次修改就要重新啟動項目,不僅增加了系統(tǒng)的不穩(wěn)定性,也大大提高了維護成本,非常麻煩,且耗費時間2022-09-09一文帶你了解Java創(chuàng)建型設計模式之原型模式
原型模式其實就是從一個對象在創(chuàng)建另外一個可定制的對象,不需要知道任何創(chuàng)建的細節(jié)。本文就來通過示例為大家詳細聊聊原型模式,需要的可以參考一下2022-09-09Java的MyBatis框架中實現(xiàn)多表連接查詢和查詢結(jié)果分頁
這篇文章主要介紹了Java的MyBatis框架中實現(xiàn)多表連接查詢和查詢結(jié)果分頁,借助MyBatis框架中帶有的動態(tài)SQL查詢功能可以比普通SQL查詢做到更多,需要的朋友可以參考下2016-04-04Nacos?版本不一致報錯Request?nacos?server?failed解決
這篇文章主要為大家介紹了Nacos?版本不一致報錯Request?nacos?server?failed的解決方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11