自Reqable正式发布以来,收到了很多开发者朋友们的肯定和支持。在此,写一个项目日志系列。这是第二篇,还原下Reqable是如何解决Flutter Web跨域问题的。
跨域问题
跨域限制是浏览器的安全策略,禁止不同的域名之间相互调用,比如Reqable
的许可证管理系统的域名是license.reqable.com
,而后端部署的服务域名是api-dev.reqable.com
,这个就属于两个域名了。在开发调试的时候,域名是localhost
,和后端服务域名也属于两个不同的域名。无论哪种情况都会触发浏览器跨域限制。
按下F12打开浏览器调试模式,发现请求报红了,虽然服务器返回了200
的状态码:
这个是非常典型的跨域问题。
问题分析
有没有可能是浏览器根本没有向服务器发送请求呢,我们使用Reqable
抓个包看看情况。 当然,也可以用其他的流量分析软件,但是Reqable
本身就是专业的流量分析工具😄。
我们确认浏览器确实向服务器发送了请求,服务器也正常响应了这个请求。看来,这个跨域问题是浏览器本身的限制。那如何让浏览器放开这个限制呢?
常规的解决方式是服务器返回的响应加上这两个头部:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: "GET, PUT, POST, DELETE, HEAD, OPTIONS"
在服务器修改之前,我们先用Reqable修改响应头部模拟下试试这个方式是否真的有用。这里使用Reqable的脚本功能(当然也可以使用重写和断点功能),不熟悉操作的可以参考这篇文章如何使用Reqable脚本功能提高API开发效率。
重新发送一下请求,发现已经正常了。
但是,当我在请求头中添加了一个自定义的channel
字段时,新的跨域问题又出现了。
final Map<String, dynamic> headers = {
'channel': 1,
};
final response = await http.post(Uri.parse(url),
headers: headers.map((key, value) => MapEntry(key, value.toString())),
body: "xxx"
);
从下面截图可以看到,浏览器向服务器发送了一个POST
和OPTIONS
方法的请求,但是实际我们项目中发送的是一个POST
请求,这是怎回事呢?难道是代码写错了?
接下来,检查下代码。我使用的是官方的http
库,并没有使用任何的第三方库,代码里面写的也确实是调用的post
方法,难道是http
这个库干了挂羊头卖狗肉的事情?
简单看了下http
库的源码,发现这个库在Web端只是简单封装了XMLHttpRequest
,看来这个问题确实是浏览器本身的行为了。再次通过Reqable抓包,看到浏览器实际只发送了一个OPTIONS
请求:
我在Github上找到了issue #667,评论里原因讲得非常清楚了。
This is behavior of the browser. We cannot control this from this library or the Dart SDK. Any time you make a CORS
POST
request with a content-type header that isn't in a narrow allowed list it will first make anOPTIONS
request before thePOST
. developer.mozilla.org/en-US/docs/…
意思就是说,服务器需要在OPTIONS
请求里响应通过Access-Control-Request-Headers
回应浏览器有哪些请求头可以允许跨域。
继续通过Reqable的脚本功能加上Access-Control-Request-Headers
字段,由于服务器未处理OPTIONS
请求,我们强行把状态码改成204
:
再次发送请求,跨域问题终于没有了。
问题解决
下面,前面虽然通过Reqable模拟成功了,但是这个问题还是要由服务端来解决。Reqable许可证的服务端是通过Dart编写的,解决非常简单,配置一个middleware
即可:
import 'package:shelf/shelf.dart';
Handler cors(Handler innerHandler) {
return (request) async {
if (request.method == 'OPTIONS') {
return Response(204, headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET',
'Access-Control-Allow-Headers': 'channel',
'Access-Control-Max-Age': '86400'
});
}
return innerHandler(request);
};
}