跨域的前因后果

134 阅读11分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第N天,点击查看活动详情 >>

什么是跨域

同源策略(same origin policy)

同源 = 协议 + 主机 + 端口 + 子域名 均相同

如果没有同源策略,则会发生在 a.com 的脚本去获取 b.com 的站点里的信息

同源策略限制主体分类

  • Ajax同源策略:无法向非同源地址发送 AJAX 请求 或 fetch 请求(可以发送,但浏览器拒绝接受响应)
  • DOM同源策略也一样,它限制了不同源页面不能获取DOM,这样可以防止一些恶意网站在自己的网站中利用iframe嵌入正gui的网站并迷惑用户,以此来达到窃取用户信息。
  • storage同源策略:无法获取非同源网页的 cookie、localstorage 和 indexedDB。个人理解这一句话主要说的是 比如浏览完 A 页面后去 浏览 B 页面,那么在 B 页面拿不到 A 页面的 Cookie

为什么要有跨域

  • cookie 可以跨域的话必然造成身份信息泄露 → CSRF(跨站请求伪造)
  • DOM 可以跨域获取的话:iframe 嵌套 → 获取用户的输入信息

可以跨域的标签

<script>、<img>、<iframe>、<link>;

为什么他们可以跨域?这些标签本质上是去加载一种资源,使用的是GET请求,对于其返回的结果,开发人员的 JS脚本无法去读写,也就不会对客户端造成危害

XHR:同源策略的目标

通过目标域的HTTP头来授权是否允许跨域,因为开发者无法在HTTP到达浏览器之前篡改头部来欺骗浏览器

CORS

基础概念

CORS 是一个 W3C 标准,全称是 跨域资源共享(Cross-origin resource sharing) ,它允许浏览器向跨源服务器,发出XMLHttpRequest请求。

客户端请求头

OPTIONS /oapi/get HTTP/2
Host: position.csdnimg.cn
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:102.0) Gecko/20100101 Firefox/102.0
Access-Control-Request-Method: GET
Access-Control-Request-Headers: content-type

Referer: <https://blog.csdn.net/>
**Origin: <https://blog.csdn.net>**
Connection: keep-alive

Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
🚀 Origin 标记发起请求的 源URL,用来判断是否同源~
  • Access-Control-Request-Method: GET

  • Access-Control-Request-Headers: content-type

  • Sec-Fetch-Dest: empty

    • 表示请求的目的地,即如何使用这个元素,可以是比如 img / video 之类的
  • Sec-Fetch-Mode: cors

    • cors:跨域请求;
    • no-cors:限制请求只能使用请求方法(get/post/put)和请求头(accept/accept-language/content-language/content-type);
    • same-origin:如果使用此模式向另外一个源发送请求,显而易见,结果会是一个错误。你可以设置该模式以确保请求总是向当前的源发起的;
    • navigate:表示这是一个浏览器的页面切换请求(request)。 navigate请求仅在浏览器切换页面时创建,该请求应该返回HTML;
    • websocket:建立websocket连接;

💡 具体解释 1. cors表示跨域请求,且要求后端需要设置cors响应头;【OPTION也是这个呢】 2. no-cors并不是代表请求不跨域,而是服务端不设置cors响应头,什么情况下会是这种模式呢,图片/脚本/样式表这些请求是容许跨域且不用设置跨域响应头的,而no-cors也是默认的模式; 4. same-origin表示同源请求,这就限制了不能跨域,前面说的cors和no-cors是容许跨域的,只是要求服务端的设置不同而已,熟悉fetch接口的同学对mode属性应该不陌生,其实跟这里的含义是一样的,只是fetch的mode大家可以手动设置,而Sec-Fetch-Mode不能干预而已;

  • Sec-Fetch-Site: cross-site

    • cross-site:跨域请求;

    • same-origin:发起和目标站点源完全一致;

    • same-site:有几种判定情况,详见说明;

      • 一级域名 → 二级域名 【不跨域】
      • 二级域名 → 另一个二级域名 【跨域】
      • 二级域名 → 一级域名 【跨域】
    • none:如果用户直接触发页面导航,例如在浏览器地址栏中输入地址,点击书签跳转等,就会设置none;

服务端响应头

HTTP/2 204 No Content
date: Tue, 26 Jul 2022 08:23:26 GMT
access-control-allow-origin: <https://blog.csdn.net>
access-control-allow-methods: GET,PUT,POST,DELETE,OPTIONS
access-control-allow-credentials: true
access-control-allow-headers: Accept,Authorization,Cache-Control,
	Content-Type,DNT,If-Modified-Since,JWT-TOKEN,Keep-Alive,Origin,
	User-Agent,UserName,UserToken,X-Access-Token,X-App-ID,X-CustomHeader,
	X-Data-Type,X-Device-ID,X-Device-Signature,X-Mx-ReqToken,X-OS,X-Requested-With,
	body,uber-trace-id,x-csrf-token,x-log-apiversion,x-log-bodyrawsize,
	x-log-requestid,x-token,x-ca-signature-headers
access-control-max-age: 1728000
  • [必含] Access-Control-Allow-Origin : 允许的域名,只能填 *(通配符)或者单域名
  • [必含] Access-Control-Allow-Methods : 这允许跨域请求的 http 方法(常见有 POST、GET、OPTIONS)。
  • [可选] Access-Control-Allow-Headers : 这是对预请求当中 Access-Control-Request-Headers 的回复,和上面一样是以逗号分隔的列表,可以返回所有支持的头部。
  • [可选] Access-Control-Allow-Credentials :表示是否允许发送Cookie,只有一个可选值:true(必为小写)如果不包含cookies,请略去该项,而不是填写false。这一项与 XmlHttpRequest 对象当中的 withCredentials 属性应保持一致,即 withCredentials 为true时该项也为true;withCredentials 为false时,省略该项不写。反之则导致请求失败。
  • [可选] Access-Control-Max-Age: 以秒为单位的缓存时间。在有效时间内,浏览器无须为同一请求再次发起预检请求。

CORS 跨域的判定流程

  1. 浏览器 判断是否符合同域的限制,同域 → 直接发送;不同域名 → 跨域请求(Sec-Fetch-Site: cross-site)。
  2. 服务器收到浏览器跨域请求后,根据自身配置返回对应HTTP头。若未配置过任何允许跨域,则HTTP头里不包含 Access-Control-Allow-origin 字段,若配置过域名,则返回 Access-Control-Allow-origin + 对应配置规则里的域名的方式
  3. 浏览器根据接受到的 响应头里的 Access-Control-Allow-origin 字段做匹配,若无该字段,说明不允许跨域,从而抛出一个错误;若有该字段,则对字段内容和当前域名做比对,如果同源,则说明可以跨域,浏览器接受该响应;若不同源,则说明该域名不可跨域,浏览器不接受该响应,并抛出一个错误。

上面说到的两种类型的报错,控制台输出是不一样的:

  • 服务器允许跨域请求,但是 Origin 指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含 Access-Control-Allow-Origin 字段,就知道出错了,从而抛出一个错误,被 XMLHttpRequest的onerror 回调函数捕获。注意,这种错误无法通过状态码识别,因为 HTTP 回应的状态码有可能是200。
<!--控制台返回结果-->
 XMLHttpRequest cannot load <http://localhost/city.json>.
 The 'Access-Control-Allow-Origin' header has a value '<http://segmentfault.com>' that is not equal to the supplied origin.
 Origin '<http://www.zhihu.com>' is therefore notallowed access.
复制代码
  • 服务器不允许任何跨域请求
<!--控制台返回结果-->
XMLHttpRequest cannot load <http://localhost/city.json>.
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin '<http://www.zhihu.com>' is therefore not allowed access.
复制代码

预检请求 Option

对于简单请求不会发送预检请求,会直接发送请求,此刻要发送的信息已经被服务器接受 → 可能会写入数据库

对于复杂请求会发送预检请求,预检不通过就不发送消息 → 不会危害服务器

简单请求:使用 form 表单可以发出的

简单请求是指满足以下条件的(一般只考虑前面两个条件即可):

  1. 使用 GET、POST、HEAD 其中一种请求方法。

  2. HTTP的头信息不超出以下几种字段:

    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain
  3. 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;

  4. XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。 请求中没有使用 ReadableStream 对象。

对于简单请求,浏览器直接发起 CORS 请求,具体来说就是服务器端会根据请求头信息中的 origin 字段(包括了协议 + 域名 + 端口),来决定是否同意这次请求。

如果 origin 指定的源在许可范围内,服务器返回的响应,会多出几个头信息字段:

Access-Control-Allow-Origin: <http://xxx.xxx.com>
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

非简单请求

非简单请求时指那些对服务器有特殊要求的请求,比如请求方法是 putdelete,或者 content-type 的类型是 application/json。其实简单请求之外的都是非简单请求了。

非简单请求的 CORS 请求,会在正式通信之前,使用 OPTIONS 方法发起一个预检(preflight)请求到服务器,浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。

一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样了。

前端请求携带Cookie

  1. 前端请求时在request对象中配置"withCredentials": true

    XMLHttpRequest.withCredentials : 指示了是否该使用类似 cookies , authorization headers(头部授权)或者TLS客户端证书这一类资格证书来创建一个跨域请求。

    在同一个站点下使用withCredentials属性是无效的。

  2. 服务端在responseheader中配置"Access-Control-Allow-Origin", "<http://xxx>:${port}";

    不能设置为 “*”

  3. 服务端在responseheader中配置"Access-Control-Allow-Credentials", "true"

    要么不设置,要么就设置成 “true”, 不能设置成 false

🚀 推测: 复杂请求 Option 的响应中 携带了 `"Access-Control-Allow-Credentials", "true"` ;所以此刻发送的请求是有 Cookie 的;但是简单请求没有收到 `"Access-Control-Allow-Credentials", "true"` 此刻不见得 Cookie 能发出去

JSONP

JSONP,全称 JSON with Padding,为了解决跨域的问题而出现。利用 <script> 这种标签发起的 资源性请求 不受跨域限制,所以只限于 get 请求。

JSONP 基于两个原理:

  1. 动态创建 script,使用 script.src 加载请求跨过跨域
  2. script.src 加载的脚本内容为 JSONP: 即 PADDING(JSON) 格式

例如 script.src = "<https://demo/api/user?id=100&callback=padding>"

此刻浏览器会去加载这个 js 文件,后端要配合,从query部分摘出参数,以及将要执行的回调 callback=padding, 此刻前端一定在 window.padding 设置好了回调函数,只需要后端返回一个脚本字符串,在字符串里调用这个 padding,HTTP响应后,前端自然会执行这个回调

例如后端返回

let response = 'padding({name:'linjunjie'})'
//定义获取数据的回调方法
function getData(data) {
  console.log(data);
}

// 创建一个script标签,并且告诉后端回调函数名叫 getData
var body = document.getElementsByTagName('body')[0];
var script = document.gerElement('script');
script.type = 'text/javasctipt';
script.src = 'demo.js?callback=getData';
body.appendChild(script);

//script 加载完毕之后从页面中删除,否则每次点击生成许多script标签
script.onload = function () {
  document.body.removeChild(script);
}

webpack-dev-server

Webpack-dev-server

在编译之后不会写入到任何输出文件, 而是将bundle文件保留在内存中

Webpack Dev Middleware

webpack-dev-middleware 是一个封装器,它可以把webpack处理过的文件发送到一个server

  • webpack-dev-server在内部使用了它,然而它也可以作为一个单独的package来使用,以便根据需求进行更多自定义配置
  • 搭配一个服务器来使用它,比如express.
  • npm install --save express webpack-dev-middleware
const express = require("express")
const webpack = require("webpack")
const webpackDevMiddleware = require("webpack-dev-middleware")

const  app = express()
const config = require("./webpack.config")
const compiler = webpack(config)

app.use(webpackDevMiddleware(compiler,{
    publicPath:config.output.publicPath
}),()=>{
    console.log("这里是回调函数")
})

app.listen(3000,()=>{
    console.log("Server running")
})

Proxy

  • target:标识的是代理到的目标地址,比如/api/moment会被代理到 http://localhost:8888/api/moment

  • pathRewrite:默认情况下,我们的/api也会被写入到URL中,如果希望删除,可以使用

  • secure:默认情况下不接受转发到https的服务器,如果希望支持,设置为false

  • changeOrigin:表示是否更新代理后请求headers中的host地址

  • historyApiFallback:解决SPA页面在路由跳转后,进行页面刷新返回404的错误

    • boolean值:默认是false,如果设置为true,刷新的时候,返回404错误时,会自动返回index.html的内容
    • object值:可以配置rewrites属性 , 可以配置from来匹配路径,决定要跳到哪个页面,详情查阅官方文档。

document.domain

该方式只能用于二级域名相同的情况下,比如 a.test.comb.test.com 适用于该方式。

只需要给两个页面都添加 document.domain = 'test.com',通过在 a.test.com 创建一个 iframe,去控制 iframewindow,从而进行交互。

postMessage

window.postMessage 是一个 HTML5 的 api,允许两个窗口之间进行跨域发送消息。

这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息

// 发送消息端
var receiver = document.getElementById('receiver').contentWindow;
var btn = document.getElementById('send');
btn.addEventListener('click', function (e) {
    e.preventDefault();
    var val = document.getElementById('text').value;
    receiver.postMessage("Hello "+val+"!", "<http://res.42du.cn>");
});

// 接收消息端
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event){
  if (event.origin !== "<http://www.42du.cn>")
    return;
}