0.背景
手机APP一个页面请求一个后台看不到数据,本地调试后发现有个跨域问题,然后整个问题一边排查一边修改,经历1.5天后终于找到问题所在,写这篇文章简单总结一下。
技术栈:
后台:springMVC+mybatis
前台:angular
APP:ionic
架构:后台有两个,我们假设分布是后台A,后台B。手机APP访问后台A时,一切正常,手机APP访问后台B时出现跨域报错:
Response to preflight request doesn’t pass access control check: It does not have HTTP ok status.
1.解决方法—前端
一般跨域问题都改后端,但是为了排查问题也尝试去改了改前端。
1.在前端请求时加入请求头 Access-Control-Allow-Origin。经过测试没有用
2. 设置代理地址,比如angular项目有proxy.config.json,我们可以参考下面这样设置:
{ "/": { "target": "http://127.0.0.1:8080/" } }
上面就表示前台把所有的请求都转发到127.0.0.1:8080。但是这个方法明显也不适合我,第一是用的手机APP,用ionic,ionic.config.json当中没有配置代理的情况下访问A和B这两个后台只有B会出现跨域问题,说明不是这个问题。
2.测试后台
从上面基本可以断定不是前台的问题,大概率是后台问题,于是我在我在本地启动了一个空白网站,地址是:http://192.168.1.103:8096,浏览器打开这个网站,然后打开F12,参考下面代码粘贴上去:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://127.0.0.1:8080/xxx'); xhr.setRequestHeader("Authorization",'123'); xhr.send(null); xhr.onload = function(e) { var xhr = e.target; console.log(xhr.responseText); }
然后回车测试后台。
A.修改请求参数
一开始以为上面这样的代码请求头会有问题,于是给上面的请求头加上了Access-Control-Allow-Origin。变成下面这样:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://127.0.0.1:8080/xxx'); xhr.setRequestHeader("Authorization",'123'); xhr.setRequestHeader("Access-Control-Allow-Origin",'*'); xhr.send(null); xhr.onload = function(e) { var xhr = e.target; console.log(xhr.responseText); }
结果报错:
Request header field access-control-allow-origin is not allowed by Access-Control-Allow-Headers in preflight response.
B.修改Filter
网络上大部分的文章都是修改这个地方,这个文件定义在web.xml中,
<filter> <filter-name>CORSFilter</filter-name> <filter-class>com.xinhai.cms.controller.CorsFilter</filter-class> </filter> <filter-mapping> <filter-name>CORSFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
然后参考下面的写法:
import org.apache.commons.lang.StringUtils; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class CorsFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; HttpServletRequest request = (HttpServletRequest) servletRequest; response.setHeader("Access-Control-Allow-Origin", "*"); if (StringUtils.equalsIgnoreCase(request.getMethod(), "OPTIONS")) { response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept,X-Requested-With"); } filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy() {} }
网上的很多文章基本都说增加 Access-Control-Allow-Origin 这个。
修改后,对我的问题并没有效果,还是报错。
C.使用注解
一些文章说是增加@CrossOrigion注解解决跨域问题,要求是spring 4.2以上的版本比如下面这样:
直接写在类上
@CrossOrigin(origins="*",maxAge=3600) @RestController public class LoginController {}
也可以写在方法上
@CrossOrigin("http://localhost:5173") @GetMapping("/test") public String test(){ return "test"; }
经过测试,对我的问题并没有效果
D.实现WebMvcConfigurer接口
@Configuration public class WebMvcOriginConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**")// 对所有请求进行跨域 .allowedOriginPatterns("http://localhost:5173") .maxAge(-1) .allowedMethods("*"); } }
经过测试,对我的问题没有效果。
E.排查问题
上面大部分主流的方法都试了一遍,均没有效果,于是开始一点一点排查问题,先在CorsFilter中打上断点,发现根本不会进入断点。于是把所有的Filter全部打上断点开始观察,发现没有进入CorsFilter,进入了XssFilter,在单步运行后,代码不能继续往下执行,但是可以从调试信息中可以找到HttpServletRequest从中中看到前台发过来的请求,这表示前台的请求发送到后台了,但是在CorsFilter之前在某个地方被拦截或者被修改了。
而且,前台发生请求时,会发送两次请求,一次是options,options通过后发送get或者post请求。
在此想到web.xml有多个过滤器,应该一层一层的分析,看看先执行哪个。
此时,刚好看到一篇文章,说是项目中使用了Spring Security,参考这篇文章:
https://blog.csdn.net/java0506/article/details/120620447文章中说是要 新增Spring Security配置,改成下面这样:
@Override protected void configure(HttpSecurity http) throws Exception { http.cors(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) .sessionManagement(item -> item.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeRequests(req -> req //非普通请求(比如请求新增了自定义头部信息,比如Jwt头),会发送预检Option请求,这里直接让他通过 .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() .antMatchers("/user/biz/login").permitAll() .anyRequest().authenticated()) .addFilterBefore(jwtSecurityFilter, UsernamePasswordAuthenticationFilter.class) .httpBasic(AbstractHttpConfigurer::disable) ; }
本来以为找到了解决办法,结果发现我们的项目中早就重写里面的DelagetingFilterProxy,而且A和B都用的同一个Filter。既然A使用这个Filter没有跨域问题,那么同理B也不会因为这个产生跨域问题。
继续排查问题….
还是查找web.xml中的配置问题,终于找到了一个配置,如下:
<security-constraint> <web-resource-collection> <web-resource-name>NoAccess</web-resource-name> <url-pattern>/*</url-pattern> <http-method>OPTIONS</http-method> <http-method>TRACE</http-method> </web-resource-collection> <auth-constraint/> </security-constraint>
security-constraint一般是用来授权和身份验证,那么上面这段配置是什么意思呢?
上面这段配置表示HTTP OPTIONS 和TRACE方法只能由经过身份验证的用户使用。
其中:
OPTIONS方法用来描述了目标资源的通信选项,会返回服务器支持预定义URL的HTTP策略。 TRACE方法用于沿着目标资源的路径执行消息环回测试;它回应收到的请求,以便客户可以看到中间服务器进行了哪些(假设任何)进度或增量。
豁然开朗,因为发送请求时,会先发送一个OPTIONS,而这个配置,因为没有登录的原因,禁止使用,而前台的OPTIONS请求没有得到相应,就不能继续发送其他类似GET或者POST请求,这样就会出现跨域问题。
解决办法,要么修改或注释掉这个配置(对本文APP适用)
要么登录成功后再使用(对本文APP问题不适用)
3.后记
实际测试中,在跨域时发现请求localhost:8080 和127.0.0.1:8080 的报错不一样,从192.168.1.103:8096请求localhost:808时会报错:
No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
后来发现这个是后台自己写的一个Filter导致的。
另外,测试过程中发现一个报错:
The request client is not a secure context and the resource is in more-private address space local
解决办法是打开chrome浏览器,输入 chrome://flags, 找到Bolck insecure private network requests 这个选项,将它改为Disabled。
(这个对于本文APP问题没有作用)