同源策略与跨域

1,359 阅读7分钟

什么是同源?

-- 两个 url 同协议、同域名、同端口,则这两个 url 同源

什么是同源策略?

-- 浏览器默认允许访问同源间的资源和操作同源间的 DOM ,如果想要访问不同源的资源或者操作 DOM ,则会受到浏览器制定的一套基础安全策略,这套安全策略就是同源策略

DOM 层面

-- 同源策略限制来自不同域的 Js 脚本对当前 DOM 对象的读写操作

数据层面

-- 同源策略限制了跨域的站点读取当前站点的 Cookie、indexDB、localStorage、sessionStorage等当前站点的本地存储数据
网络层面
-- 同源策略限制了通过 XMLHttpRequest 的方式将站点数据发送给跨域的站点。所以就会发生在开发中很常见的请求跨域问题

什么是同源跨域?

-- 解决浏览器同源策略的方法

浏览器做出的同源策略安全性与便利性的权衡


允许页面嵌入第三方资源

浏览器支持引用支持外部引用资源文件的。这容易引发 XSS 跨站脚本攻击问题,浏览器为了解决这个问题,引入了内容安全策略(CPS)-- 让服务器决定可以浏览器加载哪些第三方资源和执行哪些内联的 Javascript 脚本。

跨域资源共享(CORS)

允许浏览器向跨域服务器发起 XMLHttpRequest 请求,从而克服由于同源策略限制AJAX 只能同源使用的限制。实现需要浏览器和服务的同时支持,但关键是需要服务器实现 CORS 接口

跨文档消息机制

浏览器针对同源策略不能操作不同源的 DOM 的问题采取了跨文档消息机制来让出部分安全性来提升开发的便利性。如浏览器提供了 window.postMessage 接口来是不同源的文档直接进行通信

解决跨域的方案有哪些?

跨域资源共享(CORS)

-- 跨域资源共享区分简单和非简单的请求,相应做不同的实现,请求同时满足以下两个条件则为简单请求,否则为非简单请求
  • 请求方法为 HEAD 、GET 、POST 中的一种
  • HTTP 请求头信息不超过以下几个字段
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type 为以下几个值
      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain

针对简单请求的实现
  • 浏览器直接发出 CORS 请求(浏览器自动在请求头信息中添加 Origin 字段标识请求的来源,服务器根据配置的请求源决定是否返回这次请求)
    • 请求源在服务器允许范围内,服务器在响应头中会增加以下几个字段

//发起请求的请求源或者*,如果要允许浏览器发送 Cookie 则不能设置成*,必须是明确的请求源
Access-Control-Allow-Origin: http://xxx.com 
//值只能为 true,表示允许浏览器发送 Cookie 到服务器,需要客户端同时给 AJAX 请求设置 withCredentials = true 属性
//服务器在不需要浏览器发送 Cookie 时直接去掉该字段
Access-Control-Allow-Credentials: true 
//CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:
//Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、
//Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
Access-Control-Expose-Headers: FooBar

Content-Type: text/html; charset=utf-8
  • 请求源不在服务器允许范围内,服务器会返回一个正常的(不带上述几个字段)的响应。浏览器发现响应头中没有 Access-Control-Allow-Origin 字段,则在XMLHttpRequest 对象的 onerror 回调函数中捕捉错误(这种错误无法通过 HTTP 的状态码来识别,状态码有可能是 200)

针对非简单请求的实现(非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json)
  • 发起预检请求

-- 在正式通信之前,发起一次HTTP查询请求,当前网页所在的域名是否在服务器的许可名单之中,可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。OPTIONS /cors HTTP/ 1.1 //预检请求的方法
Origin:xxx.com // 请求源
Access-Control-Request-Method:POST //表明后续发送的 CORS 请求会应用到的请求方法
Access-Control-Request-Headers://指定后续发送的 CORS 请求会携带哪些额外的请求头字段
  • 预检请求回应
-- 服务器收到"预检"请求以后,检查了 Origin、Access-Control-Request-Method 和

Access-Control-Request-Headers 字段以后,确认允许跨域请求,做出回应// 表示服务器允许浏览器在下面字段值定义的域中进行跨域请求,
// * 号则表示允许所有域进行跨域请求
Access-Control-Allow-Origin: http://xxx.com
// 表明服务器支持的所有跨域请求的方法
Access-Control-Allow-Methods: GET, POST, PUT
// 表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段
Access-Control-Allow-Headers: X-Custom-Header
//值只能为 true,表示允许浏览器发送 Cookie 到服务器,需要客户端同时给 AJAX 请求设置 withCredentials = true 属性
//服务器在不需要浏览器发送 Cookie 时直接去掉该字段
Access-Control-Allow-Credentials: true
       --  如果浏览器否定"预检"请求(即服务器器不会返回 Access-Control-Allow-Origin 响应头),此时服务器会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。然后浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest 对象的 onerror 回调函数捕获。控制台会抛出错误:XMLHttpRequest cannot load http://xxx.com.
Origin http://xxx1.com is not allowed by Access-Control-Allow-Origin.
  • 浏览器发起正常的 CORS 请求(跟简单请求时一样)

JSONP

-- JSON with Padding的简称。是一个非官方的协议,允许在服务器端集成 Script 标签返回至客户端,通过javascript callback的形式实现跨域访问
  • 利用 script 标签的 src 属性来实现跨域(<script> 标签的 src 属性并不被同源策略约束,可以执行 src 引入的脚本)
  • 通过将前端方法作为参数传递到服务器端,然后由服务器端注入参数之后再返回,实现服务器端向客户端通信
-- 在本地定义一个名叫 callback 的回调函数(名字可以是自定义,但是要跟服务器上调用的名字相同),在服务器上调用这个回调函数并将需要的数据以 JSON 的形式作为 callback 的参数返回给本地
  • 只支持get方法

/* 
    新建client.html
    在启动了本地 8081 服务之后刷新页面
    前端定义一个 JSONP 的请求方法,用以跨域发送 JSONP 请求获取服务器数据 
*/
function jsonpFn(req){
    var script = document.createElement('script');
    var url = req.url + '?callback=' + req.callback.name;
    script.src = url;
    document.getElementsByTagName('head')[0].appendChild(script); 
}
/* 发起请求 */
jsonpFn({
    url : 'http://localhost:8081',
    callback:getDataFn 
});
/* 定义一个回调函数供服务端调用 */
function getDataFn(res){
    console.log(res);
}

/* 
    新建 server.js
    并使用 node 执行来启动本地 8081 的服务
    服务器端调用 jsonpFn 中定义的 callback 回调函数,并将数据作为 callback 的参数返回给前端 
*/
var http = require('http');
var urllib = require('url');
var port = 8081;
var data = {'data':'我是跨域服务器返回的数据'};
http.createServer(function(req,res){
    var params = urllib.parse(req.url,true);
    if(params.query.callback){
        /* 执行客户端传输的回调函数 */
        var str = params.query.callback + '(' + JSON.stringify(data) + ')';
        res.end(str);
    } else {
        res.end();
    }
}).listen(port,function(){
    console.log('jsonp server is on');
});