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问题没有作用)