現網中有個應用A,之前一直是請求透傳訪問的,最近從安全方面考慮將該A應用不直接暴露給客戶端訪問,而是從有一定安全校驗機制的應用B做訪問入口,由B的後端將HTTP請求中轉到A,再將A的響應通過B輸出到客戶端。這種方案有兩個好處,1.可以利用應用B已有的安全校驗機制,而不需要應用A再複製一份安全校驗。2.原來在客戶端需要同時訪問應用A和應用B,這就涉及到瀏覽器的同源策略的安全性問題,所以在配置上必須讓應用A和應用B在同一個域名之下,經過這種改造就不存在這種問題了。
看起來功能實現很簡單,客戶端訪問應用B,應用B的後端用apache的httpclient訪問應用A,再將應用A的響應寫入B應用的響應中。HttpClient調用一般步驟:構造請求方法(HttpPost),構造請求參數內容,將請求參數塞入請求方法對象,發送請求,解析響應。代碼如下:
Java代碼 下載
HttpClient httpclient = new DefaultHttpClient();
try {
//new一個HttpPost
HttpPost httppost = new HttpPost(url);
if (paramMap != null) {
//構造NameValuePair的內容塞到HttpPost中
List<NameValuePair> formparams = new ArrayList<>();// 用於存放請求參數
for (Map.Entry<String, Object> entry : paramMap.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (value instanceof String) {
formparams.add(new BasicNameValuePair(key, String.valueOf(value)));
} else if (value instanceof List) {
List<Object> objectList = (List) value;
if (CollectionUtils.isNotEmpty(objectList)) {
for (Object object : objectList) {
formparams.add(new BasicNameValuePair(key, String.valueOf(object)));
}
}
}else if(value instanceof Number){
formparams.add(new BasicNameValuePair(key, String.valueOf(value)));
}else{
throw new Exception("不支持該類型");
}
}
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, encode);
httppost.setEntity(entity);
}
httppost.setHeader("Connection", "close");
HttpParams params = httpclient.getParams();
HttpConnectionParams.setConnectionTimeout(params, connectiontimeout);
HttpConnectionParams.setSoTimeout(params, readtimeout);
//發送post請求
HttpResponse httpResponse = httpclient.execute(httppost);
//解析響應內容,將輸出流寫入響應
if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
HttpEntity httpEntity = httpResponse.getEntity();
httpEntity.writeTo(response.getOutputStream());
}
EntityUtils.consume(httpResponse.getEntity());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (httpclient != null) {
httpclient.getConnectionManager().shutdown();
}
}
就是這麼簡單一需求還是有坑,測試發現原來的導出文件請求通過這種流中轉之後文件名不顯示了。
這種情況一看就知道是響應頭中丟失了信息,通過HttpResponse能夠獲取到所有A響應的的Header,取出來塞到B的響應中不就行了?代碼如下:
Java代碼 下載
HttpResponse httpResponse = httpclient.execute(httppost);// 發送post 請求
if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
HttpEntity httpEntity = httpResponse.getEntity();
httpEntity.writeTo(response.getOutputStream());
if (httpResponse.getFirstHeader("Content-Disposition") != null) {
//文件導出時在響應中添加文件內容的響應header
response.setHeader("Content-Disposition", httpResponse.getFirstHeader("Content-Disposition").getValue());
}
}
結果發現這樣寫卻並沒有放到響應頭報文中。
跟蹤源碼,調用response.setHeader方法最終會執行到具體web容器的setHeader方法,我本地跑的tomcat,所以最終執行的是org.apache.catalina.connector.ResponseFacade類的setHeader方法:
Java代碼
@Override
public void setHeader(String name, String value) {
if (isCommitted()) {
return;
}
response.setHeader(name, value);
}
原來在設置header之前會調用一下isCommitted方法判斷要輸出的內容是否已經提交:
Java代碼 下載
@Override
public boolean isCommitted() {
if (response == null) {
throw new IllegalStateException(
sm.getString("responseFacade.nullResponse"));
}
return (response.isAppCommitted());
}
Java代碼
/**
* Application commit flag accessor.
*/
public boolean isAppCommitted() {
return (this.appCommitted || isCommitted() || isSuspended()
|| ((getContentLength() > 0)
&& (getContentWritten() >= getContentLength())));
}
所以設置響應頭的代碼要在設置響應的輸出流之前調用,否則無效:
Java代碼 下載
if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
String headerName;
for (Header header : httpResponse.getAllHeaders()) {
headerName = header.getName();
switch (headerName){
//這裏根據需要自己添加需要輸出的響應頭
case "Content-Disposition":
case "Content-Type":
response.setHeader(headerName, header.getValue());
break;
}
}
HttpEntity httpEntity = httpResponse.getEntity();
httpEntity.writeTo(response.getOutputStream());
}