参考:
浏览器的同源策略
浏览器同源政策及其规避方法
同源政策
什么是同源策略?
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
源的定义
如果两个页面的协议,端口(如果有指定)和域名都相同,则两个页面具有相同的源。
同源检测示例:http://www.example.com/directory/page.html
http://www.example.com/directory/other.html // 成功
http://www.example.com/directory/inner/another.html // 成功
https://example.com/index.html // 失败 不同协议(http | https)
http://example.com:90/dir/secure.html // 失败 不同端口(80 | 90)
http://new.example.com:90/dir/secure.html // 失败 不同域名(new.example | example)
同源下的脚本只能读取与所属文档同源的窗口和文档的属性。
同源政策的目的,是为了保护用户信息的安全,防止恶意的网站窃取数据。
目前,非同源将会受到限制的行为有:
- 无法读取非同源资源下的:Cookie、LocalStorage、IndexedDB。
- 无法操作非同源网页的DOM。
- 无法向非同源地址发送Ajax请求(实际上,服务器会收到请求,也会返回,但最终被浏览器拦截)。
注意
- 对于当前页面来说页面存放的 JS 文件的域不重要,重要的是加载该 JS 页面所在什么域。
- 同源策略限制的是脚本嵌入的文本来源,而不是脚本本身。
不受同源策略限制:
- 页面中的链接,重定向以及表单提交是不会受到同源策略限制的。
- 跨域资源的引入是可以的。但是js不能读写加载的内容。如嵌入到页面中的<script src="..."></script>,<img>,<link>,<iframe>等。
实现一个同源限制
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" href="./HS/css/test.css">
</head>
<body>
<h1>Hello World!</h1>
</body>
<script>
var xhr = new XMLHttpRequest();
xhr.open("GET","http://localhost:8080/getSomething",true);
xhr.send();
xhr.addEventListener("load",function() {
console.log(xhr.responseText);
});
</script>
</html>
server.js
var http = require("http");
var fs = require("fs");
var path = require("path");
var url = require("url");
var server = http.createServer(function (req, res) {
var pathObj = url.parse(req.url, true);
console.log(pathObj.pathname);
switch (pathObj.pathname) {
case "/getSomething":
res.end(
JSON.stringify({ beijing: "sunny" })
);
break;
default:
fs.readFile(path.join(__dirname, pathObj.pathname), function (err, data) {
if (err) {
res.writeHead(404, "not found");
res.end("<h1>404 Not Found</h1>")
} else {
res.end(data);
}
})
break;
}
});
server.listen(8080);
console.log("visit http://localhost:8080/httpServer.html");
HTML页面中的Ajax请求就是test point,页面中,我们设置了一个Ajax请求,并在node server中的switch语句进行设定,当请求发送后,如果有一个指向
http://localhost:8080/getSomething
的请求,本地服务器就会返回一个被JSON.stringify()方法解析后的JS对象。否则,就使用fs模块读取静态文件(css、js等)
正常访问成功下的请求状况:
每一个文件都显示
200 ok
,请求成功,并且服务器也收到了Ajax请求,并返回数据。
我们稍微对Ajax请求URL更改一下:
http://www.localhost:8080/getSomething
运行服务器:
此时,我们就实现了一个跨域请求,只是没有数据返回。
我们发现,HTTP状态码显示200,说明请求是成功的,但却没有数据返回,且有红字报错。
Failed to load http://www.localhost:8080/getSomething: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8080' is therefore not allowed access.
出现这段提示,就是浏览器告诉你,跨域了!
No 'Access-Control-Allow-Origin' header is present on the requested resource
意思是不存在Access-Control-Allow-Origin
标记,这个标记是我们实现跨域时才会出现的,如果存在此标记,浏览器不会对跨域的请求进行拦截,稍后会详细解释。
注意:请求是成功的,但是因为浏览器的安全机制,请求的数据被拦截。
跨域 —— JSONP
JSONP是JSON with padding,填充式JSON或参数式JSON的缩写。是应用JSON的一种新方法,在后来的Web服务器中非常流行。JSONP看起来与JSON差不多,只不过是被包含在函数调用中的JSON,就像下面这样。
callback({"name": "Nicholas"});
JSONP由两部分组成:
- 回调函数
- 数据
回调函数是当响应到来时应该在页面中调用的函数。回调函数的名字一般是在请求中指定的。而数据就是传入回调函数的JSON数据。
http://freegeoip.net/json/?callback=handleResponse
这个URL请求在请求一个JSONP地理定位服务。通过查询字符串来指定JSONP服务的回调函数是很常见的,就像上面URL所示,这里指定的回调函数名字叫handleResponse()
。
<script src="http://freegeoip.net/json/?callback=handleResponse"></script>
这个请求到达后端后,后端回去解析callback这个参数获取到字符串handleResponse,在发送数据做以下处理:
假设之前后端返回数据: {"city": "hangzhou", "weather": "晴天"}
,现在后端返回数据: handleResponse({"city": "hangzhou", "weather": "晴天"})
前端script标签在加载数据后会把 handleResponse({"city": "hangzhou", "weather": "晴天"})
做为 js 来执行,这实际上就是调用handleResponse
这个函数,同时参数是{"city": "hangzhou", "weather": "晴天"}
。 用户只需要在加载提前在页面定义好handleResponse
这个全局函数,在函数内部处理参数即可。
<script>
function handlerResponse(ret){
console.log(ret);
}
</script>
<script src="http://freegeoip.net/json/?callback=handleResponse"></script>
总结:
JSONP是通过 script 标签加载数据的方式去获取数据当做 JS 代码来执行 提前在页面上声明一个函数,函数名通过接口传参的方式传给后台,后台解析到函数名后在原始数据上「包裹」这个函数名,发送给前端。换句话说,JSONP 需要对应接口的后端的配合才能实现。
栗子
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div class="container">
<ul class="news">
</ul>
<button class="show">show news</button>
</div>
</body>
<script>
$('.show').addEventListener('click', function () {
var script = document.createElement('script');
script.src = 'http://127.0.0.1:8080/getNews?callback=handleResponse';
document.head.appendChild(script);
document.head.removeChild(script);
})
function handleResponse(news) {
var html = '';
for (var i = 0; i < news.length; i++) {
html += '<li>' + news[i] + '</li>';
}
console.log(html);
$('.news').innerHTML = html;
}
function $(id) {
return document.querySelector(id);
}
</script>
</html>
我们创建了一个<script>节点,并向其src属性赋值为一个跨域URL的Ajax请求(见node server)。在document文档头部添加了这个节点,当文档加载运行时,一旦文档树中有引用script标签,都会将其下载下来,所以届时会自动下载script脚本,只不过这个脚本的本质是JSON。removeChild是为了加载script后在移除它,保持整体语义。
接下来定义callback,这里回调函数的作用就是接受JSON字符串,作为参数进入函数转化为字符串,进而转换为HTML元素内容等等。而为其“包裹”上回调函数的行为,是在后端进行的
node server
var http = require("http");
var fs = require("fs");
var path = require("path");
var url = require("url");
var server = http.createServer(function (req, res) {
var pathObj = url.parse(req.url, true);
switch (pathObj.pathname) {
case "/getNews":
var news = ["News-A", "News-B", "News-C"];
res.setHeader("Content-Type", "text/json; charset=utf-8");
console.log(pathObj); // '/getNews?callback=handleResponse'
console.log(pathObj.query.callback); // handleResponse
if (pathObj.query.callback) {
res.end(pathObj.query.callback + "(" + JSON.stringify(news) + ")");
// handleResponse(["News-A","News-B","News-C"]) 服务端返回的数据
} else {
res.end(JSON.stringify(news));
}
break;
default:
fs.readFile(path.join(__dirname, pathObj.pathname), function (e, data) {
if (e) {
res.writeHead(404, "not found");
res.end("<h1>404 Fot Found</h1>")
} else {
res.end(data);
}
})
}
});
server.listen(8080);
console.log("visit http://localhost:8080/httpServer.html");
node代码如上,我们这里模拟的JSON是一个新闻列表。模拟了一个/getNews/
URL,使用script作为JS脚本下载并执行。
如果存在一个回调"值",就把此值当做作为JS函数名,并将JSON字符串传递进来。
点击后,我们就看到了一个来自跨域的HTML内容。
JSONP之所以在开发人员中极为流行,主要原因是它非常简单易用。与图像Ping相比,它的优点在于能够直接访问响应文本,支持在浏览器与服务器之间双向通信。不过,JSONP也有两点不足:
- 首先JSONP是从其他域中加载代码执行,如果其他域不安全,很可能会在响应中夹带一些恶意代码,而此时除了完全放弃JSONP调用之外,没有办法追究。因此在使用不是你自己运维的Web服务时,一定保证安全可靠。
- 确定JSONP请求是否失败不容易
————《JS高程》
跨域 —— CORS
通过XHR实现Ajax通信的一个主要限制,来源于跨域安全策略。默认情况下,XHR对象只能访问包含它的页面位于同一个域中的资源。这种安全策略可以预防某些恶意行为。但是,实现合理的跨域请求对开发某些浏览器应用程序也是直观重要的。
CORS(Cross-Origin Resource Sharing,跨域资源共享)是W3C的一个工作草案,定义了在必须访问跨域资源时,浏览器与服务器应该如何沟通。CORS背后思想,就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。
比如一个简单的使用GET和POST发送的请求,它没有自定义的头部,而主体内容是text/plain。在发送该请求时,需要给它附加一个额外的Origin头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。
总结:使用 XMLHttpRequest 发送请求时,浏览器发现该请求不符合同源策略,会给该请求加一个请求头:Origin,后台进行一系列处理,如果确定接受请求则在返回结果中加入一个响应头:Access-Control-Allow-Origin; 浏览器判断该相应头中是否包含 Origin 的值,如果有则浏览器会处理响应,我们就可以拿到响应数据,如果不包含浏览器直接驳回,这时我们无法拿到响应数据。
栗子
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div class="container">
<ul class="news">
</ul>
<button class="show">show news</button>
</div>
</body>
<script>
$('.show').addEventListener('click', function () {
var xhr = new XMLHttpRequest()
xhr.open('GET', 'http://127.0.0.1:8080/getNews', true)
xhr.send();
xhr.onload = function () {
appendHtml(JSON.parse(xhr.responseText))
}
})
function appendHtml(news) {
var html = '';
for (var i = 0; i < news.length; i++) {
html += '<li>' + news[i] + '</li>';
}
$('.news').innerHTML = html;
}
function $(selector) {
return document.querySelector(selector)
}
</script>
</html>
node server
var http = require("http");
var fs = require("fs");
var path = require("path");
var url = require("url");
var server = http.createServer(function (req, res) {
var pathObj = url.parse(req.url, true);
switch (pathObj.pathname) {
case "/getNews":
var news = ["News-A", "News-B", "News-C"];
res.setHeader('Access-Control-Allow-Origin','http://localhost:8080');
res.end(JSON.stringify(news));
break;
default:
fs.readFile(path.join(__dirname, pathObj.pathname), function (e, data) {
if (e) {
res.writeHead(404, "not found");
res.end("<h1>404 Fot Found</h1>")
} else {
res.end(data);
}
})
}
});
server.listen(8080);
console.log("visit http://localhost:8080/httpServer.html");
HTML中发送Ajax请求后http://127.0.0.1:8080/getNews
,会在请求头部自动加上Origin。
之后在后端设置了一个
res.setHeader('Access-Control-Allow-Origin','http://localhost:8080')
,表示接受来自http://localhost:8080的请求,在响应头部中打上了Access-Control-Allow-Origin
标记,返回数据后,浏览器会识别标记,确认通过拿到数据。
我们修改一下,假设后端只接收端口9000的跨域请求,那么会这样。
服务器一样会返回请求,在Preview中我们可以看到,但是请求域的端口不一样,于是被浏览器拦截了。
如若想通过任何域的请求,可以这样设置:
res.setHeader('Access-Control-Allow-Origin','*');