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)說(shuō),也是需要的,只是微信有些特殊,微信平臺(tái)限制只有微信公眾號(hào)服務(wù)號(hào)才能使用授權(quán)登錄。那我們這種普通使用者是不是沒(méi)法搞了?
實(shí)際上,微信還是提供了一個(gè)測(cè)試平臺(tái)來(lái)供我們模擬服務(wù)號(hào)進(jìn)行功能測(cè)試,我們可以到微信公眾平臺(tái)接口申請(qǐng)測(cè)試賬號(hào)
通過(guò)掃碼登錄后,會(huì)顯示如下頁(yè)面:
微信的不是叫ClientID,而是appid
你以為這樣就OK啦?當(dāng)然不是!看到了那個(gè)接口配置信息了沒(méi),微信需要我們配置一個(gè)接口,然后在提交時(shí)他會(huì)去請(qǐng)求我們的接口,做一次校驗(yàn),我們需要在自己的服務(wù)器提供這樣的接口,并且按微信的要求正確返回,他才認(rèn)為我們的服務(wù)器是正常的。
具體的要求可以看他的文檔:消息接口使用指南其中最關(guān)鍵的就是這個(gè):

其實(shí)這個(gè)也好辦,咱們寫(xiě)個(gè)程序就可以了,但是這里又會(huì)有另一問(wèn)題需要解決,我們自己在電腦寫(xiě)的應(yīng)用,電腦的網(wǎng)絡(luò)大概率是內(nèi)網(wǎng)(除非你在有公網(wǎng)的服務(wù)器開(kāi)發(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)用程序開(kāi)啟的端口
- 回到第二步配置接口url和Token
搞定內(nèi)網(wǎng)穿透后,將80端口對(duì)應(yīng)的http的接口填入微信配置中:
token可以隨便填,但需要和接口代碼中的token保持一樣。
這里點(diǎn)擊提交顯示配置失敗,是因?yàn)槲覀兊慕涌谶€沒(méi)寫(xiě),微信服務(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í)際就是類(lèi)似之前在gitee中新建一個(gè)OAuth app,但是不知道是否還記得,當(dāng)時(shí)我們需要填寫(xiě)一個(gè)授權(quán)成功后的回調(diào)url的,接著我們就來(lái)微信這配置。
還是微信公眾平臺(tái)測(cè)試號(hào)管理這個(gè)頁(yè)面,往下拉,會(huì)看到一個(gè)體驗(yàn)接口權(quán)限表,沒(méi)錯(cuò),我們需要獲取用戶(hù)信息,就在這個(gè)里面:
點(diǎn)擊修改,會(huì)展示如下:

在這里填入我們的域名,注意不需要協(xié)議頭,只要域名即可,也就是內(nèi)網(wǎng)穿透給我們的那個(gè):7453dd4b.r15.cpolar.top
注意這里不需要配置端口,只需要域名即可
好了,到了這一步,環(huán)境準(zhǔn)備就完成了。
二. 開(kāi)始編碼
閱讀官方文檔
首先,先看看微信的官方接口文檔說(shuō)明在這文檔里,我們可以了解到各個(gè)接口的請(qǐng)求路徑以及參數(shù),這在接下來(lái)配置中需要用到。
另外,我們也可以看到,微信使用appid而不是clientid,這也是我們需要自定義的地方。配置文件
根據(jù)文檔,將相關(guān)的配置項(xiàng)寫(xiě)入
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
- 回顧下之前走過(guò)的登錄流程分析,我們已經(jīng)配置好了微信的Provider,在訪問(wèn)受限制的接口時(shí)會(huì)跳轉(zhuǎn)到登錄頁(yè)面,點(diǎn)擊wechat,就會(huì)被OAuth2AuthorizationRequestRedirectFilter過(guò)濾器過(guò)濾處理,因此我們要自定義參數(shù),需要到這個(gè)過(guò)濾器中去查找可自定義的地方。
- 之前也分析過(guò),在默認(rèn)的實(shí)現(xiàn)類(lèi)
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ù)名稱(chēng)
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è)趺磳?xiě)出那個(gè)自定義的代碼的,結(jié)合這些應(yīng)該是可以理解的。
- 第二步是通過(guò)code獲取access_token

可以看到這里的請(qǐng)求參數(shù)也是需要做下變更的。
按照流程,這一步會(huì)被
OAuth2LoginAuthenticationFilter過(guò)濾處理,然后會(huì)交給AuthenticationManager,最終會(huì)委托給ProviderManager處理,再找到合適的Provider處理,這里是OAuth2LoginAuthenticationProvider,它又讓OAuth2AuthorizationCodeAuthenticationProvider幫忙處理了。直接來(lái)到
OAuth2AuthorizationCodeAuthenticationProvider的authenticate()方法,它是交給了accessTokenResponseClient去請(qǐng)求獲取access_token的:
找到
OAuth2AccessTokenResponseClient的實(shí)現(xiàn)類(lèi):DefaultAuthorizationCodeTokenResponseClient,看到他的getTokenResponse方法,存在一個(gè)requestEntityConverter,請(qǐng)求實(shí)體轉(zhuǎn)換器,并且提供了set方法,這就是說(shuō)明我們可以自定義替換默認(rèn)實(shí)現(xiàn)
接著進(jìn)去它的實(shí)現(xiàn)類(lèi)看看做了什么:

一眼看穿,實(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 框架幫忙填寫(xiě)了
*/
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> customOAuth2AccessTokenResponseClient() {
// 定義默認(rèn)的Token響應(yīng)客戶(hù)端
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的問(wèn)題,自定義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ì)踩坑,沒(méi)有做處理的話,可能你會(huì)遇到這樣的報(bào)錯(cuò):

既然返回的是
text/plain,那我們也只能做處理去兼容,注意下DefaultAuthorizationCodeTokenResponseClient類(lèi)不止是提供了我們自定義請(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)類(lèi))時(shí)傳入了轉(zhuǎn)換器OAuth2AccessTokenResponseHttpMessageConverter,進(jìn)去看看:

這就是官方自己定義的一個(gè)轉(zhuǎn)換器,用來(lái)處理請(qǐng)求access_token響應(yīng)的消息轉(zhuǎn)換器,其實(shí)我們自定義就可以照貓畫(huà)瓢,照抄這個(gè)轉(zhuǎn)換器,再改改適配我們需要的。
但是看到這個(gè)轉(zhuǎn)換器也提供了一些自定義的接口:accessTokenResponseConverter和accessTokenResponseParametersConverter,那我們也可以直接就自定義這部分。
- 接著看看這個(gè)
OAuth2AccessTokenResponseHttpMessageConverter繼承了AbstractHttpMessageConverter<T> implements HttpMessageConverter<T>,該父類(lèi)內(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));
- 到這里,好像都沒(méi)問(wèn)題了,但是一運(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ù)里沒(méi)有TokenType,因此我們這里需要再處理下,給他填個(gè)值,這里就要用到OAuth2AccessTokenResponseHttpMessageConverter提供的自定義接口accessTokenResponseConverter了,在將參數(shù)轉(zhuǎn)為OAuth2AccessTokenResponse對(duì)象時(shí)給他的OAuth2AccessToken設(shè)置一個(gè)TokenType:
// 處理TOKEN_TYPE為null的問(wèn)題,自定義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)。
- 第三步通過(guò)access_token獲取用戶(hù)信息

在這里依然是需要自定義一些操作,首先就是請(qǐng)求了,然后響應(yīng)也是需要處理,因?yàn)槲⑿彭憫?yīng)的用戶(hù)信息的實(shí)體是不同的,自然也是需要自定義了。
- 根據(jù)之前的流程分析,我們回到
OAuth2LoginAuthenticationProvider的authenticate方法中,在獲取到access_token后,緊接著就是獲取用戶(hù)信息了:
這里調(diào)用了一個(gè)userService的loadUser方法,并且返回了一個(gè)OAuth2User,這個(gè)OAuth2User是一個(gè)接口,因此我們自定義的用戶(hù)實(shí)體只要實(shí)現(xiàn)它即可作為返回值返回了,在這里先定義出來(lái):
@Data
public class WeChatEntity implements OAuth2User {
// 用戶(hù)的唯一標(biāo)識(shí)
private String openid;
// 用戶(hù)昵稱(chēng)
private String nickname;
// 用戶(hù)的性別,值為1表示男,值為2表示女,值為0表示未知
private Integer sex;
// 用戶(hù)個(gè)人資料填寫(xiě)的省份
private String province;
// 普通用戶(hù)個(gè)人資料填寫(xiě)的城市
private String city;
// 國(guó)家,如中國(guó)為CN
private String country;
// 用戶(hù)頭像,最后一個(gè)數(shù)值代表正方形頭像大?。ㄓ?、46、64、96、132數(shù)值可選,0代表640*640正方形頭像),
// 用戶(hù)沒(méi)有頭像時(shí)該項(xiàng)為空。若用戶(hù)更換頭像,原有頭像URL將失效。
private String headimgurl;
// 用戶(hù)特權(quán)信息
private List<String> privilege;
// 只有在用戶(hù)將公眾號(hào)綁定到微信開(kāi)放平臺(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)類(lèi)DefaultOAuth2UserService:
雖然這里也是提供了自定義接口,但是微信獲取用戶(hù)信息的接口參數(shù)是query參數(shù),需要拼接在請(qǐng)求url上,獲取的類(lèi)型也是我們自定義的實(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了,而且在微信獲取用戶(hù)信息中返回的還是JSON字符串,text/plain的,因此我們還需要再處理這些問(wèn)題,有了上面的處理經(jīng)驗(yàn),我們知道是從RestTemplate入手,我們可以參考SpringSecurity官方實(shí)現(xiàn)的這個(gè)OAuth2AccessTokenResponseHttpMessageConverter,還是照抄,再改寫(xiě):
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,以上的自定義,沒(méi)有配置到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());
// 獲取用戶(hù)信息端點(diǎn)配置
oauth2LoginCustomizer.userInfoEndpoint().userService(new WeChatUserService());
});
return http.build();
}到了這里就真的大功告成…
接著準(zhǔn)備測(cè)試…
三. 測(cè)試驗(yàn)證
有了以上的自定義改造后,剩下的就是測(cè)試驗(yàn)證了,對(duì)于微信,因?yàn)槲覀冎皇菧y(cè)試,沒(méi)有接入網(wǎng)站應(yīng)用,因此我們也沒(méi)法使用那種二維碼掃碼登錄的方式來(lái)測(cè)試了。。
但我們可以使用微信開(kāi)發(fā)者工具來(lái)發(fā)起請(qǐng)求,微信開(kāi)發(fā)者工具需要先使用微信賬號(hào)登錄,這樣你發(fā)起請(qǐng)求就相當(dāng)于是用這個(gè)賬號(hào)來(lái)申請(qǐng)微信的權(quán)限。
打開(kāi)后登錄后如下界面:

啟動(dòng)我們的應(yīng)用,然后在微信開(kāi)發(fā)者工具中訪問(wèn)
http://347b2d93.r8.cpolar.top/hello或http://347b2d93.r8.cpolar.top/user:
點(diǎn)擊
tencent-wechat,同意授權(quán):
最后訪問(wèn)到資源:

注意:關(guān)于獲取用戶(hù)信息,性別和地區(qū)等字段是空的問(wèn)題,不要慌,是因?yàn)槲⑿潘辉俜祷剡@些字段的值了。
具體可以查看這個(gè):微信公眾平臺(tái)用戶(hù)信息相關(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用戶(hù)認(rèn)證和授權(quán)的實(shí)現(xiàn)
- SpringBoot整合SpringSecurity認(rèn)證與授權(quán)
- 深入淺析springsecurity入門(mén)登錄授權(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ě)到項(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)通過(guò)示例為大家詳細(xì)聊聊原型模式,需要的可以參考一下2022-09-09
Java編程Webservice指定超時(shí)時(shí)間代碼詳解
這篇文章主要介紹了Java編程Webservice指定超時(shí)時(shí)間代碼詳解,簡(jiǎn)單介紹了webservice,然后分享了通過(guò)使用JDK對(duì)Webservice的支持進(jìn)行Webservice調(diào)用實(shí)現(xiàn)指定超時(shí)時(shí)間完整示例,具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-11-11
Java的MyBatis框架中實(shí)現(xiàn)多表連接查詢(xún)和查詢(xún)結(jié)果分頁(yè)
這篇文章主要介紹了Java的MyBatis框架中實(shí)現(xiàn)多表連接查詢(xún)和查詢(xún)結(jié)果分頁(yè),借助MyBatis框架中帶有的動(dòng)態(tài)SQL查詢(xún)功能可以比普通SQL查詢(xún)做到更多,需要的朋友可以參考下2016-04-04
Nacos?版本不一致報(bào)錯(cuò)Request?nacos?server?failed解決
這篇文章主要為大家介紹了Nacos?版本不一致報(bào)錯(cuò)Request?nacos?server?failed的解決方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11

