前段时间在做微服务改造升级,公司使用的 springboot 版本比较老,是 1.5.9 的老版本,使用的 zuul 的版本如下:
然后,还需要对接公司的日志平台、日志平台使用的是 springboot2.3.7 的主体架构,并且还需要使用日志平台的 parent 依赖;这没法啊,升级啊。然后 zuul 依赖升级为
然后我们这边的文件上传微服务就出现了问题,只要是文件上传的请求都在网关这里获取不到请求的参数,准确的来说是解析不到请求。因为Content-Type: multipart/form-data
类型的请求带的有文件格式的数据,所以需要做特殊解析处理。而项目里面也是做了处理。
然后获取的 request 应该是可以解析的,但是还是无法获取到数据,然后打了断点进去后;发现 parameter 的 size 是 0。说明还是没有解析到参数。然后我就在网上找了这种方法。使用 Spring 封装好的类,这个类可以解析文件类型的请求。
解析处理后,将请求往下处理,然后发现,果然可以获取参数了。
但是,在 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 方法,直接抛出异常。
综上所述,在request.getinputstream
读取一次后 position 到了文件末尾,第二次就读取不到数据,由于无法reset()
,所以,request.getinputstream
只能读取一次。
对此,我们可以继承 HttpServletRequestWrapper,将请求体中的流 copy 一份,覆写 getInputStream() 和 getReader() 方法供外部使用。每次调用覆写后的 getInputStream() 方法都是从复制出来的二进制数组中进行获取,这个二进制数组在对象存在期间一直存在,这样就实现了流的重复读取。
但是这样虽说是解决了问题,但是究竟是什么原因导致的呢?排查了好久发现一个配置关闭了。。。。。。。
spring.http.multipart.enabled=true
这个配置设为 false 了。淦~~~ 然后所有问题都没了。
然后来说下网关这的跨域问题。
正常情况下,跨域是这样的:
- 微服务配置跨域 + zuul 不配置 = 有跨域问题
- 微服务配置 + zuul 配置 = 有跨域问题
- 微服务不配置 + zuul 不配置 = 有跨域问题
- 微服务不配置 + 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 说明 地址找不到,但是地址明明是有的,然后我就不得不怀疑到网关前缀这个地方了。<( ̄▽ ̄)/ 太难受了。