Reqable项目日志:如何解决Flutter Web跨域问题

1,310 阅读3分钟

Reqable正式发布以来,收到了很多开发者朋友们的肯定和支持。在此,写一个项目日志系列。这是第二篇,还原下Reqable是如何解决Flutter Web跨域问题的。

跨域问题

跨域限制是浏览器的安全策略,禁止不同的域名之间相互调用,比如Reqable的许可证管理系统的域名是license.reqable.com,而后端部署的服务域名是api-dev.reqable.com,这个就属于两个域名了。在开发调试的时候,域名是localhost,和后端服务域名也属于两个不同的域名。无论哪种情况都会触发浏览器跨域限制。

按下F12打开浏览器调试模式,发现请求报红了,虽然服务器返回了200的状态码:

screenshot_01.png

这个是非常典型的跨域问题。

问题分析

有没有可能是浏览器根本没有向服务器发送请求呢,我们使用Reqable抓个包看看情况。 当然,也可以用其他的流量分析软件,但是Reqable本身就是专业的流量分析工具😄。

screenshot_02.png

我们确认浏览器确实向服务器发送了请求,服务器也正常响应了这个请求。看来,这个跨域问题是浏览器本身的限制。那如何让浏览器放开这个限制呢?

常规的解决方式是服务器返回的响应加上这两个头部:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: "GET, PUT, POST, DELETE, HEAD, OPTIONS"

在服务器修改之前,我们先用Reqable修改响应头部模拟下试试这个方式是否真的有用。这里使用Reqable的脚本功能(当然也可以使用重写和断点功能),不熟悉操作的可以参考这篇文章如何使用Reqable脚本功能提高API开发效率

screenshot_03.png

重新发送一下请求,发现已经正常了。

screenshot_04.png

但是,当我在请求头中添加了一个自定义的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"
);

从下面截图可以看到,浏览器向服务器发送了一个POSTOPTIONS方法的请求,但是实际我们项目中发送的是一个POST请求,这是怎回事呢?难道是代码写错了?

screenshot_05.png

接下来,检查下代码。我使用的是官方的http库,并没有使用任何的第三方库,代码里面写的也确实是调用的post方法,难道是http这个库干了挂羊头卖狗肉的事情?

简单看了下http库的源码,发现这个库在Web端只是简单封装了XMLHttpRequest,看来这个问题确实是浏览器本身的行为了。再次通过Reqable抓包,看到浏览器实际只发送了一个OPTIONS请求:

screenshot_06.png

我在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 an OPTIONS request before the POST. developer.mozilla.org/en-US/docs/…

意思就是说,服务器需要在OPTIONS请求里响应通过Access-Control-Request-Headers回应浏览器有哪些请求头可以允许跨域。

继续通过Reqable的脚本功能加上Access-Control-Request-Headers字段,由于服务器未处理OPTIONS请求,我们强行把状态码改成204:

screenshot_07.png

再次发送请求,跨域问题终于没有了。

问题解决

下面,前面虽然通过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);
  };
}