RestTemplate踩坑 之 ContentType 自動添加字符集


寫在前邊

最近在寫 OAuth2 對接的代碼,由於授權服務器(竹雲BambooCloud IAM)部署在甲方內網,所以想着自己 Mock 一下授權方的返回體,驗證一下我的代碼。我這才踩到了坑……

故事背景

選擇的 Mock 框架是 國產開源的 Moco(https://github.com/dreamhead/moco),先下載moco-runner-1.3.0-standalone.jar

再根據 Moco的官方文檔(https://github.com/dreamhead/moco/blob/master/moco-doc/apis.md)和竹雲對接文檔配置了以下的mock配置:

BambooCloud-IAM-OAuth2-Moco.json

[
    {
        "description": "授權回調接口",
        "request": {
            "uri": "/idp/oauth2/authorize",
            "method": "get",
            "queries": {
                "client_id": "client-id-test",
                "redirect_uri": "http://localhost:8188/api/oauth2/callback",
                "response_type": "code"
            }
        },
        "redirectTo" : "http://localhost:8188/api/oauth2/callback?code=123456"
    },
    {
        "description": "獲取token接口",
        "request": {
            "uri": "/idp/oauth2/getToken",
            "method": "post",
            "headers": {
                "content-type": "application/x-www-form-urlencoded"
            },
            "forms": {
                "client_id" : "client-id-test",
                "client_secret" : "client-secret-test",
                "grant_type" : "authorization_code",
                "code" : "123456"
            }
        },
        "response": {
            "json": {
                "access_token" : "123456789"
            }
        }
    },
    {
        "description": "獲取用戶信息接口",
        "request": {
            "uri": "/idp/oauth2/getUserInfo",
            "method": "get",
            "queries": {
                "client_id": "client-id-test",
                "access_token": "123456789"
            }
        },
        "response": {
            "json": {
                "spRoleList":["zhangsan"],
                "uid":"20190904124905344-F4BE-C2C9EFF24",
                "sorgId":null,
                "displayName":"張三",
                "loginName":"zhangsan",
                "secAccValid":1,
                "givenName":"729026",
                "pinyinShortName":null,
                "spNameList":["portalID","tyxtest","data","certificate","sysCertification","customer"],
                "employeeNumber":null
            }
        }
    }
]

啟動 Moco

java -Dfile.encoding=UTF-8 -jar moco-runner-1.3.0-standalone.jar http -p 12306 -c BambooCloud-IAM-OAuth2-Moco.json
  • 作者不愧是國人,官方文檔里的端口號竟然是12306火車票訂票網站,等等,該不會作者是方便模擬 12306 開發搶票功能才寫的 Moco 吧😂,這里也用12306端口~

  • -Dfile.encoding=UTF-8 - 指定 Moco 啟動服務的字符集,作者默認在程序里寫了 GBK,不指定返回中文可能亂碼

然后配置 SpringBoot 服務配置文件,這里就是給大家看看,不一定具備通用性:

#                 OAuth2配置                   #
################################################
#是否開啟oauth2單點登錄,true/false,默認不啟用。
oauth2.sso.enabled=true
#授權方頒發的clientId
oauth2.sso.clientId=client-id-test
#授權方頒發的clientSecret
oauth2.sso.clientSecret=client-secret-test
#登錄地址,授權方提供,示例:https://{host}:{port}/idp/oauth2/authorize
oauth2.sso.authUrl=http://localhost:12306/idp/oauth2/authorize
#獲取token地址,授權方提供,示例:https://{host}:{port}/idp/oauth2/getToken
oauth2.sso.tokenUrl=http://localhost:12306/idp/oauth2/getToken
#獲取用戶信息地址,授權方提供,示例:https://{host}:{port}/idp/oauth2/getUserInfo
oauth2.sso.userInfoUrl=http://localhost:12306/idp/oauth2/getUserInfo
#授權回調地址,由【Nginx代理當前服務的地址 或 當前服務地址+"/api/oauth2/callback"】 組成,示例:https://{host}:{port}/api/oauth2/callback
oauth2.sso.redirectUri=http://localhost:8188/api/oauth2/callback

啟動項目(8188端口,有兩個api地址: /api/oauth2/sso/api/oauth2/callback),先通過程序 http://localhost:8188/api/oauth2/sso

第一、二個請求會重定向請求到 Moco 服務上的 授權回調接口 並帶上一些參數

第三個請求由 Moco 服務的 授權回調接口 回調項目的回調地址(授權碼回調,項目會先調用 Moco 的 獲取token接口 ),然后因為全局錯誤攔截返回了左側的 JSON,提示 "400 Bad Request: [no body]"

發現問題

我還以為是 Moco 服務的問題,就用 Postman 調了下 獲取token接口,結果是通的!!

然后再看下 Moco 的控制台輸出:

經過找不同,發現項目調用接口時請求頭 Content-Type 加了 ;chartset=UTF-8 !!!

由於 Moco 配置文件里指定請求頭必須一致,才能正確返回,否則就是 400狀態碼,而且沒有返回體。

定位問題

先看看出問題的代碼部分:

懷疑 MediaType.APPLICATION_FORM_URLENCODED 值有UTF-8字符集,點進去

看注釋時寫的是不帶字符集的,debug 看下 setContentType后的 headers 內容

繼續debug

看來還沒改變,繼續debug,進入 RestTemplateexchange() 方法

resolveUrl() 這個方法我看了下,沒涉及改請求頭,問題應出在 doExecute() 方法中,斷點打在 request 對象后,看看 request 對象中的 headers,發現是空的,而下邊有一個方法是 requestCallback.doWithRequest(request); 看起來是對 request 對象進行了處理,直接跟進去

RequestCallback 有兩個最終的實現,Xhr 對應 XMLHttpRequest,這里發的是HTTP請求,所以是 HttpEntityRequestCallback 的實現

在懷疑的位置打兩個斷點,跟進循環看看,首先進了下邊的判斷,messageConverter 對象是 AllEncompassingFormHttpMessageConverter 的實例,而且在write()方法執行前,headers 仍是正常的

查看 write() 方法實現,經典 3 選 1,由於我們的 Content-Typeapplication/x-www-form-urlencoded,理應進入 FormHttpMessageConverter 實現中

writeForm() 執行前 ContentType 仍未改變,進入 writeForm() 方法看看

終於,在 org.springframework.http.converter.FormHttpMessageConverter.getFormContentType(MediaType) 的注釋和代碼中發現了問題。

對於表單內容類型的請求的 ContentType 設置分三種情況:

  • 如果沒有 ContentType 就默認添加 application/x-www-form-urlencoded;charset=UTF-8
  • 如果有 ContentType 但沒字符集,也會自動加字符集
  • 如果 ContentType 存在且有字符集設置,則直接返回

跳出 getFormContentType(),可以看到 contenType 對象已經有字符集了。

聯想到作者信誓旦旦地注釋 // should never occur 這就很說明問題了 🤪

解決問題

RestTemplate 自動使用 org.springframework.http.converter.FormHttpMessageConverter 添加字符集可能是出於對請求亂碼的擔憂。而我們這里的 Moco 配置添加了請求頭判斷,值全轉成大寫(或小寫)一定要一致,所以改一下 Moco 問題接口配置的content-type的值,加上字符集驗證效果。

有1說1 ,保存完后,看下 Moco 控制台它已經自動刷新配置文件了,Moco 真好用。

重新調用接口驗證通過。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM