理解跨域,我认为先理清同源策略

850 阅读7分钟

最近的面试经历让我发现,知其然知其所以然,不能光说出来方法,更要理解它为什么要这么做 。梳理下为什么会出现同源策略,同时引出相应的跨域方法。

Same-Origin Policy on its own increases security but is not enough to prevent all Cross-Site Request Forgery (CSRF) attacks, which basically are an attempt to take advantage of different origins. That is why anti-CSRF tokens should still be used as an additional form of protection. SOP is also completely useless as a method of protection against Cross-site Scripting (XSS) because it would have to limit loading of scripts from different sites and that would completely hinder the functionality of web applications. www.acunetix.com/blog/web-se…


上面简单来说同源策略本身是增加了安全性,但是不能防止所有Cross-Site Request Forgery (CSRF)和Cross-site Scripting (XSS),这些攻击基本上是企图利用不同源。所以它相当于浏览器基本的保护,但是限制更多会导致开发困难,所以合理的准守策略,不应该什么都跨域。

同源策略(Same-origin Policy )最早是Netscape 工程师解决加载资源管理的关边界关系,出现的规则。满足协议、 URL 和端口一致。如果在没有同源策略的情况,你被诱导进入假的银行网站,但是里面用iframe加载真的银行网站,你进入之后所以信息都会被假的网站获取iframe里面的窃取信息。

同源策略其实不光是针对请求API,操作iframe里的docment等,其实Web上所以的资源都是应用这种检查机制。

同源策略只会出现在浏览器下,有时候发送请求但是控制台显示失败,不是没法送出去,是浏览器拦截你的响应,服务器这个时候已经接到的你的请求。

源的继承

如果打开一个新的窗口about:blank和javascript:URL 执行脚本,用这个窗口执行脚本也会使用创建它的父级源。

    let blankTab = window.open('about:blank')
    
    //在新打开的about:blank
    //document.domain等于父级源

源的更改

但是只能修改到父级域名


    //store.company.com =>  company.com
    document.domain = "company.com";
    //以下会触发异常
    //1.在iframe元素里document  因为源不同,所以不能设置iframe里面的document到父级域名
    //2.document没有browsing context
    //3.document.domain 为null
    //4.document.domain 不为有效值
    //5.响应头 Feature-Policy:enabled

跨域访问可以分为三种情况

  1. Cross-origin writes 和Cross-origin embedding通常是允许的,Cross-origin writes 跳转,重定向连接跳到另一个网站。Cross-origin embedding 加载CSS,JavaScript,多媒体等资源。
  2. Cross-origin reads 通常是不允许的,就像你用canvas在里面读取跨域图片会报跨域问题(例如img标签可以加载图片,属于直接嵌入不能读取和写入图片的信息)。
类型 说明
iframe 跨域的话需响应头配置X-Frame-Options,同时跨域不能访问iframe里面的docment
CSS 允许 @import加载跨域资源
image 允许嵌入,读取受限
multimedia 允许嵌入
script 允许嵌入,但是会拦截跨域请求的api

JSONP就是利用script标签可以加载

 //http://www.example.com/jsonp?callback=foo
//返回数据 foo([1,2,3,4]);

function jsonp(url, jsonp, callback) {

  //创建触发script标签
  let script = document.createElement('script')
  script.src = url
  script.type = 'text/javascript'
  document.body.appendChild(script)

  //定义获取数据接收后的函数
  window[jsonp] = (data)=> {
    callback(data)
  }
  document.body.appendChild(script)
}

jsonp('http://www.example.com/jsonp', 'foo',(data)=>console.log(data))

跨源脚本可以访问的API

其实大部分的属性只读,方法都是功能性的例如关闭,跳转等,window.postMessage 属于双方约定的形式,暴露出来的操作相对安全。

方法 说明
window.close 正常
window.focus 正常
window.postMessage 正常
location.replace 正常
window.blur 正常

属性 读写权限
window.closed Read only
window.frames Read only
window.length Read only
window.location Read/Write
window.opener Read only
window.self Read only
window.parent Read only
window.top Read only
window.window Read only


Cross-Origin Resource Sharing (CORS)

其实可以看出要是遵循浏览器的跨域规则也能使用的,再不行也有JSONP。但是JSONP是单向只读的,而随着Web的发展满足不了现在的需求,需要一个又能宽松和安全的策略,浏览器厂商们就制订解决方案,但这个时候微软在IE9和IE8中XDomainRequest是它的解决方案。但Chrome, Firefox和一些其他厂商实现的就是CORS,随着Chrome的崛起,微软只能在IE10上兼容CORS和XDomainRequest

其实简单说的话,就是约定服务器给的响应头来判断跨域请求是否允许,例如常见的Access-Control-Allow-Origin: www.example.com 就是允许来着网站请求www.example.com。

详细说的话其实主要分两种情况简单的请求(Simple Request)和预检请求(Preflight Request)。这样就可以解释为什么我们用POST和GET请求也会触发option探测请求了,因为现在有时候开发验证用JWT需要约定Authorization: Bearer <token> 请求头,或者跟服务器协商约定的请求头,都会触发预检请求(Preflight Request)。

这也能解释为什么websocket能跨域因为cors和sop没有约束它

  1. 完成 websocket 不需要 HTTP 响应数据,
  2. 数据传输通过 WebSocket 协议进行。

想了解更多可以看看这篇文章

blog.securityevaluators.com/websockets-…

下面就是二种情况,大家也可以结合项目对应看看满足了哪些条件,触发了什么请求也能更好地理解。

简单请求(Simple Request)

  • HTTP方法:
    • GET
    • HEAD
    • POST
  • 不能设置集合之外的其他首部字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • Content-Type 的值仅限于下列三者之一:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

预检请求(Preflight Request)

  • 使用了下面任一 HTTP 方法:
    • PUT
    • DELETE
    • CONNECT
    • OPTIONS
    • TRACE
    • PATCH
  • 设置集合之外的其他首部字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • Content-Type 的值不为下列三者之一:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

HTTP响应头

这些需要服务端配置的响应头

响应头 例子
Access-Control-Allow-Origin Access-Control-Allow-Origin: http://www.example.com
Access-Control-Expose-Headers Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
Access-Control-Max-Age Access-Control-Max-Age: 600
Access-Control-Allow-Credentials Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers Access-Control-Allow-Headers: X-Custom-Header

如果需要带Cookie的时候

    // 服务端响应头需要添加 Access-Control-Allow-Credentials: true
    // 这样服务器和客户端就可以互相发送Cookie了
    // 需要注意服务端响应头Access-Control-Allow-Origin:“*”	的时候请求会失败,应该设置相应的域名
    // 例:Access-Control-Allow-Origin:“127.0.0.1”
    
        //  客户端代码
        let xhr = new XMLHttpRequest();
        let url = 'http://127.0.0.1/api/user';
        xhr.open('get',url,true)
        xhr.withCredentials = true;
        xhr.send();

反向代理

其实本身代理跟主题没什么关系,但为了完备性吧,因为我们要通过代理目的是为了做到同源,通过在一个和你同源的服务器上,把跨域请求的响应都通过代理服务器接收这样就是同源的了。正向代理是服务于客户端的,相当于让服务端感知不到客户端例如大家用的SSR。反向代理服务于服务端的,隐藏服务端例如负载均衡用户不知道他访问的是最后是什么服务器。因为我们要屏蔽服务端这里选用反向代理。

选自知乎阿笠硕士

这里借助http-proxy-middleware,就是webpack-dev-server里的proxy。这样请求/api的时候相当于请求http://otherDomain:1234/api

    let express = require('express');
    const proxy = require('http-proxy-middleware')
    const app = express()
    app.use(express.static(__dirname + '/'))
    app.use('/api', proxy({ target: 'http://localhost:1234/api', changeOrigin: false }));
    module.exports = app

总结

其实把同源策略理清,就能看见很多大家用的跨域方法,这样能更好的记忆,而且遇到问题能够更好的应对。我建议开两台本地的服务测试下,能够更好的理解。



第一次写博文,有什么错误请指正,对我来说也是一个交流的机会,谢谢大家~


参考链接

  1. developer.mozilla.org/en-US/docs/…
  2. www.acunetix.com/blog/web-se…
  3. developer.mozilla.org/en-US/docs/…
  4. web.dev/same-origin…
  5. css.csail.mit.edu/6.858/2015/…
  6. www.netsparker.com/whitepaper-…