同源策略控制了不同源之间的交互,不同源之间的请求就会出现跨域,同源指:协议、域名、端口号必须一致
严格的说,浏览器并不是拒绝所有的跨域请求,实际上拒绝的是跨域的读操作。浏览器的同源限制策略是这样执行的:
- 通常浏览器允许进行跨域写操作(Cross-origin writes),如链接,重定向;
- 通常浏览器允许跨域资源嵌入(Cross-origin embedding),如 img、script 标签;
- 通常浏览器不允许跨域读操作(Cross-origin reads)。
下面为允许跨域资源嵌入的示例,即一些不受同源策略影响的标签示例:
<script src="..."></script>
标签嵌入跨域脚本。<link rel="stylesheet" href="...">
标签嵌入CSS。<img src="..."/>
嵌入图片。<video> 和 <audio>
嵌入多媒体资源。<object>, <embed> 和 <applet>
的插件。- @font-face引入的字体。一些浏览器允许跨域字体( cross-origin fonts),一些需要同源字体(same-origin fonts)。
<frame>和<iframe>
载入的任何资源。站点可以使用X-Frame-Options消息头来阻止这种形式的跨域交互。
常用的跨域方式有以下几种:JSONP、CORS、postMessage、代理服务器
1、JSONP
利用script标签不受跨域限制而形成的一种方案。
function jsonp({url, params, callback}){
return new Promise((resolve, reject)=>{
// 创建一个script
let script = document.createElement('script')
// 前后台统一一个回调函数名称用来接收
window[callback] = function(data){
resolve(data);
document.body.removeChild(script)
}
// 解析URL
const paramsStr = Object.entries({...params, callback}).map(item=>`${item[0]}=${item[1]}`).join('&')
const queryStr = url.split('?')[1]
if(queryStr == null || queryStr === ''){
url += '?' + paramsStr
}else{
url += '&' + paramsStr
}
script.src = url
document.body.appendChild(script)
})
}
【JSONP的优缺点】
- 优点:兼容性好(兼容低版本IE)
- 缺点:
- 1、JSONP只支持GET请求;
- 2、XMLHttpRequest相对于JSONP有着更好的错误处理机制;
- 3、不安全,容易受XSS攻击。
2、CORS(跨域资源共享)
跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站有权限访问哪些资源。
规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。
浏览器将CORS跨域请求分为简单请求和非简单请求;
只要同时满足一下两个条件,就属于简单请求,不同时满足下面的两个条件,就属于非简单请求,浏览器对这两种的处理,是不一样的。
-
使用下列方法之一
- head
- get
- post
-
请求的Heder是
- Accept
- Accept-Language
- Content-Language
- Content-Type: 只限于三个值:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
1、简单请求
对于简单请求,浏览器直接发出CORS请求。具体来说,就是头信息之中,增加一个Origin字段。
1、非简单请求
非简单请求就是那种对服务器有特殊要求的请求,比如请求方法为PUT或DELETE,或者Content-Type字段为application/json;
-
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为“预检”请求; 浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段,只有得到肯定答复,浏览器才会发出正式的接口请求,否则就会报错;
-
一旦服务器通过了预检请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段;
-
CORS请求默认不发送Cookie和HTTP认证信息,如果要把Cookie发到服务器,一方面需要服务器同意,设置响应头Access-Control-Allow-Credentials: true,另一方面在客户端发出请求的时候也要进行一些设置;
// XHR
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true);
xhr.withCredentials = true;
xhr.send(null);
// Fetch
fetch(url, {
credentials: 'include'
})
- 如果设置请求头'Content-Type': 'application/x-www-form-urlencoded',这种情况则为简单请求; 会有跨域问题,直接设置 响应头 Access-Control-Allow-Origin为*, 或者具体的域名;注意如果设置响应头Access-Control-Allow-Credentials为true,表示要发送cookie,则此时Access-Control-Allow-Origin的值不能设置为星号,必须指定明确的,与请求网页一致的域名。
const login = ctx => {
const req = ctx.request.body;
const userName = req.userName;
ctx.set('Access-Control-Allow-Origin', '*');
ctx.response.body = {
data: {},
msg: '登陆成功'
};
}
- 如果设置请求头'Content-Type': 'application/json',这种情况则为非简单请求 处理OPTIONS请求,服务端可以单独写一个路由,来处理login的OPTIONS的请求
app.use(route.options('/login', ctx => {
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', 'Content-Type');
ctx.status = 204;
}));
大家都知道前端调用服务端的时候,会调用很多个接口,并且每个接口处理跨域请求的逻辑是完全一样的,我们可以把这部分抽离出来,作为一个中间件;
具体CORS的详细原理,推荐 @木子星兮 之 CORS原理及@koa/cors源码解析
let express = require('express');
let app = express();
let whiteList = ['http://localhost:3000']
app.use(function(req, res, next){
let origin = req.headers.origin;
if(whiteList.includes(origin)){
//设置那个源可以访问我,参数为 * 时,允许任何人访问,但是不可以和 cookie 凭证的响应头共同使用
res.setHeader('Access-Control-Allow-Origin', origin);
//允许带有name的请求头的可以访问
res.setHeader('Access-Control-Allow-Headers','name');
// 设置哪些请求方法可访问
res.setHeader('Access-Control-Allow-Methods', 'PUT');
// 设置带cookie请求时允许访问
res.setHeader('Access-Control-Allow-Credentials', true);
// 后台改了前端传的name请示头后,再传回去时浏览器会认为不安全,所以要设置下面这个
res.setHeader('Access-Control-Expose-Headers','name');
// 预检的存活时间-options请示
res.setHeader('Access-Control-Max-Age',3)
// 设置当预请求发来请求时,不做任何处理
if(req.method === 'OPTIONS'){
res.end();//OPTIONS请示不做任何处理
}
}
next();
});
3、postMessage
window.postMessage() 方法被调用时,会在所有页面脚本执行完毕之后向目标窗口派发一个MessageEvent消息。
// a.html
<iframe src="http://localhost:4000/b.html" id="frame" onload="load()"></iframe>
<script>
function load(params){
let frame = document.getElementById('frame');
//获取iframe中的窗口,给iframe里嵌入的window发消息
frame.contentWindow.postMessage('hello','http://localhost:4000')
// 接收b.html回过来的消息
window.onmessage = function(e){
console.log(e.data)
}
}
</script>
// b.html
<script>
//监听a.html发来的消息
window.onmessage = function(e){
console.log(e.data)
//给发送源回消息
e.source.postMessage('nice to meet you',e.origin)
}
</script>
4、代理服务器
因为跨域问题是浏览器端的问题,服务器端是没有跨域问题的,所以可以开启一个同源的代理服务器转发数据给目标服务器,再接受目标服务器返回的数据,最终返回给客户端
1、Nginx
Nginx (engine x) 是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP服务器。
// client.html
let xhr = new XMLHttpRequest;
xhr.open('get', 'http://localhost/a.json', true);
xhr.onreadystatechange = function() {
if(xhr.readyState === 4){
if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304 ){
console.log(xhr.response);
}
}
}
// server.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);
// nginx.conf
location / {// 代表输入/时默认去打开root目录下的html文件夹
root html;
index index.html index.htm;
}
location ~.*\.json{//代表输入任意.json后去打开json文件夹
root json;
add_header "Access-Control-Allow-Origin" "*";
}
2、http-proxy-middleware
NodeJS 中间件 http-proxy-middleware 实现跨域代理,原理大致与 nginx 相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置 cookieDomainRewrite 参数修改响应头中 cookie 中的域名,实现当前域的 cookie 写入,方便接口登录认证。
vue框架
利用 node + webpack + webpack-dev-server 代理接口跨域。在开发环境下,由于 Vue 渲染服务和接口代理服务都是 webpack-dev-server,所以页面与代理接口之间不再跨域,无须设置 Headers 跨域信息了。
module.exports = {
entry: {},
module: {},
...
devServer: {
historyApiFallback: true,
proxy: [{
context: '/login',
target: 'http://www.proxy2.com:8080', // 代理跨域目标接口
changeOrigin: true,
secure: false, // 当代理某些 https 服务报错时用
cookieDomainRewrite: 'www.domain1.com' // 可以为 false,表示不修改
}],
noInfo: true
}
}
非vue框架的跨域
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>nginx跨域</title>
</head>
<body>
<script>
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写 cookie
xhr.withCredentials = true;
// 访问 http-proxy-middleware 代理服务器
xhr.open('get', 'http://www.proxy1.com:3000/login?user=admin', true);
xhr.send();
</script>
</body>
</html>
// 中间代理服务器
var express = require("express");
var proxy = require("http-proxy-middleware");
var app = express();
app.use(
"/",
proxy({
// 代理跨域目标接口
target: "http://www.proxy2.com:8080",
changeOrigin: true,
// 修改响应头信息,实现跨域并允许带 cookie
onProxyRes: function(proxyRes, req, res) {
res.header("Access-Control-Allow-Origin", "http://www.proxy1.com");
res.header("Access-Control-Allow-Credentials", "true");
},
// 修改响应信息中的 cookie 域名
cookieDomainRewrite: "www.proxy1.com" // 可以为 false,表示不修改
})
);
app.listen(3000);
// 服务器
var http = require("http");
var server = http.createServer();
var qs = require("querystring");
server.on("request", function(req, res) {
var params = qs.parse(req.url.substring(2));
// 向前台写 cookie
res.writeHead(200, {
"Set-Cookie": "l=a123456;Path=/;Domain=www.proxy2.com;HttpOnly" // HttpOnly:脚本无法读取
});
res.write(JSON.stringify(params));
res.end();
});
server.listen("8080");