同源策略 -- 如何跨域

121 阅读4分钟

同源策略

浏览器的重要功能

  • 定义

    • 协议、端口、主机名(域名或IP)都相同称为 同源
  • 用途

    • 不同源的网页,不能共享数据/偷数据
  • 为什么只有浏览器有同源策略(插件,本地控制台等等都可以跨域)

    • 为了保证用户的数据安全
  • 不同源的两个网址如何互相请求数据(俗称 跨域)的方法:

    • CORS
    • JSONP
    • 反向代理

CORS

步骤:

  • 假设b.com:7777 想要访问 a.com:8888 的 /data接口(POST/GET)
  • 方法: 后端在 /data接口 设置 响应头: Access-Control-Allow-Origin: b.com:7777

企业微信截图_1686295213103.png

这样数据返回回来的响应头里就有:Access-Control-Allow-Origin: b.com:7777 这句,浏览器就会认为你们两个网站达成了某种协议,就不会阻止了

但是:在某些复杂的跨域请求上,只设置上面的响应头依旧会被浏览器阻止

浏览器把跨源资源共享机制(跨域)分成了简单请求和复杂请求

简单请求

若请求满足所有下述条件,则该请求可视为简单请求

备注:  Firefox 还没有将 Range 实现为安全的请求标头。参见 bug 1733981

  • Content-Type的值仅限于下列三者之一:

    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
  • 如果请求是使用 XMLHttpRequest 对象发出的,在返回的 XMLHttpRequest.upload 对象属性上没有注册任何事件监听器;也就是说,给定一个 XMLHttpRequest 实例 xhr,没有调用 xhr.upload.addEventListener(),以监听该上传请求。

  • 请求中没有使用 ReadableStream 对象。

  • 以上为简单请求的条件,简单请求跨域只要后端在响应头设置 Access-Control-Allow-Origin 即可

复杂请求

简单请求不同,“复杂请求”浏览器会先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响,如果预检请求不通过,那么该复杂请求不会发出去,直接被浏览器毙掉

如下是一个复杂请求

const xhr = new XMLHttpRequest();
xhr.open('POST', 'http://b.com:7777/data');
xhr.setRequestHeader('X-PINGOTHER', 'pingpong');
xhr.setRequestHeader('Content-Type', 'application/xml');
xhr.onreadystatechange = handler;
xhr.send('<person><name>Arun</name></person>');

上面的代码是一个POST请求,该请求包含了一个非标准的请求头:X-PINGOTHER,另外该请求的Content-Type为application/xml,这不符合简单请求的要求,所以这是一个复杂请求,浏览器会先发一个预检请求,如果不通过,请求则不会发出

  • 那么跨域复杂请求怎么处理呢

  • 后端处理设置响应头如下:

    • Access-Control-Allow-Origin: b.com:7777
    • Access-Control-Allow-Methods: POST,GET,OPTIONS
    • Access-control-Allow-Headers: X-PINGOTHER, Content-Type
  • 了解更多 : MDN-跨源资源共享(CORS)

总结:CORS解决跨域问题:需要后端去做

JSONP

利用了script标签不受同源策略约束,可以任意请求JS的原理(script标签可以执行一段JS,但是得不到JS的内容)

步骤

  • 假设b.com:7777 想要访问a.com:8888 的 /data接口
  • b.com:7777 前端写的
// 创建一个script标签
const script = document.createElement('script')
// 创建一个随机名字,并成为window的属性
let callName = 'JSONP' + Date.now()
window[callName] = (res) => {
    console.log('从a.com得到的数据', res)
}
// script的地址为http://a.com:8888/data加上callback参数为刚刚创建的随机名字,会下载这个文件并执行
script.src = "http://a.com:8888/data?callback="+callName
// 把script标签放在head下面
document.head.appendChild(script)
// 当script文件下载完毕,删除window[callName]函数
script.onload = () => {
    delete window[callName]
}
  • a.com:8888 后端写的
if (path === '/data') {
    response.statusCode = 200
    // 设置Content-Type为script
    response.setHeader('Content-Type', 'text/javascript;charset=utf-8')
    // 返回下面的字符串,前端js下载完后就会直接执行,那就相当于执行了window.${query.callback}这个函数,参数为....
    response.write(`window.${query.callback}({"name": "Jack"})`)
    response.end
}
  1. 前端创建一个script标签
  2. 创建一个随机名字, 如name1314
  3. 定义window.name1314这个函数
  4. 请求a.com:8888/data?callba…
  5. 把script标签放在head下面,这样页面一执行就能下载这个script,并执行里面的js
  6. 等script下载完毕后删除window.name1314这个方法
  7. 后端设置响应头的Content-Type为text/script
  8. 获取到传过来的callback参数(name1314),返回window.name1314({"name": "Jack"}), 这个函数里面的对象就是后端要传的值
  • 这样页面一打开,就会去下载a.com:8888/data这个路径里的代…
  • 相当于执行了 window.name1314({"name": "Jack"})
  • 而上面第3步的时候就定义了这个函数,所以在这个函数的形参就能拿到后端返回的值,拿到返回后做什么,可以在函数里面写

总结:JSONP现在已经不用了,因为缺点也很明显,它只能GET请求,而且也是需要后端支持,前端无法独立完成

反向代理

  • 一般通过nginx配置实现
  • 后端实现

总结

跨域三大解决方法

  • CORS
  • JSONP
  • 反向代理

都需要后端支持或者全由后端实现,前端没有办法依靠自己实现跨域操作