中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

利用springboot怎么實現一個代理分發服務功能

發布時間:2021-01-25 16:20:06 來源:億速云 閱讀:645 作者:Leah 欄目:開發技術

這篇文章給大家介紹利用springboot怎么實現一個代理分發服務功能,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。

思路:客戶端發送請求,由代理服務端通過匹配請求內容,然后在作為代理去訪問真實的服務器,最后由真實的服務器將響應返回給代理,代理再返回給瀏覽器。

技術:說道反向代理,可能首先想到的就是nginx。不過在我們的需求中,對于轉發過程有更多需求:

  • 需要操作session,根據session的取值決定轉發行為

  • 需要修改Http報文,增加Header或是QueryString

第一點決定了我們的實現必定是基于Servlet的。springboot提供的ProxyServlet就可以滿足我們的要求,ProxyServlet直接繼承自HttpServlet,采用異步的方式調用內部服務器,因此效率上不會有什么問題,并且各種可重載的函數也提供了比較強大的定制機制。

實現過程

引入依賴

<dependency>
  <groupId>org.mitre.dsmiley.httpproxy</groupId>
  <artifactId>smiley-http-proxy-servlet</artifactId>
  <version>1.11</version>
</dependency>

構建一個配置類

@Configuration
public class ProxyServletConfiguration {
 
 private final static String REPORT_URL = "/newReport_proxy/*";
 
 @Bean
 public ServletRegistrationBean proxyServletRegistration() {
  List<String> list = new ArrayList<>();
  list.add(REPORT_URL); //如果需要匹配多個url則定義好放到list中即可
  ServletRegistrationBean registrationBean = new ServletRegistrationBean();
  registrationBean.setServlet(new ThreeProxyServlet());
  registrationBean.setUrlMappings(list);
  //設置默認網址以及參數
  Map<String, String> params = ImmutableMap.of("targetUri", "null", "log", "true");
  registrationBean.setInitParameters(params);
  return registrationBean;
 }
}

編寫代理邏輯

public class ThreeProxyServlet extends ProxyServlet {
 
 private static final long serialVersionUID = -9125871545605920837L;
 
 private final Logger logger = LoggerFactory.getLogger(ThreeProxyServlet.class);
 public String proxyHttpAddr;
 public String proxyName;
 
 private ResourceBundle bundle =null;
 @Override
 public void init() throws ServletException {
  bundle = ResourceBundle.getBundle("prop");
  super.init();
 }
 
 @Override
 protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws ServletException, IOException {
 
  // 初始切換路徑
  String requestURI = servletRequest.getRequestURI();
  proxyName = requestURI.split("/")[2];
  //根據name匹配域名到properties文件中獲取
  proxyHttpAddr = bundle.getString(proxyName);
 
  String url = proxyHttpAddr;
  if (servletRequest.getAttribute(ATTR_TARGET_URI) == null) {
   servletRequest.setAttribute(ATTR_TARGET_URI, url);
  }
 
  if (servletRequest.getAttribute(ATTR_TARGET_HOST) == null) {
   URL trueUrl = new URL(url);
   servletRequest.setAttribute(ATTR_TARGET_HOST, new HttpHost(trueUrl.getHost(), trueUrl.getPort(), trueUrl.getProtocol()));
  }
 
  String method = servletRequest.getMethod();
  // 替換多余路徑
  String proxyRequestUri = this.rewriteUrlFromRequest(servletRequest);
 
  Object proxyRequest;
  if (servletRequest.getHeader("Content-Length") == null && servletRequest.getHeader("Transfer-Encoding") == null) {
   proxyRequest = new BasicHttpRequest(method, proxyRequestUri);
  } else {
   proxyRequest = this.newProxyRequestWithEntity(method, proxyRequestUri, servletRequest);
  }
 
  this.copyRequestHeaders(servletRequest, (HttpRequest)proxyRequest);
  setXForwardedForHeader(servletRequest, (HttpRequest)proxyRequest);
  HttpResponse proxyResponse = null;
  try {
   proxyResponse = this.doExecute(servletRequest, servletResponse, (HttpRequest)proxyRequest);
   int statusCode = proxyResponse.getStatusLine().getStatusCode();
   servletResponse.setStatus(statusCode, proxyResponse.getStatusLine().getReasonPhrase());
   this.copyResponseHeaders(proxyResponse, servletRequest, servletResponse);
   if (statusCode == 304) {
    servletResponse.setIntHeader("Content-Length", 0);
   } else {
    this.copyResponseEntity(proxyResponse, servletResponse, (HttpRequest)proxyRequest, servletRequest);
   }
  } catch (Exception var11) {
   this.handleRequestException((HttpRequest)proxyRequest, var11);
  } finally {
   if (proxyResponse != null) {
    EntityUtils.consumeQuietly(proxyResponse.getEntity());
   }
 
  }
 }
 
 @Override
 protected HttpResponse doExecute(HttpServletRequest servletRequest, HttpServletResponse servletResponse, HttpRequest proxyRequest) throws IOException {
  HttpResponse response = null;
  // 攔截校驗 可自定義token過濾
  //String token = servletRequest.getHeader("ex_proxy_token");
  // 代理服務鑒權邏輯
  this.getAuthString(proxyName,servletRequest,proxyRequest);
  //執行代理轉發
  try {
   response = super.doExecute(servletRequest, servletResponse, proxyRequest);
  } catch (IOException e) {
   e.printStackTrace();
  }
  return response;
 }
}

增加一個properties配置文件

上邊的配置簡單介紹一下,對于/newReport_proxy/* 這樣的寫法,意思就是當你的請求路徑以newReport_proxy 開頭,比如http://localhost:8080/newReport_proxy/test/get1 這樣的路徑,它請求的真實路徑是https://www.baidu.com/test/get1 。主要就是將newReport_proxy 替換成對應的被代理路徑而已,* 的意思就是實際請求代理項目中接口的路徑,這種配置對get 、post 請求都有效。

遇到問題

按如上配置,在執行代理轉發的時候需要對轉發的代理服務器的接口進行鑒權,具體鑒權方案調用就是 "this.getAuthString(proxyName,servletRequest,proxyRequest);”這段代碼。代理服務的鑒權邏輯根據入參+token值之后按算法計算一個值,之后進行放到header中傳遞。那么這就遇到了一個問題,就是當前端采用requestBody的方式進行調用請求時服務1進行代理轉發的時候會出現錯誤:

利用springboot怎么實現一個代理分發服務功能

一直卡在執行 doExecute()方法。一頓操作debug后定位到一個點,也就是最后進行觸發進行執行代理服務調用的點:

利用springboot怎么實現一個代理分發服務功能

在上圖位置拋了異常,上圖中i的值為-1,說明這個sessionBuffer中沒有數據了,讀取不到了所以返回了-1。那么這個sessionBuffer是個什么東西呢?這個東西翻譯過來指的是會話輸入緩沖區,會阻塞連接。 與InputStream類相似,也提供讀取文本行的方法。也就是通過這個類將對應請求的數據流發送給目標服務。這個位置出錯說明這個要發送的數據流沒有了,那么在什么時候將請求的數據流信息給弄沒了呢?那就是我們加點鑒權邏輯,鑒權邏輯需要獲取requestBody中的參數,去該參數是從request對象中通過流讀取的。這個問題我們也見過通常情況下,HttpServletRequst 中的 body 內容只會讀取一次,但是可能某些情境下可能會讀取多次,由于 body 內容是以流的形式存在,所以第一次讀取完成后,第二次就無法讀取了,一個典型的場景就是 Filter 在校驗完成 body 的內容后,業務方法就無法繼續讀取流了,導致解析報錯。

最終實現

思路:用裝飾器來修飾一下 request,使其可以包裝讀取的內容,供多次讀取。其實spring boot提供了一個簡單的封裝器ContentCachingRequestWrapper,從源碼上看這個封裝器并不實用,沒有封裝http的底層流ServletInputStream信息,所以在這個場景下還是不能重復獲取對應的流信息。

參照ContentCachingRequestWrapper類實現一個stream緩存

public class CacheStreamHttpRequest extends HttpServletRequestWrapper {
 private static final Logger LOGGER = LoggerFactory.getLogger(CacheStreamHttpRequest.class);
 private final ByteArrayOutputStream cachedContent;
 private Map<String, String[]> cachedForm;
 
 @Nullable
 private ServletInputStream inputStream;
 
 public CacheStreamHttpRequest(HttpServletRequest request) {
  super(request);
  this.cachedContent = new ByteArrayOutputStream();
  this.cachedForm = new HashMap<>();
  cacheData();
 }
 
 @Override
 public ServletInputStream getInputStream() throws IOException {
  this.inputStream = new RepeatReadInputStream(cachedContent.toByteArray());
  return this.inputStream;
 }
 
 @Override
 public String getCharacterEncoding() {
  String enc = super.getCharacterEncoding();
  return (enc != null ? enc : WebUtils.DEFAULT_CHARACTER_ENCODING);
 }
 
 @Override
 public BufferedReader getReader() throws IOException {
   return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
 }
 
 @Override
 public String getParameter(String name) {
  String value = null;
  if (isFormPost()) {
   String[] values = cachedForm.get(name);
   if (null != values && values.length > 0) {
    value = values[0];
   }
  }
 
  if (StringUtils.isEmpty(value)) {
   value = super.getParameter(name);
  }
 
  return value;
 }
 
 @Override
 public Map<String, String[]> getParameterMap() {
  if (isFormPost() && !CollectionUtils.sizeIsEmpty(cachedForm)) {
   return cachedForm;
  }
 
  return super.getParameterMap();
 }
 
 @Override
 public Enumeration<String> getParameterNames() {
  if (isFormPost() && !CollectionUtils.sizeIsEmpty(cachedForm)) {
   return Collections.enumeration(cachedForm.keySet());
  }
 
  return super.getParameterNames();
 }
 
 @Override
 public String[] getParameterValues(String name) {
  if (isFormPost() && !CollectionUtils.sizeIsEmpty(cachedForm)) {
   return cachedForm.get(name);
  }
 
  return super.getParameterValues(name);
 }
 
 private void cacheData() {
  try {
   if (isFormPost()) {
    this.cachedForm = super.getParameterMap();
   } else {
    ServletInputStream inputStream = super.getInputStream();
    IOUtils.copy(inputStream, this.cachedContent);
   }
  } catch (IOException e) {
   LOGGER.warn("[RepeatReadHttpRequest:cacheData], error: {}", e.getMessage());
  }
 
 }
 
 private boolean isFormPost() {
  String contentType = getContentType();
  return (contentType != null &&
    (contentType.contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ||
      contentType.contains(MediaType.MULTIPART_FORM_DATA_VALUE)) &&
    HttpMethod.POST.matches(getMethod()));
 }
 
 private static class RepeatReadInputStream extends ServletInputStream {
  private final ByteArrayInputStream inputStream;
 
  public RepeatReadInputStream(byte[] bytes) {
   this.inputStream = new ByteArrayInputStream(bytes);
  }
 
  @Override
  public int read() throws IOException {
   return this.inputStream.read();
  }
 
  @Override
  public int readLine(byte[] b, int off, int len) throws IOException {
   return this.inputStream.read(b, off, len);
  }
 
  @Override
  public boolean isFinished() {
   return this.inputStream.available() == 0;
  }
 
  @Override
  public boolean isReady() {
   return true;
  }
 
  @Override
  public void setReadListener(ReadListener listener) {
 
  }
 }
}

如上類核心邏輯是通過cacheData() 方法進行將 request對象緩存,存儲到ByteArrayOutputStream類中,當在調用request對象獲取getInputStream()方法時從ByteArrayOutputStream類中寫回InputStream核心代碼:

 @Override
 public ServletInputStream getInputStream() throws IOException {
  this.inputStream = new RepeatReadInputStream(cachedContent.toByteArray());
  return this.inputStream;
 }

使用這個封裝后的request時需要配合Filter對原有的request進行替換,注冊Filter并在調用鏈中將原有的request換成該封裝類。代碼:

//chain.doFilter(request, response); 
//換掉原來的request對象 用new RepeatReadHttpRequest((HttpServletRequest) request) 因為后者流中由緩存攔截器httprequest替換 可重復獲取inputstream
chain.doFilter(new RepeatReadHttpRequest((HttpServletRequest) request), response);

關于利用springboot怎么實現一個代理分發服務功能就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

阿拉善左旗| 樟树市| 蓬莱市| 鹤庆县| 伊宁县| 钟祥市| 馆陶县| 石渠县| 双城市| 班玛县| 阜南县| 康定县| 赤峰市| 新昌县| 海阳市| 恩施市| 武宣县| 当雄县| 延安市| 汽车| 阜新市| 建宁县| 年辖:市辖区| 恩平市| 黔江区| 苍南县| 大石桥市| 行唐县| 且末县| 台前县| 濮阳市| 宝坻区| 彭山县| 锡林郭勒盟| 沧州市| 遵义县| 濮阳县| 岳普湖县| 凤凰县| 海原县| 苏尼特右旗|