背景
最近在 ITA 写了一个聊天机器人的 Flask 服务,自己写了一些 node 单元测试脚本跑没有问题,但是测试的同学也想覆盖到所有的 case,于是就帮忙写一个 html 页面去测试,然后就遇到了下面的问题:
XMLHttpRequest cannot load http://localhost:8085/predict. No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘null’ is therefore not allowed access.
这个是典型的跨域问题(跨域是指:协议、域名、端口有任何一个不同,都被当做是不同的域),想想之前也了解过跨域的知识,现在借着这个机会总结一下了。关于 GET 请求的跨域,使用 JSONP 是目前最好的解决方案,各大浏览器也基本都支持 JSONP,而 jQuery,AngularJS 等前端框架也都默认添加了对 JSONP 的封装,并且这次遇到的跨域问题是 POST 请求的,于是暂时先不写关于 JSONP 的相关知识。
简化代码
服务器代码:
1 | from flask import Flask |
页面代码:
1 |
|
– 原谅我用 Angular 做页面 ☹
main.js
1 | angular.module('chatApp', []) |
解决方案
要想解决跨域,必先理解跨域。那什么是跨域呢?
对于 web 开发来讲,由于浏览器的同源策略,我们需要经常使用一些 hack 的方法去跨域获取资源,直到 W3C 出了一个标准-CORS-“跨域资源共享”(Cross-origin resource sharing),
它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。
CORS 与 JSONP 的使用目的相同,但是比 JSONP 更强大。
JSONP 只支持 GET 请求,CORS 支持所有类型的 HTTP 请求。JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。
CORS 解决方案:
(1) 服务器代码
1 | from flask import Flask, Response, request |
(2) main.js
1 | angular.module('chatApp', []) |
此时再次发送 Ajax call就可以拿到结果了:
注意到服务器端代码发生了一点改动,那就是在Response header中增加了一个参数 “Access-Control-Allow-Origin”,表示接受某域名的请求,“*” 表示允许所有的请求。
也可以使用确定的值,如: “http://api.abc.com”。
于是代码中增加 headers = {“Access-Control-Allow-Origin”: “*”} 后服务器就可以响应所有的请求了。
再看 Web 端的代码,我们在请求头里面添加了 “Content-Type”,为了能向服务端传递数据。这里使用的 “Content-Type” 为 “application/x-www-form-urlencoded” 表示以表单提交的形式传递参数。
为什么要用表单的形式提交POST请求呢?
两种请求
浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。
(1) 请求方法是以下三种方法中的一个:
- HEAD
- GET
- POST
(2) HTTP的头信息不超出以下几种字段: - Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type 其值仅限于 application/x-www-form-urlencoded、multipart/form-data、text/plain
上文中的请求属于简单请求。
简单请求(simple request)
对于简单的跨域请求,浏览器会自动在请求的头信息加上 Origin 字段,表示本次请求来自哪个源(协议 + 域名 + 端口),服务端会获取到这个值,然后判断是否同意这次请求并返回。
// 请求
GET /cors HTTP/1.1
Origin: http://api.abc.com
Host: api.bcd.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0…
如果服务端许可本次请求,就会在返回的头信息多出关于 Access-Control 的信息,比如上述服务器返回的信息:
非简单请求(not-so-simple request)
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUT 或 DELETE,或者 Content-Type 字段的类型是 application/json。
非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为“预检”请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。
“预检”请求用的请求方法是 OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。
非简单请求解决方案
项目中使用的 Content-Type 为 application/json,属于非简单请求,将上述程序修改为
(1) main.js:
1 | angular.module('chatApp', []) |
服务器代码:
1 | from flask import Flask, Response, request |
启动后发送请求,发现可以跑通,但是获取不到参数,原因是使用 application/json 的形式发送 request, 参数并没有放在 form 里面,而是放在 request.data 里面了。
request.data 里面为 bytes 类型的数据,通过 request.json 可以获取其 dict 类型。
通过以上方式,完美地解决了复杂请求的跨域问题。
才怪嘞!!!♋
问题所在
以上解决跨域的方式为 CORS,准确地说,这是一种服务器端的技术。而现实生产环境中,如果一个前端想要用这种方式实现跨域,不知道要跟后端做多少沟通,那有没有纯前端的解决方案呢?
且听下回分解。☛