浏览器跨域

Posted by sqq5682 on May 13, 2024

浏览器跨域

浏览器的同源策略,浏览器的同源策略限制我们只能在相同的协议、IP地址、端口号相同,如果有任何一个不通,都不能相互的获取数据。并且,http和https之间也存在跨域,因为https一般采用的是443端口,http采用的是80端口或者其他。 同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指”协议+域名+端口”三者相同,即便两个不同的域名指向同一个ip地址,也非同源。 同源策略限制内容有:

1.Cookie、LocalStorage、IndexedDB 等存储性内容
2.DOM 节点
3.AJAX 请求发送后,结果被浏览器拦截了

但是有三个标签是允许跨域加载资源:

<img src=XXX>
<link href=XXX>
<script src=XXX>

常见的跨域场景: 当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。常见跨域场景如下图所示:

URL 说明 是否允许通信
http://www.a.com/a.js http://www.a.com/b.js 同一域名下 允许
http://www.a.com/lab/a.js http://www.a.com/script/b.js 同一域名下不同文件夹 允许
http://www.a.com:8000/a.js http://www.a.com/b.js 同一域名,不同端口 不允许
http://www.a.com/a.js https://www.a.com/b.js 同一域名,不同协议 不允许
http://www.a.com/a.js http://70.32.92.74/b.js 域名和域名对应ip 不允许
http://www.a.com/a.js http://script.a.com/b.js 主域相同,子域不同 不允许
http://www.a.com/a.js http://a.com/b.js 同一域名,不同二级域名(同上) 不允许(cookie这种情况兄下也不允许访问)
http://www.cnblogs.com/a.js http://www.a.com/b.js 不同域名 不允许

其实,跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了,常见的9种跨域方案,主要有: 通过jsonp跨域、 document.domain + iframe跨域、 location.hash + iframe、window.name +iframe跨域、postMessage跨域、跨域资源共享(CORS)、nginx代理跨域、nodejs中间件代理跨域、WebSocket协议跨域9种,并有着各自独特的跨域原理。

一、JSONP实现跨域

原理:jsonp实现跨域的原理是跨域的服务端把客户端所需要的数据放进客户端本地的一个js方法里,进行调用,客户端在本地的js对返回的数据进行处理。这样就实现了不同域名下的两个站点间的交流。

JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。

JSONP的实现流程

· 声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。

· 创建一个<script>标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)。

· 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是show(‘我不爱你’)。

· 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。

简单实例:

// 服务端相关
// 导入 http 内置模块
const http = require('http')
// 这个核心模块,能够帮我们解析 URL地址,从而拿到  pathname  query 
const urlModule = require('url')

// 创建一个 http 服务器
const server = http.createServer()
// 监听 http 服务器的 request 请求
server.on('request', function (req, res) {

  // const url = req.url
  const { pathname: url, query } = urlModule.parse(req.url, true)

  if (url === '/getscript') {
    // 拼接一个合法的JS脚本,这里拼接的是一个方法的调用
    // var scriptStr = 'show()'
    var data = {
      name: '张三',
      age: 3,
    }

    var scriptStr = `${query.callback}(${JSON.stringify(data)})`
    // res.end 发送给 客户端, 客户端去把 这个 字符串,当作JS代码去解析执行
    res.end(scriptStr)
  } else {
    res.end('404')
  }
})

// 指定端口号并启动服务器监听
server.listen(3000, function () {
  console.log('server listen at http://127.0.0.1:3000')
})
<!--前端相关-->
<body>
  <script>
    function showInfo(data) {
      console.log(data)
    }
  </script>
<script src="http://127.0.0.1:3000/getscript?callback=showInfo"></script>
</body>

二、document.domain 跨域(此方案仅限主域相同,子域不同的跨域应用场景)

如果两个窗口包含的脚本把 document.domain 设置成了相同的值,那么这两个窗口就不再受同源策略的约束,它们可以互相读取对方的属性。例如,home.example.com 和 developer.example.com 里的脚本要解除同源限制,可以把 document.domain 设置成 example.com。另外,domain 值中必须有一个点号,不能把它设置为“com”或其他顶级域名。

实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

(1)父窗口:(http://www.domain.com/a.html)

<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
    document.domain = 'domain.com';
    var user = 'admin';
</script>

(2)子窗口:(http://child.domain.com/b.html)

<script>
    document.domain = 'domain.com';
    // 获取父窗口中变量
    alert('get js data from parent ---> ' + window.parent.user);
</script>

三、location.hash+iframe跨域

这种方法是一个很奇妙的方法,比如有一个这样的url:http://www.xxx.com#abc=123,那么我们通过执行location.hash就可以得到这样的一个字符串#abc=123,同时改变hash页面是不会刷新的。

假如现在我们有A页面在7777端口(前端显示的文件),B页面在8888端口,后台运行在8888端口。我们在A页面中通过iframe嵌套B页面。

从A页面要传数据到B页面

我们在A页面中通过,修改iframe的src的方法来修改hash的内容。然后在B页面中添加setInterval事件来监听我们的hash是否改变,如果改变那么就执行相应的操作。比如像后台服务器提交数据或者上传图片这些。

从B页面传递数据到A页面

经过上面的方法,从B页面向A页面发送数据就是修改A页面的hash值了。对没错方法就是这样,但是我在执行的时候会出现一些问题。我们在B页面中直接:parent.location.hash = “#xxxx”

这样是不行的,因为前面提到过的同源策略不能直接修改父级的hash值,所以这里采用了一个我认为很巧妙的方法。部分代码:

try{
  // ie、chrome 的安全机制无法修改parent.Location.hash,
  // 利用一个中间htmL的代理修这Location.hash
  // 好A =>B =>C其中,当前页面是B,A,C在相同同的一个域下.B不能直接修改A的hash,可以通过修改C,让C修改A 
  // 文件地址:fontEndService/www/demo3/proxy.htmL
catch(e)
  parent.location.hash =`message=${JSON.stringify(data)}`;
  if(!ifrProxy){
    ifrProxy = document.createElement('iframe');
    ifrProxy.style.display = 'none';
    document.body.appendChild(ifrProxy);
  }
  ifrProxy.src = 'http://127.0.0.1:7777/demo3/proxy.html#message=${JSON.stringify(data)}';
}

在代理proxy.html页面中:

parent.parent.location.hash = self.location.hash.substring(1);

只需要写这样的一段js代码就完成了修改A页面的hash值,同样在A页面中也添加一个setInterval事件来监听hash值的改变。

实现的核心思路就是通过修改URL的hash值,然后用定时器来监听值的改变来修改。所以说最大的问题就是,我们传递的数据会直接在URL里面显示出来,不是很安全,同时URL的长度是一定的所以传输的数据也是有限的。

四、window.name+iframe跨域

原理就是window.name属性在于加载不同的页面(包括域名不同的情况下),如果name值没有修改,那么它将不会变化,并且这个值可以非常的长(2MB)

方法原理:A页面通过iframe加载B页面。B页面获取完数据后,把数据赋值给window.name.然后在A页面中修改iframe使他指向本域的一个页面。这样在A页面中就可以直接通过iframe.contentWindow.name获取到B页面中获取到的数据。

简单实例,第一个页面放在域名localhost,html如下

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>页面一</title>
</head>
<body>
  <h1>页面一</h1>
  <script>
    var iframe = document.createElement('iframe');//工具人
    iframe.style.display = "none";//工具人隐藏在背后默默付出
    var flag = false;
    iframe.onload = function () {
        if ( flag ) {
            var data = iframe.contentWindow.name;//contentWindow.name可以拿到iframe的窗口name值
            console.log(data);
            iframe.contentWindow.close();//关闭隐藏的页面
            document.body.removeChild(iframe);//删除隐藏的页面
        } else {
            flag = true;
            iframe.contentWindow.location = 'http://localhost/demo2.html';//这里因为浏览器同源策略,需要将链接改成与页面一同源的页面(也就是页面二),这里会再次触发load事件
        }
    }
    iframe.src = 'http://data/data.html';//跨域,这里窗口已经拿到name,所以上面更换地址后name的值依旧存在
    document.body.appendChild(iframe);
  </script>
</body>
</html>

第二个页面是空页面,也是在localhost域名下

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>页面二</title>
</head>
<body>
</body>
</html>

第三个页面是数据页面,放在data域名下

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>data</title>
</head>
<body>
    <h1>数据</h1>
<script>
  window.name = '{data:"数据"}';//将数据存到window.name里
</script>
</body>
</html>

五、postMessage跨域

postMessage是HTML5引入的API。他可以解决多个窗口之间的通信(包括域名的不同)。我个人认为他算是一种消息的推送,可以给每个窗口推送。然后在目标窗口添加message的监听事件。从而获取推送过来的数据。

简单实例

<!--a.html-->
<!DOCTYPE html>
<html>
<body>
<iframe id="iframe" src="http://domain-b.com/b.html"></iframe>
<script>
  window.onload = function() {
    var iframeWindow = document.getElementById('iframe').contentWindow;
    iframeWindow.postMessage('Hello, Domain B!', 'http://domain-b.com');
  };
 
  // 监听从b.html发送过来的消息
  window.addEventListener('message', function(event) {
    if (event.origin === "http://domain-b.com") {
      console.log("Received message: ", event.data);
    }
  });
</script>
</body>
</html>
<!--b.html-->
<!DOCTYPE html>
<html>
<body>
<script>
  // 监听从a.html发送过来的消息
  window.addEventListener('message', function(event) {
    if (event.origin === "http://domain-a.com") {
      console.log("Received message: ", event.data);
      // 向a.html发送消息
      event.source.postMessage('Hello, Domain A!', event.origin);
    }
  });
</script>
</body>
</html>

六、nginx代理跨域

nginx配置解决iconfont跨域,浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件eot|otf|ttf|woff|svg例外,此时可在nginx的静态资源服务器中加入以下配置。

location / {
  add_header Access-Control-Allow-Origin *;
}

nginx反向代理接口跨域

跨域原理:同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

nginx具体配置:

server{ #proxy 服务器
  listen 81;
  server_name WwM.domainz.com;
  location /{
    proxy_pass http://www.domain2.com:8080;#反向代理
    proxy_cookie_domain www.domain2.com WWW.domain1.com; #修改 cookie 里域名
    index index.html index.html;
    # 当用wekpack-dex=servex。等中间件代理接口访问 nign&。时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
    add header Access-Control-Allow-Origin http://www.domain1.com;#当前端只跨域不带cookie 时,可为*
    add_header Access-Control-Allow-Credentials true; # 表示是否允许发送Cookie,true 发送,false 否
  }
}

七、nodejs跨域

其实这种办法和上一种用nginx的方法是差不多的。都是你把请求发给一个中间人,由于中间人没有同源策略,他可以直接代理或者通过爬虫或者其他的手段得到想到的数据,然后返回(是不是和VPN的原理有点类似)。简单实例

const express = require('express');
const cors = require('cors');
 
const app = express();
 
// 使用cors中间件
app.use(cors());
 
// 其他路由和中间件
 
// 监听端口
const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

这样就设置了一个允许跨域请求的Express服务器。如果你需要更多的CORS配置选项,比如指定允许哪些源、方法和头部等,可以传递一个对象给cors中间件。

app.use(
  cors({
    origin: 'http://example.com', // 或使用函数来判断是否允许跨域
    methods: ['GET', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    exposedHeaders: ['Authorization'],
    credentials: true,
    maxAge: 3600 // 1小时内不需要再预检请求
  })
);

以上代码允许从http://example.com发起的跨域请求,并且只允许GET和POST方法,以及设置了Content-Type和Authorization头部。

八、webSocket跨域

webSocket主要是为了客服端和服务端进行全双工的通信。但是这种通信是可以进行跨端口的。所以说我们可以用这个漏洞来进行跨域数据的交互,基于webSocket构建一个客服端和服务端,简单实例

const express = require('express');
const WebSocket = require('ws');
const http = require('http');

// 初始化 Express 应用
const app = express();

// 创建 HTTP 服务器,并将其传递给 WebSocket 服务器
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

// Express 应用的路由处理(可选)
app.get('/', (req, res) => {
  res.send('WebSocket server is running');
});

// WebSocket 连接处理
wss.on('connection', (ws, req) => {
  console.log('New client connected from', req.headers['origin']);

  // 在这里可以添加基于 req.headers['origin'] 的验证逻辑
  // 如果不添加验证,则默认允许所有域连接

  // 监听客户端消息
  ws.on('message', (message) => {
    console.log('Received:', message);
    // 向客户端发送消息
    ws.send(`Server received: ${message}`);
  });

  // 监听连接关闭
  ws.on('close', () => {
    console.log('Client disconnected');
  });
});

// 监听端口
const PORT = process.env.PORT || 8080;
server.listen(PORT, () => {
  console.log(`Express and WebSocket server is listening on port ${PORT}`);
});

<!DOCTYPE html>
<html>
<head>
  <title>WebSocket Cross-Origin Example</title>
</head>
<body>
  <h1>WebSocket Cross-Origin Example</h1>
  <script>
    // 假设 WebSocket 服务器运行在 ws://localhost:8080
    // 若服务器支持 HTTPS,则应使用 wss:// 协议
    const ws = new WebSocket('ws://localhost:8080');

    // 连接打开时触发
    ws.onopen = () => {
      console.log('WebSocket connection opened');
      // 向服务器发送消息
      ws.send('Hello from client');
    };

    // 接收到消息时触发
    ws.onmessage = (event) => {
      console.log('Received from server:', event.data);
    };

    // 连接关闭时触发
    ws.onclose = () => {
      console.log('WebSocket connection closed');
    };

    // 连接错误时触发
    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };
  </script>
</body>
</html>

九、跨域资源共享(CORS)

跨源资源共享(CORS,或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。 跨源 HTTP 请求的一个例子:运行在 https://domain-a.com 的 JavaScript 代码使用XMLHttpRequest来发起一个到 https://domain-b.com/data.json 的请求。 出于安全性,浏览器限制脚本内发起的跨源 HTTP 请求。例如,XMLHttpRequest 和 Fetch API 遵循同源策略。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非响应报文包含了正确 CORS 响应头。

CORS 机制允许 Web 应用服务器进行跨源访问控制,从而使跨源数据传输得以安全进行。现代浏览器支持在 API 容器中(例如XMLHttpRequest 或 Fetch使用 CORS,以降低跨源 HTTP 请求所带来的风险。

跨源资源共享(CORS)使用场景

这份 cross-origin sharing standard 允许在下列场景中使用跨站点 HTTP 请求:

  1. 由XMLHttpRequest 或 Fetch APIs 发起的跨源 HTTP 请求。
  2. Web 字体 (CSS 中通过 @font-face 使用跨源字体资源),因此,网站就可以发布 TrueType 字体资源,并只允许已授权网站进行跨站调用。
  3. WebGL 贴图
  4. 使用 drawImage 将 Images/video 画面绘制到 canvas。
  5. 来自图像的 CSS 图形

功能概述

跨源资源共享标准新增了一组 HTTP 标头字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(例如 Cookie 和 HTTP 认证相关数据)。 CORS 请求失败会产生错误,但是为了安全,在 JavaScript 代码层面无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。

预检请求

我们都知道浏览器的同源策略,就是出于安全考虑,浏览器会限制从脚本发起的跨域HTTP请求,像XMLHttpRequest和Fetch都遵循同源策略。 浏览器限制跨域请求一般有两种方式:

  1. 浏览器限制发起跨域请求
  2. 跨域请求可以正常发起,但是返回的结果被浏览器拦截了

一般浏览器都是第二种方式限制跨域请求,那就是说请求已到达服务器,并有可能对数据库里的数据进行了操作,但是返回的结果被浏览器拦截了,那么我们就获取不到返回结果,这是一次失败的请求,但是可能对数据库里的数据产生了影响。 为了防止这种情况的发生,规范要求,对这种可能对服务器数据产生副作用的HTTP请求方法,浏览器必须先使用OPTIONS方法发起一个预检请求,从而获知服务器是否允许该跨域请求:如果允许,就发送带数据的真实请求;如果不允许,则阻止发送带数据的真实请求。 HTTP请求包括: 简单请求和需预检的请求

1. 简单请求

简单请求不会触发CORS预检请求,“简属于 单请求”术语并不属于Fetch(其中定义了CORS)规范。 若满足所有下述条件,则该请求可视为“简单请求”: 使用下列方法之一:

GET     
HEAD     
POST     
Content-Type: (仅当POST方法的Content-Type值等于下列之一才算做简单需求)      
text/plain      
multipart/form-data     
application/x-www-form-urlencoded 
2.需预检的请求

“需预检的请求”要求必须首先使用OPTIONS方法发起一个预检请求到服务区,以获知服务器是否允许该实际请求。“预检请求”的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。 当请求满足下述任一条件时,即应首先发送预检请求,使用了下面任一 HTTP 方法:

PUT    
DELETE    
CONNECT     
OPTIONS    
TRACE    
PATCH     

人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。该集合为:

Accept    
Accept-Language    
Content-Language     
Content-Type     
DPR    
Downlink     
Save-Data    
Viewport-Width    
Width    

Content-Type的值不属于下列之一:

application/x-www-form-urlencoded     
multipart/form-data     
text/plain   

如下是一个需要执行预检请求的HTTP请求:

var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = '<?xml version="1.0"?><person><name>Arun</name></person>';
 
function callOtherDomain(){
  if(invocation)
    {
      invocation.open('POST', url, true);
      invocation.setRequestHeader('X-PRODUCT', 'H5');
      invocation.setRequestHeader('Content-Type', 'application/xml');
      invocation.onreadystatechange = handler;
      invocation.send(body); 
    }
}
......

上面的代码使用POST请求发送一个XML文档,该请求包含了一个自定义的首部字段(X-PRODUCT:H5)。另外,该请求的Content-Type为application/xml。因此,该请求需要首先发起“预检请求”。

 OPTIONS /resources/post-here/ 
 HTTP/1.1
 Host: bar.other
 User-Agent: Mozilla/5.0 (Macintosh; U; 5.Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
 Accept-Language: en-us,en;q=0.5
 Accept-Encoding: gzip,deflate
 Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
 Connection: keep-alive
 Origin: http://foo.example
 Access-Control-Request-Method: POST
 Access-Control-Request-Headers: X-PINGOTHER, Content-Type
 
 HTTP/1.1 200 OK
 Date: Mon, 01 Dec 2008 01:15:39 GMT
 Server: Apache/2.0.61 (Unix)
 Access-Control-Allow-Origin: http://foo.example
 Access-Control-Allow-Methods: POST, GET, OPTIONS
 Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
 Access-Control-Max-Age: 86400
 Vary: Accept-Encoding, Origin
 Content-Encoding: gzip
 Content-Length: 0
 Keep-Alive: timeout=2, max=100
 Connection: Keep-Alive
 Content-Type: text/plain

从上面的报文中可以看到,第1~12行发送了一个使用OPTIONS方法的预检请求。 OPTIONS是HTTP/1.1协议中定义的方法,用以从服务器获取更多信息。该方法不会对服务器资源产生影响。遇见请求中同时携带了下面两个首部字段:

Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PRODUCT

首部字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法。首部字段 Access-Control-Request-Headers 告知服务器,实际请求将携带两个自定义请求首部字段:X-PINGOTHER 与 Content-Type。服务器据此决定,该实际请求是否被允许。

第14~26行 为预检请求的响应,表明服务器将坚守后续的实际请求。重点看第17~20行:

Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400

首部字段 Access-Control-Allow-Methods 表明服务器允许客户端使用 POST,GET 和 OPTIONS 方法发起请求。 首部字段Access-Control-Allow-Headers 表明服务器允许请求中携带字段X-PINGOTHER 与Content-Type。与 Access-Control-Allow-Methods一样,Access-Control-Allow-Headers的值为逗号分割的列表。 最后,首部字段 Access-Control-Max-Age 表明该响应的有效时间为 86400 秒,也就是 24 小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。 预检请求完成之后,发送实际请求:

POST /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: http://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: http://foo.example
Pragma: no-cache
Cache-Control: no-cache
 
 
<?xml version="1.0"?><person><name>Arun</name></person>
 
 
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain

[Some GZIP'd payload]
 

预检请求与重定向

并不是所有浏览器都支持预检请求的重定向。如果一个预检请求发生了重定向,一部分浏览器将报告错误。 CORS 最初要求浏览器具有该行为,不过在后续的修订中废弃了这一要求。但并非所有浏览器都实现了这一变更,而仍然表现出最初要求的行为。 在浏览器的实现跟上规范之前,有两种方式规避上述报错行为:

  1. 在服务端去掉对预检请求的重定向;
  2. 将实际请求变成一个简单请求。

如果上面两种方式难以做到,我们仍有其他办法:

  1. 发出一个简单请求(使用 Response.url 或 XMLHttpRequest.responseURL)以判断真正的预检请求会返回什么地址。
  2. 发出另一个请求(真正的请求),使用在上一步通过 Response.url 或 XMLHttpRequest.responseURL 获得的 URL。

不过,如果请求是由于存在 Authorization 字段而引发了预检请求,则这一方法将无法使用。这种情况只能由服务端进行更改。

附带身份凭证的请求

备注:当发出跨源请求时,第三方 cookie 策略仍将适用。无论如何改变本章节中描述的服务器和客户端的设置,该策略都会强制执行。

XMLHttpRequest 或 Fetch 与 CORS 的一个有趣的特性是,可以基于 HTTP cookies 和 HTTP 认证信息发送身份凭证。一般而言,对于跨源 XMLHttpRequest 或 Fetch 请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要设置 XMLHttpRequest 对象的某个特殊标志位,或在构造 Request 对象时设置。

本例中,https://foo.example 的某脚本向 https://bar.other 发起一个 GET 请求,并设置 Cookies。在 foo.example 中可能包含这样的 JavaScript 代码:

const invocation = new XMLHttpRequest();
const url = "https://bar.other/resources/credentialed-content/";

function callOtherDomain() {
  if (invocation) {
    invocation.open("GET", url, true);
    invocation.withCredentials = true;
    invocation.onreadystatechange = handler;
    invocation.send();
  }
}

第 7 行将XMLHttpRequest的withCredentials标志设置为true,从而向服务器发送 Cookies。因为这是一个简单GET请求,所以浏览器不会对其发起“预检请求”。但是,如果服务器端的响应中未携带Access-Control-Allow-Credentials: true,浏览器将不会把响应内容返回给请求的发送者。

预检请求和凭据

CORS 预检请求不能包含凭据。预检请求的响应必须指定 Access-Control-Allow-Credentials: true 来表明可以携带凭据进行实际的请求。

附带身份凭证的请求与通配符,在响应附带身份凭证的请求时:

  1. 服务器不能将 Access-Control-Allow-Origin 的值设为通配符“*”,而应将其设置为特定的域,如: Access-Control-Allow-Origin: https://example.com。
  2. 服务器不能将 Access-Control-Allow-Headers 的值设为通配符“*”,而应将其设置为标头名称的列表,如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
  3. 服务器不能将 Access-Control-Allow-Methods 的值设为通配符“*”,而应将其设置为特定请求方法名称的列表,如:Access-Control-Allow-Methods: POST, GET

对于附带身份凭证的请求(通常是 Cookie),

这是因为请求的标头中携带了 Cookie 信息,如果 Access-Control-Allow-Origin 的值为“*”,请求将会失败。而将 Access-Control-Allow-Origin 的值设置为 https://example.com,则请求将成功执行。

另外,响应标头中也携带了 Set-Cookie 字段,尝试对 Cookie 进行修改。如果操作失败,将会抛出异常。

Ajax、fetch、axios携带cookie设置

ajax

原生Ajax请求(添加xhr.withCredentials = true)

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com', true);
xhr.withCredentials = true;
xhr.send();

fetch

fetch请求(添加credentials = ‘include’)

fetch('http://example.com', {
    credentials: 'include'
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
axios
const axios = require('axios');
 
// 创建axios实例
const instance = axios.create({
  withCredentials: true, // 允许发送cookies
  baseURL: 'http://your-backend-domain.com',
});
 
// 发送请求
instance.get('/some-endpoint')
  .then(response => {
    // 处理响应
  })
  .catch(error => {
    // 处理错误
  });

Ajax、fetch、axios区别

Ajax

英译过来是Aysnchronous JavaScript And XML,直译是异步JS和XML(XML类似HTML,但是设计宗旨就为了传输数据,现已被JSON代替),解释一下就是说以XML作为数据传输格式发送JS异步请求。但实际上ajax是一个一类技术的统称的术语,包括XMLHttpRequest、JS、CSS、DOM等,它主要实现网页拿到请求数据后不用刷新整个页面也能呈现最新的数据。

fetch

fetch是ES6出现的,自然功能比xhr更强,主要原因就是它是基于Promise的,它返回一个Promise,因此可以使用.then(res => )的方式链式处理请求结果,这不仅提高了代码的可读性,还避免了回调地狱(xhr通过xhr.onreadystatechange= () => {}这样回调的方式监控请求状态,要是想在请求后再发送请求就要在回调函数内再发送请求,这样容易出现回调地狱)的问题。而且JS自带,语法也非常简洁,几行代码就能发起一个请求,用起来很方便

axios

axios是用于网络请求的第三方库,它是一个库。axios利用xhr进行了二次封装的请求库,xhr只是axios中的其中一个请求适配器,axios在nodejs端还有个http的请求适配器;axios = xhr + http;它返回一个Promise。

参考地址