由于項目需要從服務端對第三方發起請求,而且第三方沒有提供SDK的情況下,只能根據對方api文檔發送請求了,對方接口的格式是:地址+簽名,post請求上送具體參數的方式去請求對方服務。
背景
很簡單的一個需求,然而開始就卡住了,在Apifox調用能正常返回數據,而一用restTemplate去請求,就報400錯誤。
org.springframework.web.client.HttpClientErrorException$BadRequest: 400 Bad Requestat org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:79)at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:122)at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:102)at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63)at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:778)at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:736)at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:670)at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:579)
...
Apifox能調通,而代碼調不通,不外乎代碼創建起來的https請求有問題
分析
本來沒打算細究,出錯了打算改用OkHttpClient
試一下,還真調通了
private void action(String uri){OkHttpClient client = new OkHttpClient();RequestBody body = null;body = RequestBody.create(MediaType.parse("application/json"), data);// data是jsonObject轉的String數據Request request = new Request.Builder().url(uri).method("POST", body).addHeader("Content-Type", "application/json").build();try {Response execute = client.newCall(request).execute();System.out.println(execute);} catch (IOException e) {throw new RuntimeException(e);}}
感覺有點離譜,所以直接用抓包工具(這里用的是fiddler,剛學著用,踩了點坑)抓包一下
POST https://xxx/xx/request?AccessKeyId=xxx&Expires=xx&Signature=xxx&Timestamp=2023-12-11T03%253A39%253A36Z HTTP/1.1
Accept: application/json, application/*+json
Content-Type: application/json;charset=UTF-8
User-Agent: Java/1.8.0_221
Host: xxxxx
Connection: keep-alive
Content-Length: xx{"data":"0"}
返回數據是
HTTP/1.1 400 Bad Request
Date: Mon, 11 Dec 2023 03:39:38 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive9d
{"code":"","message":"日期格式錯誤,請使用yyyy-MM-ddTHH:mm:ssZ格式"}
0
很明顯的錯誤提示“日期格式錯誤,請使用yyyy-MM-ddTHH:mm:ssZ格式
”,回頭看下上送的報文是“Timestamp=2023-12-11T03%253A39%253A36Z
”,雖然不怎么熟悉url的編碼解碼,但是很明顯%253A
實際上是%3A
既“:
”,所以是因為時間被雙重加密了,因為在拼接url的時候,我已經手動給時間進行了URL編碼
URLEncoder.encode(String.valueOf(time), "UTF-8")
所以去掉后再來
POST https://xxx/xx/request?AccessKeyId=xxx&Expires=xx&Signature=***&Timestamp=2023-12-11T08:25:21Z HTTP/1.1
Accept: application/json, application/*+json
Content-Type: application/json;charset=UTF-8
User-Agent: Java/1.8.0_221
Host: xxxxx
Connection: keep-alive
Content-Length: xx{"data":"0"}
返回數據
HTTP/1.1 401 Unauthorized
Date: Mon, 11 Dec 2023 08:25:21 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive6c
{"error":{"code":"AuthFailure","message":"簽名錯誤"}}
0
玩我呢…我不格式化它也不格式化,只能斷點分析了。發現RestTemplate這里對url進行了處理
@Override@Nullablepublic <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback,@Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {// 這里getUriTemplateHandler調用了一個默認的處理器對url進行了處理URI expanded = getUriTemplateHandler().expand(url, uriVariables);return doExecute(expanded, method, requestCallback, responseExtractor);}
解決
所以,只要改變這個處理器就可以解決問題了
@Beanpublic RestTemplate restTemplate(){RestTemplate restTemplate = new RestTemplate();DefaultUriBuilderFactory uriFactory = new DefaultUriBuilderFactory();// 這里選擇了不處理而是自己手動去處理編碼了,所以就不再會出現之前的問題了uriFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);restTemplate.setUriTemplateHandler(uriFactory);// ...return restTemplate;}
總結
很多時候,其實在編碼的時候會下意識自信自己編寫的沒有問題, 以至于在調試的時候很難去發現問題點(開始的時候,其實我先斷點了請求,發現請求是成功出去了,也沒能關注到%253A的問題,一個勁的根據其他文章說的協議、請求頭什么的問題在嘗試),所以當發現問題但又自認為自己沒有問題的時候,不妨換個角度換個方式,或者重新從頭寫一遍(有時候邏輯多的時候,其實也不要怕,一邊寫一邊復盤會比自己只干看著分析還可能好一點)當然具體情況要結合自己實際去行動了。