# 使用 HttpClient 進行 POST 和 GET request 並進行上下載 一般簡單的需求可用 FileUtils 做到,例如下載可用 copyURLToFile 達成 但在商業邏輯中,可能連 GET 下載都需要先在 request 設定 header 值才能取得回應,為了對 HTTP 連線做更細膩的設定,推薦使用 HttpClient ## GET 下載檔案 取得的 httpEntity.getContent() 是 InputStream 可以用 FileUtils.copyInputStreamToFile 或 ByteArrayOutputStream 來處理 但不可以用 copyURLToFile 處理 ``` CloseableHttpClient httpclient = HttpClients.createDefault(); try { HttpGet httpGet = new HttpGet(url); setGetHead(httpGet, headMap); CloseableHttpResponse response1 = httpclient.execute(httpGet); try { // 印出狀態(例如 200 或 404 ) System.out.println(response1.getStatusLine()); // httpEntity 就是我們主要想取得的回應實體,下載物在裡面 HttpEntity httpEntity = response1.getEntity(); // 長度預設是 -1,有抓到東西才會是正數 long contentLength = httpEntity.getContentLength(); // 內建內容是 InputStream,這邊利用 ByteArrayOutputStream 輸出到一個檔案實體的路徑 String filepath,建議先檢查 httpEntity != null 才做後續 // 也可以用:FileUtils.copyInputStreamToFile(imageEntity.getContent(), new File(imgSavePath)); InputStream is = httpEntity.getContent(); ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] buffer = new byte[4096]; int r = 0; long totalRead = 0; while ((r = is.read(buffer)) > 0) { output.write(buffer, 0, r); totalRead += r; if (progress != null) {// 回调进度 progress.onProgress((int) (totalRead * 100 / contentLength)); } } FileOutputStream fos = new FileOutputStream(filePath); output.writeTo(fos); output.flush(); output.close(); fos.close(); EntityUtils.consume(httpEntity) ``` ## POST 上傳檔案 用 FileBody 讀入本地檔案路徑 HttpPost 本身支援用 setHeader 設定 header,所以 MultipartEntityBuilder 應該是用來設定其他 request 參數,例如寫在 body 的那種。不確定,沒試過 ``` String respStr = null; CloseableHttpClient httpclient = HttpClients.createDefault(); try { HttpPost httppost = new HttpPost(serverUrl); FileBody binFileBody = new FileBody(new File(localFilePath)); MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create(); // add the file params multipartEntityBuilder.addPart(serverFieldName, binFileBody); // 設定上傳的其他參數 if (params != null && params.size() > 0) { Set<String> keys = params.keySet(); for (String key : keys) { multipartEntityBuilder.addPart(key, new StringBody(params.get(key),ContentType.TEXT_PLAIN)); } } HttpEntity reqEntity = multipartEntityBuilder.build(); httppost.setEntity(reqEntity); CloseableHttpResponse response = httpclient.execute(httppost); try { System.out.println(response.getStatusLine()); HttpEntity entity = response.getEntity(); if (entity != null) { InputStream is = entity.getContent(); StringBuffer strBuf = new StringBuffer(); byte[] buffer = new byte[4096]; int r = 0; while ((r = is.read(buffer)) > 0) { strBuf.append(new String(buffer, 0, r, "UTF-8")); } } String responseStr = strBuf.toString(); EntityUtils.consume(entity); System.out.println("resp=" + responseStr); ``` ## Header 設定秘文被 RFC7230 限制擋下 在與銀行或電信系統對接時,常會要求 request 帶有金鑰簽署後的秘文。然而這些單位的系統,或只要高一點的 Tomcat 版本都會基於 RFC 限制去過濾 request,所以包含有特殊字元的秘文會成為被擋下的理由 特別須注意的是,這個問題無法透過 Talend API Tester 或 Postman 去交叉驗證,因為前者是瀏覽器插件、後者是前身為插件的軟體,而主流瀏覽器本身通常不太遵守 RFC 規範,也有自己的手段去繞過這些限制,所以同樣的 header 設定在工具反而能通過 另外,URL 本身也很容易撞到此問題,所以 URL 常常會需要 encode 來避免掉符號或中文字元造成的混亂 要解決這問題可從 request 端或伺服器端著手 ### request 端 先行 encode 秘文可以迴避此問題,但**千萬不要使用內建的 java.net.URLEncoder** 各種惱人問題例如:沒有斷行的地方當作斷行轉碼、空格變+號 這邊寧可使用 spring 的 encoder 或 android.net.Uri 據說也可以過 ### server 端 1. 在 tomcat 的 conf/server.xml 下面的Connector中添加放行屬性: ``` relaxedQueryChars = "{}|[],%" relaxedPathChars = "[]|{},%" ``` 需要放行什麼特殊字元就寫什麼 詳細添加位置在: ``` <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" relaxedQueryChars="[]|{}^&#x5c;&#x60;&quot;&lt;&gt;" relaxedPathChars="[]|{}^&#x5c;&#x60;&quot;&lt;&gt;" redirectPort="8443" /> ``` 2. 在 SpringBoot 中設定全域屬性 bean 其實跟方法1意思一樣,只是是透過 bean 物件來設定,如下 ``` @Bean public void allowRfc() { System.setProperty("tomcat.util.http.parser.HttpParser.requestTargetAllow", "|{}"); } ``` **使用 HttpClient 進行 POST 和 GET request 並進行上下載** https://blog.csdn.net/nupt123456789/article/details/42721003/ **簽名無法透過 HTTP 傳遞** https://www.jianshu.com/p/bfda250989ef **Tomcat9 以後,對 request header 的嚴格字元要求** https://www.cnblogs.com/onroad2019/p/11642096.html ###### tags: `Java`