前段时间在做微服务改造升级,公司使用的 springboot 版本比较老,是 1.5.9 的老版本,使用的 zuul 的版本如下:

<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
 

然后,还需要对接公司的日志平台、日志平台使用的是 springboot2.3.7 的主体架构,并且还需要使用日志平台的 parent 依赖;这没法啊,升级啊。然后 zuul 依赖升级为

<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
 

然后我们这边的文件上传微服务就出现了问题,只要是文件上传的请求都在网关这里获取不到请求的参数,准确的来说是解析不到请求。因为Content-Type: multipart/form-data类型的请求带的有文件格式的数据,所以需要做特殊解析处理。而项目里面也是做了处理。

 			RequestContext ctx = RequestContext.getCurrentContext();
            HttpServletRequest ctxRequest = ctx.getRequest();
            HttpServletRequestWrapper request = (HttpServletRequestWrapper) ctxRequest;
 

然后获取的 request 应该是可以解析的,但是还是无法获取到数据,然后打了断点进去后;发现 parameter 的 size 是 0。说明还是没有解析到参数。然后我就在网上找了这种方法。使用 Spring 封装好的类,这个类可以解析文件类型的请求。

public class ParameterFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
         String contentType = request.getContentType();
        if (contentType != null && contentType.contains("multipart/form-data")) {
            MultipartResolver resolver = new CommonsMultipartResolver(request.getSession().getServletContext());
            MultipartHttpServletRequest multipartRequest = resolver.resolveMultipart(request);
            // 将转化后的 request 放入过滤链中
            request = multipartRequest;
        }
        filterChain.doFilter(request,response);
    }
}
 

解析处理后,将请求往下处理,然后发现,果然可以获取参数了。
但是,在 zuul 网关转发到对应的微服务的时候,又出问题了。因为需要校验一些数据,然后又以同样的方法进行解析,然后这时解析完到接口的时候会发现,接口里面什么数据都没有,不管用什么方式都办法解析到数据。
         这是因为HttpServletRequets请求中的流只能被读取一次。
原因:
Java InputStream read方法内部有一个postion标志当前流读取到的位置,每读取一次,位置就会移动一次,如果读到最后,InputStream.read方法会返回 - 1,标志已经读取完了,如果想再次读取,可以调用inputstream.reset方法,position就会移动到上次调用 mark 的位置,mark 默认是 0,所以就能从头再读了。当然,能否 reset 是有条件的,它取决于markSupported,markSupported()方法返回是否可以mark/reset。我们再回头看request.getInputStream

request.getInputStream返回的值是ServletInputStream, 查看ServletInputStream源码发现,没有重写 reset 方法,所以查看InputStream源码发现marksupported 返回 false,并且 reset 方法,直接抛出异常。

public boolean markSupported() {
   return false;
 }
  
public synchronized void reset() throws IOException {
   throw new IOException("mark/reset not supported");
 }
 

综上所述,在request.getinputstream读取一次后 position 到了文件末尾,第二次就读取不到数据,由于无法reset(),所以,request.getinputstream只能读取一次。

对此,我们可以继承 HttpServletRequestWrapper,将请求体中的流 copy 一份,覆写 getInputStream() 和 getReader() 方法供外部使用。每次调用覆写后的 getInputStream() 方法都是从复制出来的二进制数组中进行获取,这个二进制数组在对象存在期间一直存在,这样就实现了流的重复读取。

//增强类
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
  //保存流
  private byte[] requestBody = null;
  
  public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
    super(request);
    requestBody = StreamUtils.copyToByteArray(request.getInputStream());
  }
  
  @Override
  public ServletInputStream getInputStream() throws IOException {
  
    final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
  
    return new ServletInputStream() {
  
      @Override
      public int read() throws IOException {
        return bais.read();
      }
  
      @Override
      public boolean isFinished() {
        return false;
      }
  
      @Override
      public boolean isReady() {
        return false;
      }
  
      @Override
      public void setReadListener(ReadListener readListener) {
  
      }
    };
  }
  
  @Override
  public BufferedReader getReader() throws IOException{
    return new BufferedReader(new InputStreamReader(getInputStream()));
  }
} 
 
//过滤器
@Component
@WebFilter
public class RequestSqlValidFilter implements Filter {
  private final Logger logger = LoggerFactory.getLogger(this.getClass());
  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
  }
  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    ServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper((HttpServletRequest)request);
    //请求参数合法,无sql注入
    chain.doFilter(requestWrapper, response);//requestWrapper中保存着供二次使用的请求数据
  }
  @Override
  public void destroy() {
  }
}
 

但是这样虽说是解决了问题,但是究竟是什么原因导致的呢?排查了好久发现一个配置关闭了。。。。。。。
spring.http.multipart.enabled=true这个配置设为 false 了。淦~~~ 然后所有问题都没了。

然后来说下网关这的跨域问题。

正常情况下,跨域是这样的:

  1. 微服务配置跨域 + zuul 不配置 = 有跨域问题
  2. 微服务配置 + zuul 配置 = 有跨域问题
  3. 微服务不配置 + zuul 不配置 = 有跨域问题
  4. 微服务不配置 + zuul 配置 = ok

然而云环境中每个服务自己有跨域解决方案,而网关需要做最外层的跨域解决方案. 如果服务已有跨域配置网关也有,会出现 * 多次配置问题。

Access-Control-Allow-Origin:"*,*"
也就是multiple Access-Control-Allow-Origin

!!!所以我们就要,微服务配置 + zuul 配置 = 解决跨域问题

zuul 的跨域忽略配置
使用 ZUUL 配置忽略头部信息 解决 cookie 跨域携带问题

zuul:
#需要忽略的头部信息,不在传播到其他服务
	sensitive-headers: Access-Control-Allow-Origin
	ignored-headers: Access-Control-Allow-Origin,H-APP-Id,Token,APPToken

微服务应用的跨域配置

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component  
public class CorsFilter implements Filter {  
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {  
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;  
       /* String curOrigin = request.getHeader("Origin");
        System.out.println("###跨域过滤器->当前访问来源->"+curOrigin+"###");   */
        response.setHeader("Access-Control-Allow-Origin", "*");  
        response.setHeader("Access-Control-Allow-Methods", "*");  
        response.setHeader("Access-Control-Max-Age", "3600");  
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with"); 
        chain.doFilter(req, res);  
    }  
    @Override
    public void init(FilterConfig filterConfig) {}  

    @Override
    public void destroy() {}  
}  

Zuul 网关配置
在启动类上打上@EnableZuulProxy注解

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
 
import java.util.ArrayList;
import java.util.List;
@Bean
public CorsFilter corsFilter() {
    final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    final CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true); // 允许cookies跨域
    config.addAllowedOrigin("*");// 允许向该服务器提交请求的URI,*表示全部允许。。这里尽量限制来源域,比如http://xxxx:8080 ,以降低安全风险。。
    config.addAllowedHeader("*");// 允许访问的头信息,*表示全部
    config.setMaxAge(18000L);// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
    config.addAllowedMethod("*");// 允许提交请求的方法,*表示全部允许,也可以单独设置GET、PUT等
    config.addAllowedMethod("HEAD");
    config.addAllowedMethod("GET");// 允许Get的请求方法
    config.addAllowedMethod("PUT");
    config.addAllowedMethod("POST");
    config.addAllowedMethod("DELETE");
    config.addAllowedMethod("PATCH");
    source.registerCorsConfiguration("/**", config);
    return new CorsFilter(source);
}

最后我这边出的问题也很搞笑,在很老的版本的时候 zuul 网关路由前缀可以是server.servlet-path,然后升级到 netflix-zuul 版本的网关这时候就不能用这个了,需要使用zuul.prefix。当初我这边报的是 404 + 跨域的问题,排查好久排查不出问题,然后我看到这个 404 说明 地址找不到,但是地址明明是有的,然后我就不得不怀疑到网关前缀这个地方了。<( ̄▽ ̄)/ 太难受了。