springboot做代理分發(fā)服務(wù)+代理鑒權(quán)的實現(xiàn)過程
還原背景
大家都做過b-s架構(gòu)的應(yīng)用,也就是基于瀏覽器的軟件應(yīng)用?,F(xiàn)在呢有個場景就是FE端也就是前端工程是前后端分離的,采用主流的前端框架VUE編寫。服務(wù)端采用的是springBoot架構(gòu)。

現(xiàn)在有另外一個服務(wù)也需要與前端頁面交互,但是由于之前前端與服務(wù)端1交互時有鑒權(quán)與登錄體系邏輯控制以及分布式session存儲邏輯都在服務(wù)1中,沒有把認證流程放到網(wǎng)關(guān)。所以新服務(wù)與前端交互則不想再重復(fù)編寫一套鑒權(quán)認證邏輯。最終想通過服務(wù)1進行一個代理把前端固定的請求轉(zhuǎn)發(fā)到新加的服務(wù)2上。

怎么實現(xiàn)
思路:客戶端發(fā)送請求,由代理服務(wù)端通過匹配請求內(nèi)容,然后在作為代理去訪問真實的服務(wù)器,最后由真實的服務(wù)器將響應(yīng)返回給代理,代理再返回給瀏覽器。
技術(shù):說道反向代理,可能首先想到的就是nginx。不過在我們的需求中,對于轉(zhuǎn)發(fā)過程有更多需求:
- 需要操作session,根據(jù)session的取值決定轉(zhuǎn)發(fā)行為
- 需要修改Http報文,增加Header或是QueryString
第一點決定了我們的實現(xiàn)必定是基于Servlet的。springboot提供的ProxyServlet就可以滿足我們的要求,ProxyServlet直接繼承自HttpServlet,采用異步的方式調(diào)用內(nèi)部服務(wù)器,因此效率上不會有什么問題,并且各種可重載的函數(shù)也提供了比較強大的定制機制。
實現(xiàn)過程
引入依賴
<dependency> <groupId>org.mitre.dsmiley.httpproxy</groupId> <artifactId>smiley-http-proxy-servlet</artifactId> <version>1.11</version> </dependency>
構(gòu)建一個配置類
@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);
//設(shè)置默認網(wǎng)址以及參數(shù)
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];
//根據(jù)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");
// 代理服務(wù)鑒權(quán)邏輯
this.getAuthString(proxyName,servletRequest,proxyRequest);
//執(zhí)行代理轉(zhuǎn)發(fā)
try {
response = super.doExecute(servletRequest, servletResponse, proxyRequest);
} catch (IOException e) {
e.printStackTrace();
}
return response;
}
}
增加一個properties配置文件
上邊的配置簡單介紹一下,對于/newReport_proxy/* 這樣的寫法,意思就是當(dāng)你的請求路徑以newReport_proxy 開頭,比如http://localhost:8080/newReport_proxy/test/get1 這樣的路徑,它請求的真實路徑是https://www.baidu.com/test/get1 。主要就是將newReport_proxy 替換成對應(yīng)的被代理路徑而已,* 的意思就是實際請求代理項目中接口的路徑,這種配置對get 、post 請求都有效。
遇到問題
按如上配置,在執(zhí)行代理轉(zhuǎn)發(fā)的時候需要對轉(zhuǎn)發(fā)的代理服務(wù)器的接口進行鑒權(quán),具體鑒權(quán)方案調(diào)用就是 "this.getAuthString(proxyName,servletRequest,proxyRequest);”這段代碼。代理服務(wù)的鑒權(quán)邏輯根據(jù)入?yún)?token值之后按算法計算一個值,之后進行放到header中傳遞。那么這就遇到了一個問題,就是當(dāng)前端采用requestBody的方式進行調(diào)用請求時服務(wù)1進行代理轉(zhuǎn)發(fā)的時候會出現(xiàn)錯誤:

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

在上圖位置拋了異常,上圖中i的值為-1,說明這個sessionBuffer中沒有數(shù)據(jù)了,讀取不到了所以返回了-1。那么這個sessionBuffer是個什么東西呢?這個東西翻譯過來指的是會話輸入緩沖區(qū),會阻塞連接。 與InputStream類相似,也提供讀取文本行的方法。也就是通過這個類將對應(yīng)請求的數(shù)據(jù)流發(fā)送給目標(biāo)服務(wù)。這個位置出錯說明這個要發(fā)送的數(shù)據(jù)流沒有了,那么在什么時候?qū)⒄埱蟮臄?shù)據(jù)流信息給弄沒了呢?那就是我們加點鑒權(quán)邏輯,鑒權(quán)邏輯需要獲取requestBody中的參數(shù),去該參數(shù)是從request對象中通過流讀取的。這個問題我們也見過通常情況下,HttpServletRequst 中的 body 內(nèi)容只會讀取一次,但是可能某些情境下可能會讀取多次,由于 body 內(nèi)容是以流的形式存在,所以第一次讀取完成后,第二次就無法讀取了,一個典型的場景就是 Filter 在校驗完成 body 的內(nèi)容后,業(yè)務(wù)方法就無法繼續(xù)讀取流了,導(dǎo)致解析報錯。
最終實現(xiàn)
思路:用裝飾器來修飾一下 request,使其可以包裝讀取的內(nèi)容,供多次讀取。其實spring boot提供了一個簡單的封裝器ContentCachingRequestWrapper,從源碼上看這個封裝器并不實用,沒有封裝http的底層流ServletInputStream信息,所以在這個場景下還是不能重復(fù)獲取對應(yīng)的流信息。
參照ContentCachingRequestWrapper類實現(xiàn)一個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類中,當(dāng)在調(diào)用request對象獲取getInputStream()方法時從ByteArrayOutputStream類中寫回InputStream核心代碼:
@Override
public ServletInputStream getInputStream() throws IOException {
this.inputStream = new RepeatReadInputStream(cachedContent.toByteArray());
return this.inputStream;
}
使用這個封裝后的request時需要配合Filter對原有的request進行替換,注冊Filter并在調(diào)用鏈中將原有的request換成該封裝類。代碼:
//chain.doFilter(request, response); //換掉原來的request對象 用new RepeatReadHttpRequest((HttpServletRequest) request) 因為后者流中由緩存攔截器httprequest替換 可重復(fù)獲取inputstream chain.doFilter(new RepeatReadHttpRequest((HttpServletRequest) request), response);
這樣就解決了服務(wù)代理分發(fā)+代理服務(wù)鑒權(quán)一套邏輯。
到此這篇關(guān)于springboot做代理分發(fā)服務(wù)+代理鑒權(quán)的文章就介紹到這了,更多相關(guān)springboot服務(wù)代理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
struts2中simple主題下<s:fieldError>標(biāo)簽?zāi)J樣式的移除方法
這篇文章主要給大家介紹了關(guān)于struts2中simple主題下<s:fieldError>標(biāo)簽?zāi)J樣式的移除方法,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-10-10
Java使用iTextPDF生成PDF文件的實現(xiàn)方法
這篇文章主要介紹了Java使用iTextPDF生成PDF文件的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02
解讀CommandLineRunner或者ApplicationRunner接口
這篇文章主要介紹了解讀CommandLineRunner或者ApplicationRunner接口的使用,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02
基于JSON實現(xiàn)傳輸byte數(shù)組過程解析
這篇文章主要介紹了基于JSON實現(xiàn)傳輸byte數(shù)組過程解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-06-06
JDK源碼之線程并發(fā)協(xié)調(diào)神器CountDownLatch和CyclicBarrier詳解
我一直認為程序是對于現(xiàn)實世界的邏輯描述,而在現(xiàn)實世界中很多事情都需要各方協(xié)調(diào)合作才能完成,就好比完成一個平臺的交付不可能只靠一個人,而需要研發(fā)、測試、產(chǎn)品以及項目經(jīng)理等不同角色人員進行通力合作才能完成最終的交付2022-02-02
關(guān)于@PostConstruct、afterPropertiesSet和init-method的執(zhí)行順序
這篇文章主要介紹了關(guān)于@PostConstruct、afterPropertiesSet和init-method的執(zhí)行順序,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09

