软件编程
位置:首页>> 软件编程>> java编程>> springboot做代理分发服务+代理鉴权的实现过程

springboot做代理分发服务+代理鉴权的实现过程

作者:我是鳄鱼头领  发布时间:2021-06-28 03:22:14 

标签:springboot,服务,代理,鉴权

还原背景

大家都做过b-s架构的应用,也就是基于浏览器的软件应用。现在呢有个场景就是FE端也就是前端工程是前后端分离的,采用主流的前端框架VUE编写。服务端采用的是springBoot架构。

springboot做代理分发服务+代理鉴权的实现过程

现在有另外一个服务也需要与前端页面交互,但是由于之前前端与服务端1交互时有鉴权与登录体系逻辑控制以及分布式session存储逻辑都在服务1中,没有把认证流程放到网关。所以新服务与前端交互则不想再重复编写一套鉴权认证逻辑。最终想通过服务1进行一个代理把前端固定的请求转发到新加的服务2上。

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);

这样就解决了服务代理分发+代理服务鉴权一套逻辑。

来源:https://blog.csdn.net/zzming2012/article/details/112967032

0
投稿

猜你喜欢

手机版 软件编程 asp之家 www.aspxhome.com