欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringSecurityOAuth2實(shí)現(xiàn)微信授權(quán)登錄

 更新時(shí)間:2023年09月25日 10:02:21   作者:Lucas小毛驢  
微信的登錄功能是用戶注冊(cè)和使用微信的必經(jīng)之路之一,而微信授權(quán)登錄更是方便了用戶的登錄操作,本文主要介紹了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)換器也提供了一些自定義的接口:accessTokenResponseConverteraccessTokenResponseParametersConverter,那我們也可以直接就自定義這部分。

  • 接著看看這個(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ù)之前的流程分析,我們回到OAuth2LoginAuthenticationProviderauthenticate方法中,在獲取到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/hellohttp://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)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Java?nacos動(dòng)態(tài)配置實(shí)現(xià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?list移除元素相關(guān)操作指南

    Java?list移除元素相關(guān)操作指南

    這篇文章主要給大家介紹了關(guān)于Java?list移除元素相關(guān)操作的相關(guān)資料,文中介紹的方法包括增強(qiáng)for循環(huán)、迭代器、Stream流和removeIf()方法,同時(shí)還介紹了如何從一個(gè)列表中刪除包含另一個(gè)列表元素的方法,以及如何刪除指定下標(biāo)位置的元素,需要的朋友可以參考下
    2024-12-12
  • 一文帶你了解Java創(chuàng)建型設(shè)計(jì)模式之原型模式

    一文帶你了解Java創(chuàng)建型設(shè)計(jì)模式之原型模式

    原型模式其實(shí)就是從一個(gè)對(duì)象在創(chuàng)建另外一個(gè)可定制的對(duì)象,不需要知道任何創(chuàng)建的細(xì)節(jié)。本文就來(lái)通過示例為大家詳細(xì)聊聊原型模式,需要的可以參考一下
    2022-09-09
  • SpringBoot?內(nèi)置工具類的使用

    SpringBoot?內(nèi)置工具類的使用

    本文主要介紹了SpringBoot?內(nèi)置工具類的使用,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-12-12
  • Java編程Webservice指定超時(shí)時(shí)間代碼詳解

    Java編程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-11
  • Java的MyBatis框架中實(shí)現(xiàn)多表連接查詢和查詢結(jié)果分頁(yè)

    Java的MyBatis框架中實(shí)現(xiàn)多表連接查詢和查詢結(jié)果分頁(yè)

    這篇文章主要介紹了Java的MyBatis框架中實(shí)現(xiàn)多表連接查詢和查詢結(jié)果分頁(yè),借助MyBatis框架中帶有的動(dòng)態(tài)SQL查詢功能可以比普通SQL查詢做到更多,需要的朋友可以參考下
    2016-04-04
  • Springboot異步事件配置和使用示例詳解

    Springboot異步事件配置和使用示例詳解

    Spring框架提供了一套事件處理機(jī)制,允許應(yīng)用在各個(gè)組件之間傳遞狀態(tài)信息,自定義事件通常繼承自ApplicationEvent類,Springboot通過自動(dòng)配置簡(jiǎn)化了異步處理的配置,實(shí)現(xiàn)開箱即用,Spring事件模型核心是觀察者模式,適用于解耦和提高響應(yīng)速度
    2024-10-10
  • Nacos?版本不一致報(bào)錯(cuò)Request?nacos?server?failed解決

    Nacos?版本不一致報(bào)錯(cuò)Request?nacos?server?failed解決

    這篇文章主要為大家介紹了Nacos?版本不一致報(bào)錯(cuò)Request?nacos?server?failed的解決方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-11-11
  • 聊聊Spring Cloud Cli 初體驗(yàn)

    聊聊Spring Cloud Cli 初體驗(yàn)

    這篇文章主要介紹了聊聊Spring Cloud Cli 初體驗(yàn),SpringBoot CLI 是spring Boot項(xiàng)目的腳手架工具。非常具有實(shí)用價(jià)值,需要的朋友可以參考下
    2018-04-04
  • SpringBoot如何讀取resources目錄下的文件

    SpringBoot如何讀取resources目錄下的文件

    這篇文章主要介紹了SpringBoot如何讀取resources目錄下的文件問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-07-07

最新評(píng)論