同源与跨域(一)

参考:
浏览器的同源策略
浏览器同源政策及其规避方法
同源政策


什么是同源策略?

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

源的定义

如果两个页面的协议端口(如果有指定)和域名都相同,则两个页面具有相同的源。

同源检测示例: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','*');

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容