还原背景

大家都做过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/*";@Beanpublic 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;@Overridepublic void init() throws ServletException {bundle = ResourceBundle.getBundle("prop");super.init();}@Overrideprotected 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());}}}@Overrideprotected 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;@Nullableprivate ServletInputStream inputStream;public CacheStreamHttpRequest(HttpServletRequest request) {super(request);this.cachedContent = new ByteArrayOutputStream();this.cachedForm = new HashMap<>();cacheData();}@Overridepublic ServletInputStream getInputStream() throws IOException {this.inputStream = new RepeatReadInputStream(cachedContent.toByteArray());return this.inputStream;}@Overridepublic String getCharacterEncoding() {String enc = super.getCharacterEncoding();return (enc != null ? enc : WebUtils.DEFAULT_CHARACTER_ENCODING);}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));}@Overridepublic 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;}@Overridepublic Map<String, String[]> getParameterMap() {if (isFormPost() && !CollectionUtils.sizeIsEmpty(cachedForm)) {return cachedForm;}return super.getParameterMap();}@Overridepublic Enumeration<String> getParameterNames() {if (isFormPost() && !CollectionUtils.sizeIsEmpty(cachedForm)) {return Collections.enumeration(cachedForm.keySet());}return super.getParameterNames();}@Overridepublic 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);}@Overridepublic int read() throws IOException {return this.inputStream.read();}@Overridepublic int readLine(byte[] b, int off, int len) throws IOException {return this.inputStream.read(b, off, len);}@Overridepublic boolean isFinished() {return this.inputStream.available() == 0;}@Overridepublic boolean isReady() {return true;}@Overridepublic void setReadListener(ReadListener listener) {}}
}

如上类核心逻辑是通过cacheData() 方法进行将 request对象缓存,存储到ByteArrayOutputStream类中,当在调用request对象获取getInputStream()方法时从ByteArrayOutputStream类中写回InputStream核心代码:

    @Overridepublic 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做代理分发服务+代理鉴权相关推荐

  1. 微服务网关鉴权——gateway使用、网关限流使用、用户密码加密、JWT鉴权

    文章目录 微服务网关鉴权 课程目标 1.微服务网关Gateway 1.1 微服务网关概述 1.2 微服务网关微服务搭建 1.3 微服务网关跨域 1.4 微服务网关过滤器 2 网关限流 2.1 思路分析 ...

  2. 详解比springSecurity和shiro更简单优雅的轻量级Sa-Token框架,比如登录认证,权限认证,单点登录,OAuth2.0,分布式Session会话,微服务网关鉴权

    文章目录 1. 技术选型 2. Sa-Token概述 2.1 简单介绍 2.2 登录认证 2.3 权限认证 3. 功能一览 4. Sa-Token使用 4.1 引入Sa-Token依赖 4.2 Sa- ...

  3. Kong 优雅实现微服务网关鉴权,登录场景落地实战篇

    目录 登录实现 B 端登录之后,浏览器存 cookie 登录代码实现细节,cookie设计 网关介绍 API 网关是什么 为什么需要网关 从技术角度来看,什么是Kong? 为什么使用 Kong Kon ...

  4. 微服务网关鉴权:gateway使用、网关限流使用、用户密码加密、JWT鉴权

    点击上方"芋道源码",选择"设为星标" 管她前浪,还是后浪? 能浪的浪,才是好浪! 每天 10:33 更新文章,每天掉亿点点头发... 源码精品专栏 原创 | ...

  5. 微服务网关鉴权:gateway使用、网关限流使用 用户密码加密 JWT鉴权

    目标 掌握微服务网关Gateway的系统搭建 掌握网关限流的实现 能够使用BCrypt实现对密码的加密与验证 了解加密算法 能够使用JWT实现微服务鉴权 1.微服务网关Gateway 1.1 微服务网 ...

  6. SpringBoot使用security和jwt进行鉴权设计

    一.使用security默认登录接口登录成功.生成token.返回前段 项目的结构如下: 1.引入jar包 <dependency><groupId>org.springfra ...

  7. 微服务认证鉴权-API网关

    认证:验证这个用户是谁 鉴权:用户有哪些资源权限(页面.按钮.超链接.接口.接口字段) 授权:为用户添加资源权限 方案:客户端Token(JWT) 流程: 1.用户登录发起认证请求,认证服务执行认证流 ...

  8. 微服务中鉴权方式OAtuh2与JWT的比较

    统一认证与授权是微服务架构的基础功能,微服务架构不同于单体应用的架构,认证和授权非常集中.当服务拆分之后,对各个微服务认证与授权变得非常分散,所以在微服务架构中,将集成统一认证与授权的功能,作为横切关 ...

  9. springcloud微服务体系(一)— 基于security和jwt实现认证及鉴权服务

    文章目录 需求 知识点讲解 方案 SpringSecurity 具体实现 业务流程 代码 认证服务 鉴权服务 配置 需求 1.RESTfull风格的鉴权服务(路线相同的情况下根据请求方式鉴别访问权限) ...

最新文章

  1. Centos6.5 rpm方式指定目录安装JDK
  2. docker-compose运行sentry
  3. 微信小游戏开发教程-2D游戏原理讲解
  4. mysql数据库导入后莫名丢失,oracle导入丢失数据库
  5. 范围元【2013 GDCPC】有为杯 广东ACM省赛小总结
  6. 乐鑫代理启明云端分享|基于ESP32-S2彩色触摸屏86面板方案
  7. VTK:可编程字形过滤器用法实战
  8. vue 指令 v-model
  9. 3月第一周中国五大顶级域名增6万 美国增1.8万
  10. 他写出了 Vue,却做不对这十道 Vue 笔试题
  11. C#常见错误解决方法
  12. android gps转换度分秒,如何将GPS数据转换为度分秒
  13. rad linux下安装mysql_Rad Hat Enterprise Linux 5.5上安装Oracle 11g R2
  14. win7 64位系统安装HP LaserJet 5100 PCL 6
  15. 2018-08-14 UnmarshalException: 意外的元素 (uri:, local:customer)
  16. 网络创业成功的7堂课(读书笔记)
  17. OSI 七层模型和TCP/IP模型及对应协议(详解)
  18. php极简wiki,Wiki.js初体验
  19. 积分电路和微分电路的工作原理
  20. c语言 topk算法,scala写算法-用小根堆解决topK

热门文章

  1. 【攻破html系列——第六天】表单元素
  2. java接口防抖_彻底弄懂节流和防抖
  3. python的a b是什么意思_python – `a,b = b,a b`和`a = b之间的区别是什么? b =斐波那契的b` [复制]...
  4. php从数组中搜索数据结构,php数组搜索值
  5. java毕业设计二手儿童闲置物品交易平台Mybatis+系统+数据库+调试部署
  6. 消防设施操作员考试真题、模拟练习题库(4)
  7. HDU4752 Polygon
  8. 抖音快手URL Scheme
  9. ASP.NET - ScriptManager 控件概述
  10. 五年Java程序员人生的点点滴滴