Reduction of background
You’ve all done b-S architecture applications, which are browser-based software applications. Now, there is a scenario where the FE end, the front-end engineering is separated from the front end, and the mainstream front-end framework, VUE, is written. The server uses the springBoot architecture.Now there is another service that also needs to interact with the front-end page, but because the authentication and login system logic control and distributed session storage logic are in service 1 when the front-end interacts with server 1, the authentication process is not in the gateway. So new services interacting with the front end don’t want to write a set of authentication logic again. Eventually you want to have a proxy through service 1 to forward the fixed front-end requests to the newly added service 2.
How to implement
The client sends the request, the proxy server matches the request content, and then accesses the real server as the proxy. Finally, the real server returns the response to the proxy, and the proxy returns to the browser. Technology: When it comes to reverse proxies, nginx is probably the first thing that comes to mind. However, in our requirements, there are more requirements for the forwarding process:
- You need to operate the session and determine the forwarding behavior according to the value of the session
- You need to modify the Http packet by adding either a Header or a QueryString
The first point determines that our implementation must be servlet-based. ProxyServlet provided by SpringBoot can meet our requirements. ProxyServlet directly inherits from HttpServlet and calls the internal server in an asynchronous way, so there will be no problems in efficiency, and a variety of functions that can be overloaded also provide a powerful customization mechanism.
The implementation process
- Introduction of depend on
<dependency> <groupId>org.mitre.dsmiley.httpproxy</groupId> <artifactId>smiley-http-proxy-servlet</artifactId> The < version > 1.11 < / version > < / dependency >Copy the code
- Build a configuration class
@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); 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; }}Copy the code
- Writing agent logic
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 {/ / initial switching path String requestURI = servletRequest. GetRequestURI (); proxyName = requestURI.split("/")[2]; ProxyHttpAddr = bundle.getString(proxyName); // Obtain 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(); / / replace the extra path 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; //String token = servletrequest.getheader ("ex_proxy_token"); //String token = servletrequest.getheader ("ex_proxy_token"); / / proxy service authentication logic enclosing getAuthString (proxyName servletRequest, proxyRequest); Try {response = super.doexecute (servletRequest, servletResponse, proxyRequest); } catch (IOException e) { e.printStackTrace(); } return response; }}Copy the code
- Add a Properties configuration file
newReport_proxy = https://www.baidu.com
For /newReport_proxy/*, when your request path starts with newReport_proxy, Such as http://localhost:8080/newReport_proxy/test/get1 this path, it requests the real path of www.baidu.com/test/get1. NewReport_proxy is replaced with the corresponding proxy path, * means the actual request proxy interface path, this configuration is valid for both GET and POST requests.
Have a problem
According to the above configuration, at the time of execution agent forward need to forwarding agent server interface for authentication, the authentication scheme is called “enclosing getAuthString (proxyName servletRequest, proxyRequest);” This code. The authentication logic of the proxy service calculates a value based on the input parameter +token value, and then passes it in the header. RequestBody is a requestBody request that can be forwarded by service 1 as a proxy.The doExecute() method has been stuck. After an operation debug, locate a point, which is the last point to trigger the execution of the proxy service call:The sessionBuffer has no data left and cannot be read, so -1 is returned. So what is this sessionBuffer? This translates to a session input buffer that blocks the connection. Similar to the InputStream class, it provides a method to read lines of text. This class is used to send the data flow corresponding to the request to the target service. This location error indicates that the data flow to be sent is not available, so when is the data flow information requested lost? That is, we add authentication logic, which takes the parameters in the requestBody that are read through the stream from the request object. We’ve seen this before and usually, The body content in HttpServletRequst is read only once, but in some cases it can be read more than once. Because the body content exists in the form of a stream, after the first read, it cannot be read again. A typical scenario is when the Filter is checking the body The business method is unable to continue reading the stream, resulting in a parsing error.
Finally realize
Use a decorator to decorate the request so that it can wrap the content it reads multiple times. Actually spring boot offers a simple wrapper ContentCachingRequestWrapper, look from the source the wrapper is not practical, not encapsulated HTTP ServletInputStream bottom flow information, Therefore, in this scenario, the corresponding flow information cannot be obtained repeatedly.
- With reference to ContentCachingRequestWrapper class implements a stream buffer
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) { } } }Copy the code
The core logic of the above class is to cache the request object through the cacheData() method, and store it in the ByteArrayOutputStream class, Write back the InputStream core code from the ByteArrayOutputStream class when calling the getInputStream() method on the Request object:
@Override
public ServletInputStream getInputStream() throws IOException {
this.inputStream = new RepeatReadInputStream(cachedContent.toByteArray());
return this.inputStream;
}
Copy the code
When using this encapsulated request, it is necessary to replace the original request with the Filter, register the Filter and replace the original request with this encapsulated class in the invocation chain. Code:
//chain.doFilter(request, response); // Replace the original request object with new RepeatReadHttpRequest((HttpServletRequest) request) because the latter stream is replaced by the cache interceptor Httprequest to obtain the repeatable InputStream chain.doFilter(new RepeatReadHttpRequest((HttpServletRequest) request), response);Copy the code
This solves the logic of service proxy distribution + proxy service authentication.