跨域问题
跨域
出于安全原因,浏览器限制从脚本内发起的跨源HTTP请求。 例如,XMLHttpRequest和Fetch API遵循同源策略。 这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非响应报文包含了正确CORS响应头。(不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。)
浏览器同源策略
同源定义
如果两个 URL 的 protocol、port (如果有指定的话)和 host 都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”,或者直接是 “元组”。(“元组” 是指一组项目构成的整体,双重/三重/四重/五重/等的通用形式)。
例子: 和 http://oasis-f.com/index.html
的源进行对比。
URL | 是否同源 | 原因 |
---|---|---|
http://oasis-f.com/foo/bar.html | 同源 | 只有路径不同 |
https://oasis-f.com/foo/bar.html | 否 | 协议不同 |
http://oasis-f.com:8080/foo/bar.html | 否 | 端口不同 |
http://bad-oasis-f.com/foo/bar.html | 否 | 主机不同 |
不受同源限制的情况
<script src="..."></script>
标签嵌入跨域脚本。语法错误信息只能被同源脚本中捕捉到。<link rel="stylesheet" href="...">
标签嵌入CSS。由于CSS的松散的语法规则,CSS的跨域需要一个设置正确的 HTTP 头部 Content-Type 。不同浏览器有不同的限制: IE, Firefox, Chrome, Safari (跳至CVE-2010-0051)部分 和 Opera。- 通过
<img>
展示的图片。支持的图片格式包括PNG,JPEG,GIF,BMP,SVG,... - 通过
<video>
和<audio>
播放的多媒体资源。 - 通过
<object>
、<embed>
和<applet>
嵌入的插件。 - 通过 @font-face 引入的字体。一些浏览器允许跨域字体( cross-origin fonts),一些需要同源字体(same-origin fonts)。
- 通过
<iframe>
载入的任何资源。站点可以使用 X-Frame-Options 消息头来阻止这种形式的跨域交互。
JSONP
利用 script
标签不受同源策略控制的特点来访问不同源下的资源。具体可以查看这篇文章,虽然年代比较早但是讲的很清楚。
说说JSON和JSONP,也许你会豁然开朗,含jQuery用例
不过使用 jsonp 也有一些额外的问题:
- 只能发起 GET 请求
- 因为浏览器的限制无法返回捕获到错误码
- 安全性低
CORS
跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。
跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。
CORS请求失败会产生错误,但是为了安全,在JavaScript代码层面是无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。
简单请求
不会触发 CORS 预检请求,后端程序只需要在返回的响应头中加上 Access-Control-Allow-Origin 字段,并且把该字段的值设置为 跨域请求的来源地址或简单的设置为 * 就可以了。
简单请求需满足下列所述条件
- 使用下列方法:
GET
HEAD
POST
- 只设置下列
header
:- 用户代理自动设置(
Connection
,User-Agent
) Accept
Accept-Language
Content-Language
Content-Type
(只允许下列值)text/plain
mujltipart/form-data
application/x-www-urlencoded
DPR
Downlink
Save-Data
Viewport-Width
Width
- 用户代理自动设置(
- 请求中的任意
XMLHttpRequestUpload
对象没有注册任何事件监听器。 - 请求中没有使用
ReadableStream
对象
Node Express 例子
// 客户端 静态服务运行于 http://192.168.0.119:8000
function sendRequest(){
fetch('http://192.168.0.119:3000')
.then(res=>console.log(res)).catch(e=>console.error(e))
}
// 服务端
const express = require('express');
const app = express();
const port = 3000;
app.get('/',(req,res) => res.send('cors app'));
app.listen(port,()=> console.log(`cors app listening on port ${port}`));
此时请求跨域
设置CORS相关请求头
const express = require('express');
const app = express();
const port = 3000;
// CORS 中间件
const allowCrossDomain = (req,res,next)=>{
res.header('Access-Control-Allow-Origin','http://192.168.0.119:8000');
next()
}
app.use(allowCrossDomain)
app.get('/',(req,res) => res.send('cors app'));
app.listen(port,()=> console.log(`cors app listening on port ${port}`));
这时候就可以成功请求到了。
但是完整的应用不可能只有 GET 请求呀。需要有 更多的 method,cookie 等
fetch('http://192.168.0.119:3000',{
credentials: 'include', // 发送带凭据的请求
method: 'POST',
body:JSON.stringify({foo:'bar'}),
headers:new Headers({
'Content-Type':'application/json',
'Custom-Header':'8888'
})
})
.then(res=>console.log(res)).catch(e=>console.error(e))
...
const allowCrossDomain = (req,res,next)=>{
res.header('Access-Control-Allow-Origin','http://192.168.0.119:8000');
res.header('Access-Control-Allow-Methods','POST');
res.header('Access-Control-Allow-Headers',"Content-Type,Custom-Header");
res.header('Access-Control-Allow-Credentials','true');
next()
}
...
这里有个需要注意的地方:
对于附带身份凭证的请求,服务器不得设置
Access-Control-Allow-Origin
的值为“”。
这是因为请求的首部中携带了Cookie
信息,如果Access-Control-Allow-Origin
的值为“”,请求将会失败。而将Access-Control-Allow-Origin
的值设置为http://foo.example
,则请求将成功执行。
另外,响应首部中也携带了Set-Cookie
字段,尝试对Cookie
进行修改。如果操作失败,将会抛出异常。
参考链接
- Cross-Origin Resource Sharing (CORS), by MDN
- 说说JSON和JSONP,也许你会豁然开朗,含jQuery用例 by 随它去吧