一、什么是跨域
当且仅当协议、域名、端口都相同时为同源
URL | 说明 | 是否同源 |
---|---|---|
http://www.a.com/a.js http://www.a.com/b.js | 同一域名下的不同文件 | 是 |
http://www.a.com/folder1/a.js http://www.a.com/folfer2/b.js | 同一域名下的不同文件夹 | 是 |
http://www.a.com:8000/a.js http://www.a.com/b.js | 同一域名、不同端口 | 否 |
http://www.a.com/a.js https://www.a.com/b.js | 同一域名、不同协议 | 否 |
http://www.a.com/a.js http://70.32.92.74/b.js | 域名与对应IP | 否 |
http://www.a.com/a.js http://www.b.com/b.js http://script.a.com/b.js | 主域相同,子域不同,相当于不同域名 | 否 |
http://www.baidu.com/a.js http://baidu.com/b.js | 主域相同,子域不同,相当于不同域名 | 否 |
二、跨域解决方案
1. 打破浏览器限制(临时解决方案)
2. JSONP
-
原理:
script、img、iframe
等含src
属性的标签在请求资源时不会触发跨域问题。JSONP使用 script 标签来加载数据。 -
缺点:
(1) 只能发送 GET 请求
(2) 发送的不是 XHR 请求,这样导致 XHR 请求中的很多事件都无法进行处理
(3) 服务端需要改动 -
实现
# 后端 flask # 域名 demo.com @app.route('api', methods=['GET']) def api(): req_data = request.args func = req_data['callback'] # 获取函数名 res_data = { 'message': 'from backend: ' + req_data['message'] } return func + '(' + json.dumps(res_data) + ')' # 构建函数调用,需要返回的数据作为函数调用的参数
// 前端 Vue methods: { jsonp(){ // 在window中注册函数 window.handleResponse = (_data) => { // _data: 此函数参数即为需要跨域传输的数据 alert(JSON.stringify(_data)) } // 使用script的src执行API返回的函数调用 const script = document.createElement('script') let func = 'handleResponse'; // 函数名需要与上方在window中注册的函数名一致 script.type = 'text/javascript' script.src = 'http://demo.com/api?message=hello&callback=' + func; // 挂载到document中执行API返回的函数调用 document.body.appendChild(script) setTimeout(() => { document.body.removeChild(script) }, 2000) } }
结合代码稍微再解释一下JSONP:
- 服务器端:
(1)最终将向客户端返回一个函数调用,函数调用中的参数就是需要传输的数据,函数名由客户端提供。# 示例代码中服务器的响应数据 "handleResponse( { 'message': 'from backend: hello' } )" # 其实就是一个字符串,内容为一次函数调用,参数为需要跨域的数据
- 客户端:
(1)使用<script>
标签的src
属性来调用API,并在url的请求参数中提供一个函数名。<script>
会自动执行所获取的代码/js文件,也就是会执行API返回的函数调用。
(2)因此,我们只需要在<script>
脚本执行前,事先在全局window中注册一个函数名相同的函数,便能在该函数体内获取和操作该函数的参数,这个参数也就是需要跨域传输的数据。// window 中事先注册了函数handleResponse,函数的功能是把对象参数转为JSON字符串后alert出来 window.handleResponse = (_data) => { alert(JSON.stringify(_data)) } // <script>标签自动调用API获取响应数据(一段函数调用代码),并执行这段代码 // 其实就是相当于执行了下面的这行代码 handleResponse( { 'message': 'from backend: hello' } );
3. CORS
- 优点:支持多种请求、在后端设置即可、API传输的数据格式无需变动
- 实现(未考虑cookie, CORS详解可以看阮一峰老师的这篇)
# 后端 flask @app.route('/api', methods=['POST']) def api(): req_data = json.loads(request.get_data()) response = make_response({'message': 'form backend: ' + req_data['message']}) # 设置Access-Control-Allow-Origin。此处与设置'*'差不多,也可按实际需求添加一个白名单过滤器。 response.headers['Access-Control-Allow-Origin'] = request.headers['Origin'] if request.headers['Origin'] else '' return response
- CORS请求头和响应头总结
-
请求头:
- Origin: 浏览器发出 Ajax 跨域请求之前会添加此头部,值为发送请求的域
- Access-Control-Request-Method:使用了除 GET、POST、HEAD 请求方法之外的方法,浏览器会添加此头部,值为当前请求方法
- Access-Control-Request-Headers:使用了自定义头部或除了Accept、Accept-Language、Content-Language、Content-Type、Last-Event-ID 之外的头部,浏览器会添加此头部,值为当前的请求方法
-
响应头:
- Access-Control-Allow-Origin: 表示服务端允许哪些域请求资源
- Access-Control-Allow-Methods: 当客户端包含 Access-Control-Request-Method 请求头时,服务端需要响应该头部,值通常由 Reauest 的 header 中 Access-Control-Request-Method 取得
- Access-Control-Allow-Headers: 当客户端包含 Access-Control-Request-Headers 请求头时,服务端需要响应该头部,值通常由 Reauest 的 header 中 Access-Control-Request-Headers 取得
- Access-Control-Expose-Headers: 指出客户端通过 XHR 对象的 getResponseHeaders 方法可以获取的响应头有哪些
- Access-Control-Allow-Credentials: 后端允许带 cookie 的跨域请求(前端需要相应设置
xhr.withCredentials
或者fetch
的credential
属性) - Access-Control-Max-Age: 预检请求的缓存时间