使用Spring處理x-www-form-urlencoded方式

Spring處理x-www-form-urlencoded方式

最近在重寫一個項目時遇到瞭許多奇葩問題,這個項目是一個簡單的web後臺項目,基本上全都是增刪改查數據庫的操作。這裡面遇到幾個用spring接收前端post請求的接口。

基本情況是post請求有四種data參數格式,這些基礎知識在我另一片博文中提到過這裡就不廢話瞭。主要是因為前端有兩個地方用到瞭這個接口,但是在用這個接口的時候兩個地方用法都不同,奇葩的c++居然還都解析成功瞭(其實因為c++沒有對請求參數格式和數據做檢查所以一直沒有問題)。

一個地方是發送的是application/json格式,發送瞭一個jsonArray數據(數據例子[“abc”, “bcd”])這個是沒有問題的正確使用方式。(下面簡稱前者)

另一個地方是發送的application/x-www-form-urlencode格式,發送的也是一個jsonArray數據。(下面簡稱後者)

前者解析方式比較簡單

 @RequestMapping(value = "/check_apps_version",
      method = RequestMethod.POST,
      produces = {"application/json;charset=UTF-8"},
      consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
  @ResponseBody
  public BaseResponse<List<AppListItem>> checkAppsVersion(ReqCheckAppsVersion requstParam,
      @RequestBody List<String> apps) {
    return new BaseResponse<>();
  }

後者這個發送方式在spring用@RequestBody解析時就很怪異,但是前段是手機APP已經發佈出去瞭沒法修改,隻能後端來修改滿足這個奇怪的需求

通過調試發現後者前端的接口傳過來的參數是”[“abc”,”def”]=”這樣子的,本身x-www-form-urlencode是多個kev-value對的數據格式,所以現在沒有value隻有key瞭,隻能通過字符串處理來解決瞭。

  @RequestMapping(value = "/check_apps_version",
      method = RequestMethod.POST,
      produces = {"application/json;charset=UTF-8"},
      consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
  @ResponseBody
  public BaseResponse<List<AppListItem>> checkAppsVersionParams(HttpServletRequest request,
                                                                ReqCheckAppsVersion requstParam,
                                                                @RequestBody String apps) {
 String body = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
    return BaseResponse.success();
  }

兩種方式,一種是通過@RequestBody把post data解析成string格式,另一種是通過HttpServletRequest解析出整個原始post data。然後做字符串處理。

但是在做做這個測試時候我們想瞭一下會不會有隻有value沒有key的情況,也就是這樣”=[“abc”,”def”]”。

測試結果是用tomcat沒法從HttpServletRequest到這個post data,但是用jetty可以從HttpServletRequest解析到post data。

這個可能是tomcat和jetty的區別吧,還沒有弄清楚什麼原因。但是我們的問題總算是解決瞭,最大感觸就是前人挖坑後人埋啊。

希望以後能註意一下代碼健壯性的問題,避免給別人或者自己挖坑。

關於application/x-www-form-urlencoded編碼

同事遇到在servlet端通過request對象getInputStream讀取POST過來的數據,卻讀不到的問題,懷疑是tomcat的問題。查瞭一下Content-type是application/x-www-form-urlencoded,估計是被解析成瞭parameters,果然在他獲取流之前,有過request.getParameter的操作。

熟悉servlet的話,這個問題應該算常識瞭。它其實跟容器無關,所有的servlet容器都是這樣的行為。幾年前在實現一個網關代理的時候就遇到過這個問題,當時使用的是jetty,發現POST過來的數據讀不到,也是application/x-www-form-urlencoded編碼,斷點跟蹤發現是在獲取流之前有過request.getParameter,數據會被解析,並且後續數據流不可再被讀取。

在servlet規范3.1.1節裡,對POST數據何時會被當做parameters有描述:

1. The request is an HTTP or HTTPS request.
2. The HTTP method is POST.
3. The content type is application/x-www-form-urlencoded.
4. The servlet has made an initial call of any of the getParameter family of methods on the request object.

If the conditions are met, post form data will no longer be available for reading directly from the request object’s input stream.

規范裡已經明確的聲明當請求滿足:

1) http/https

2) POST

3) Content-type 是application/x-www-form-urlencoded

4) 調用過getParameter方法,則數據會被當做請求的paramaters,而不能再通過 request 的 inputstream 直接讀取。

所以不論tomcat、jetty還是其他servlet容器都遵循這個方式。不過話說回來,為什麼application/x-www-form-urlencoded編碼的數據會被當做parameter來解析呢?

使用http上傳數據可以用GET或POST,使用GET的話,隻能通過uri的queryString形式,這會遇到長度的問題,各個瀏覽器或server可能對長度支持的不同,所以到要提交的數據如果太長並不適合使用GET提交。

采用POST的話,既可以在uri中帶有queryString也可以將數據放在body中。body內容可以有多種編碼形式,其中application/x-www-form-urlencoded編碼其實是基於uri的percent-encoding編碼的,所以采用application/x-www-form-urlencoded的POST數據和queryString隻是形式不同,本質都是傳遞參數。

在tomcat的Request.parseParameters方法裡,對於application/x-www-form-urlencoded是有做判斷的,對這種編碼會去解析body裡的數據,填充到parameters裡,所以後續想再通過流的方式讀取body是讀不到的(除非你沒有觸發過getParameter相關的方法)。

在HTML4之前,表單數據的編碼方式隻有application/x-www-form-urlencoded這一種(現在默認也是這種方式),因為早期的時候,web上提交過來的數據也是非常簡單的,基本上以key-value形式為主,所以表單采用application/x-www-form-urlencoded這種編碼形式也沒什麼問題。

在HTML4裡又引入瞭multipart/form-data編碼,對於這兩種編碼如何選擇,請參考這裡

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: