前言:

在公司个项目开发中,遇到了一个问题,解决这个问题的过程很艰难,记录下来,不知是否有和我一样遇到这个问题的朋友。

首先说一下开发环境以及项目情况吧,开发工具主要为IDEAHBuilder X,后端项目基于SpringBoot搭建,是一个前后端没有分离的项目,现在有个需求,需要在项目中添加接口,供钉钉端H5微应用调用,开发过程基本都没有遇到太多的问题,从第一个问题说起吧。

在后端接口写得差不多的时候,我开始把接口接入H5中,在HBuilder X工具的内置浏览器中运行没有任何问题,当我用Chrome浏览器调试时,报错了,错误信息就是提示跨域问题,因为前后端是分开部署的,也就是说前端页面需要启动一个服务,后端接口又需要启动另一个服务,这就一定会出现跨域问题,跨域的条件大家可自行Google,网上描述很多,在此就不赘述。于是开始解决这个问题,我的印象中,跨域是前后端都可以解决的,但也没去深究过到底前端解决好还是后端解决好,我开始查找前端解决方案,因为我不想修改后端的任何一个接口或配置了,因为是使用HBuilder X开发H5,网上也有很多文章来叙述在Hbuilder X中如何解决跨域的问题,在Hbuilder X中解决跨域的方式为,在manifest.json文件中添加或修改如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"h5" : {
"devServer" : {
"https" : false,
// 这个端口号是你前端项目运行的端口号
"port" : 8080,
"disableHostCheck" : true,
"proxy" : {
// 这个是需要代理的请求前缀
"/api" : {
// 这里是后端数据请求接口基础地址
"target" : "http://192.168.1.3:8087/api",
"changeOrigin" : true,
"secure" : false,
"pathRewrite" : {
"^/api" : ""
}
}
}
}
}

添加上述代码后,在页面中发起请求,只需要以api开头发起请求即可,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
this.$http.post('/api/auth/login', {
username: this.username,
password: this.password
}).then(res => {
uni.showModal({
title: '数据',
content: JSON.stringify(res)
})
}).catch(err => {
uni.showModal({
title: '错误提示',
content: JSON.stringify(err)
})
})

上述代码中,最终的请求路径为http://192.168.1.3:8087/api/auth/login此过程一切都很顺利,于是我将所有后端接口都对接到了前端页面,本以为美滋滋的把前端打包发布就完事了,没想到,打包以后,又出现了跨域问题,经过研究,发现在manifest.json中配置前端代理来解决跨域,只能在开发过程中有效,打包后就不起作用了,因为我发现,打包后的代码中,根本没有http://192.168.1.3:8087/api这个地址,也就是说,这段代码没有被打包,因此这种代理方式只能用于开发过程中,此方法被否定。

折腾了很久,最终还是没有找到可以在前端解决跨域并且打包后还有效的方案(如果哪位大佬有此方案还请告知)。于是乎,不得不把解决跨域的问题放到后端处理,因为后端项目基于SpringBoot搭建,于是使用了如下代码来配置跨域问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class GlobalCorsConfig {

@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.setAllowCredentials(true);
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.addExposedHeader("*");
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
return new CorsFilter(configSource);
}
}

配置完成,重启项目等待成功的一刻,结果又让我失望了,还是和之前一样,也就是说我的配置没有生效,于是我猜测,是我的配置有问题吗?还是别的地方有类似的配置覆盖了我的配置,因为项目之前没有写过接口,但是我却搜索到了一些关于api的配置代码,于是问了同事,发现那些代码是之前项目复制过来的,根本没用上,反而误了我的开发,配置中有一个类实现了Filter接口,因此我的配置没有生效,因为在过滤器层级就被拦截处理了。同时还有一个类public class AuthInterceptor implements HandlerInterceptor,想想,我也不知道这个类的代码改动后会不会影响其他的代码,这也是软件开发的基本原则,对扩展开放,对修改关闭,因此我放弃了使用@Bean配置跨域,就在这个类里面处理,类重写了如下方法:

1
2
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {...}

处理跨域,按照我查阅的资料,星号*是最大的匹配,于是在开发阶段,我对跨域的配置代码段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//允许以下请求头
String allowHeaders = "*";
//允许以下请求方法
String allowMethods = "*";
//允许有认证信息(cookie)
String allowCredentials = "true";
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", allowMethods);
response.setHeader("Access-Control-Allow-Headers", allowHeaders);
response.setHeader("Access-Control-Allow-Credentials", allowCredentials);
//处理 OPTIONS 的请求
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
}

重启项目,开始真机测试,诶,好像可以了,于是美滋滋的开发未完成的功能,直到交付测试的时候,出问题了,“为啥登录不了呀?”测试的妹子问我,我说不会吧我测试都好好的呀,去她那一看,还果真不行,于是又开始找原因,百思不得其解,找了多个手机来测试,发现iOS系统的几乎都没问题,Android的好像都不行(在钉钉内打开H5),于是猜测是不是和操作系统有关,于是我又让他们在别的地方打开看一下,比如微信内打开、手机自带浏览器打开测试,结果微信(手机)内几乎都能正常使用,于是推翻了刚才的猜测,看来和操作系统没关系,而是和浏览器有关,但是烦人的是,钉钉又不能调试(我没找到好的调试方法,于是放弃了在手机钉钉内调试),于是我开始想别的办法,尝试着在pc上找到一个和安卓钉钉一样不能正常打开h5的浏览器,别说,两下就找到了,我在PC版微信内打开,和刚才他们的安卓钉钉内打开一样的,前端h5能正常加载显示,但就是请求不到后台数据,我用Fiddler抓包查看过,能正常使用的浏览器会发出两次请求,第一次请求类型为OPTIONS不带任何参数,请求也得到了正常响应,状态码是200,第二次请求就是真正带参数的GTT or POST请求,但是不能正常使用的浏览器就发了第一次OPTIONS请求于是开始上网查询这个OPTIONS请求到底何方神圣。

网上查阅的结果大致描述为:跨域的时候,浏览器会在正式发出请求之前发起一次OPTIONS请求,以检测服务端是否支持该请求的请求头字段、请求方法、请求源等,我想,我的都是*,那肯定不管是什么都能通过的呀,于是就没往这里想下去,但最终问题就出在这里。

服务端配置的跨域,并不是所有浏览器都支持*通配符的,经过各种猜测与实践,最终把服务端跨域配置修改如下,惊奇的一幕可以了,不论什么浏览器,一切都正常了,真的是雨过天晴,太激动了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//允许以下请求头
String allowHeaders = "Content-Type,token,Authorization";
//允许以下请求方法
String allowMethods = "GET,POST,PUT,DELETE,OPTIONS";
//允许有认证信息(cookie)
String allowCredentials = "true";
// 4*60*60 = 14400 秒内不会对同一请求发起OPTIONS的预检请求
String maxAge = "14400";
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", allowMethods);
response.setHeader("Access-Control-Allow-Headers", allowHeaders);
response.setHeader("Access-Control-Allow-Credentials", allowCredentials);
response.setHeader("Access-Control-Max-Age", maxAge);
//处理 OPTIONS 的请求
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
}

是的,就是允许的请求头和请求方法改为实际用到的字段,一切都好了,也不知道这是什么原因导致的,这个问题困扰了我太久,问了公司的同事,大家都被困扰了,还找了钉钉的人来问,给我们的回复是这样的:

经过调试,发现钉钉浏览器内不支持 Content-Typeapplication/json 的请求头,只能使用 url-encoding 的请求头类型,否则服务端接收不到参数。

我当时都被震惊了,不支持这种请求头类型???我完全就不能相信,因为之前的项目是可以的,所以这显然不成立。

解决问题的过程就是在不断的猜测、不停地调试,网上的答案千篇一律,但是他们的项目都和你的不同,脱离了自己项目的调试或Google,都是在刷流氓。

当发现搜索引擎上找到的答案基本都不能解决问题的时候,就要换种思考方式了,是不是别的地方有什么配置覆盖了你的配置等等,这次解决问题的过程很艰辛,但收获颇多,方式很重要,理清思路,看看到底在哪出了问题,然后再去解决它,其实到头来,一直都是我想象的跨域问题,只是大家都不认可我,觉得不可能是这个问题,坚信自己,按照自己觉得可能的问题去调试或者去推翻自己的想法,问题很快就会得到解决。