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

还原背景

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

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

怎么实现

思路:客户端发送请求,由代理服务端通过匹配请求内容,然后在作为代理去访问真实的服务器,最后由真实的服务器将响应返回给代理,代理再返回给浏览器。

技术:说道反向代理,可能首先想到的就是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进行代理转发的时候会出现错误:

一直卡在执行 doExecute()方法。一顿操作debug后定位到一个点,也就是最后进行触发进行执行代理服务调用的点:

在上图位置抛了异常,上图中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做代理分发服务+代理鉴权的文章就介绍到这了,更多相关springboot服务代理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • SpringBoot集成SpringSecurity和JWT做登陆鉴权的实现

    废话 目前流行的前后端分离让Java程序员可以更加专注的做好后台业务逻辑的功能实现,提供如返回Json格式的数据接口就可以.SpringBoot的易用性和对其他框架的高度集成,用来快速开发一个小型应用是最佳的选择. 一套前后端分离的后台项目,刚开始就要面对的就是登陆和授权的问题.这里提供一套方案供大家参考. 主要看点: 登陆后获取token,根据token来请求资源 根据用户角色来确定对资源的访问权限 统一异常处理 返回标准的Json格式数据 正文 首先是pom文件: <dependencies

  • springboot以FTP方式上传文件到远程服务器

    一.html代码   <div class="layui-form-item"> <label class="layui-form-label">上传附件:</label> <div class="layui-input-block doc-litpic"> <button type="button" name="avatar" class="

  • 服务器使用Nginx部署Springboot项目的详细教程(jar包)

    1,将java项目打成jar包 这里我用到的是maven工具 这里有两个项目,打包完成后一个为demo.jar,另一个为jst.jar 2.准备工具 1.服务器 2.域名(注:经过备案) 3.Xshell用于连接服务器 4.WinScp(注:视图工具,用于传输jar) 3.将jar包传入服务器 直接拖动即可 3.使用Xshell运行jar包 注:(服务器的java环境以及maven环境,各位请自行配置,这里不做描述.) cd到jar包路径下执行:nohup java -jar demo.jar

  • SpringBoot集成Spring Security用JWT令牌实现登录和鉴权的方法

    最近在做项目的过程中 需要用JWT做登录和鉴权 查了很多资料 都不甚详细 有的是需要在application.yml里进行jwt的配置 但我在导包后并没有相应的配置项 因而并不适用 在踩过很多坑之后 稍微整理了一下 做个笔记 一.概念 1.什么是JWT Json Web Token (JWT)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519) 该token被设计为紧凑且安全的 特别适用于分布式站点的单点登录(SSO)场景 随着JWT的出现 使得校验方式更加简单便

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

    还原背景 大家都做过b-s架构的应用,也就是基于浏览器的软件应用.现在呢有个场景就是FE端也就是前端工程是前后端分离的,采用主流的前端框架VUE编写.服务端采用的是springBoot架构. 现在有另外一个服务也需要与前端页面交互,但是由于之前前端与服务端1交互时有鉴权与登录体系逻辑控制以及分布式session存储逻辑都在服务1中,没有把认证流程放到网关.所以新服务与前端交互则不想再重复编写一套鉴权认证逻辑.最终想通过服务1进行一个代理把前端固定的请求转发到新加的服务2上. 怎么实现 思路:客户

  • SpringBoot使用Filter实现签名认证鉴权的示例代码

    情景说明 鉴权,有很多方案,如:SpringSecurity.Shiro.拦截器.过滤器等等.如果只是对一些URL进行认证鉴权的话,我们完 全没必要引入SpringSecurity或Shiro等框架,使用拦截器或过滤器就足以实现需求.         本文介绍如何使用过滤器Filter实现URL签名认证鉴权. 本人测试软硬件环境:Windows10.Eclipse.SpringBoot.JDK1.8 准备工作 第一步:在pom.xml中引入相关依赖 <dependencies> <dep

  • 如何在SpringBoot中使用Spring-AOP实现接口鉴权

    目录 面向切面编程 AOP的底层原理实现 AOP的相关术语 相关注解以及切入点表达式 实现接口鉴权 1. 配置yml文件 2. 读取账密配置 3.编写接口鉴权方法 4. 编写AOP 5.编写接口测试 面向切面编程 面向切面编程,可以将与业务无关但是需要被各个业务模块共同调用的逻辑抽取出来,以切面的方式切入到代码中,从而降低系统中代码的耦合度,减少重复的代码. Spring AOP是通过预编译方式和运行期间动态代理实现程序面向切面编程 AOP的底层原理实现 AOP底层使用动态代理完成需求,为需要增

  • springboot中shiro使用自定义注解屏蔽接口鉴权实现

    目录 传统做法 使用自定义注解屏蔽接口鉴权 拓展内容:关于spring中的派生注解 传统做法 spring boot整合shiro后,如果某些接口需要屏蔽鉴权的话(比如登录)接口,我们一般会这么做: @Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilterFactoryBean(org.apache.shiro.mgt.SecurityManager securityManager) { ShiroFil

  • Spring Cloud下实现用户鉴权的方案

    目录 一.整体架构 二.实现步骤 三.其它问题 四.完整代码 Java下常用的安全框架主要有Spring Security和shiro,都可提供非常强大的功能,但学习成本较高.在微服务下鉴权多多少少都会对服务有一定的入侵性. 为了降低依赖,减少入侵,让鉴权功能相对应用服务透明,我们采用网关拦截资源请求的方式进行鉴权. 一.整体架构 用户鉴权模块位于API GateWay服务中,所有的API资源请求都需要从此通过. 做身份认证,通过则缓存用户权限数据,不通过返回401 做用户鉴权,比对当前访问资源

  • koa2服务端使用jwt进行鉴权及路由权限分发的流程分析

    大体思路 后端书写REST api时,有一些api是非常敏感的,比如获取用户个人信息,查看所有用户列表,修改密码等.如果不对这些api进行保护,那么别人就可以很容易地获取并调用这些 api 进行操作. 所以对于一些api,在调用之前,我们在服务端必须先对操作者进行"身份认证",这就是所谓的鉴权. Json Web Token 简称为 JWT,它定义了一种通信双方之间以 JSON 对象的形式安全传递信息的方法.JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名,复杂度较

  • 手把手教你用Java实现一套简单的鉴权服务

    前言 时遇JavaEE作业,题目要求写个简单web登录程序,按照老师的意思是用servlet.jsp和jdbc完成.本着要么不做,要做就要做好的原则,我开始着手完成此次作业(其实也是写实训作业的用户鉴权部分),而之前写项目的时候也有相关经验,这次正好能派上用场. 一.何为鉴权服务 引用百度百科的话说 鉴权(authentication)是指验证用户是否拥有访问系统的权利. 鉴权包括两个方面: 用户鉴权,网络对用户进行鉴权,防止非法用户占用网络资源. 网络鉴权,用户对网络进行鉴权,防止用户接入了非

  • Springboot实现VNC的反向代理功能

    背景 ​ 用户需要通过前端HTML页面的noVNC(noVNC是什么?)客户端连接底层VNC Server服务端,为了防止VNC Server的IP暴露,因此需要做一层代理.正常情况下使用Nginx.Apache等都可以搞定,但是由于项目架构的一些问题,暂时不能再加一台反向代理服务器,所以决定写一个单独的模块实现反向代理的功能. ​ 在网上和Github上找了一下,使用了HTTP-Proxy-Servlet,引入该依赖搭建一个Spring Boot项目. 搭建 引入代理的依赖 <dependen

随机推荐