# 同源策略

同源策略/SOP(Same origin policy)是一种约定,是浏览器最核心基本的安全功能,缺少了同源策略,很容易受到XSS、CSFR等攻击。所谓同源是指 "协议+域名+端口" 三者相同。

同源策略限制以下几种行为:

  1. Cookie、LocalStorage 和 IndexDB 无法读取
  2. DOM 和 Js对象无法获得
  3. AJAX 请求不能发送

注意

  1. 如果不配置'Access-Control-Allow-Headers':'content-type',也会产生跨域问题,在application/json时,而application/x-www-form-urlencoded不受影响
  2. post请求不管是form表单还是json格式,都可以去读取请求体和url中拼接的数据,两者如果同时存在也不会报错
  3. 注意跨域不同报错

# 跨域解决方案

  1. 通过jsonp跨域
  2. document.domain + iframe跨域
  3. location.hash + iframe
  4. window.name + iframe跨域
  5. postMessage跨域
  6. 跨域资源共享(CORS)
  7. nginx代理跨域
  8. nodejs中间件代理跨域
  9. WebSocket协议跨域

# 通过jsonp跨域

通常为了减轻web服务器的负载,把js、css,img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,可以通过动态创建script,再请求一个带参网址实现跨域通信。

  1. 原生实现:
if(req.url.includes("t1")){
	var t1={
		k1:1000
	}
	var tm =req.url.split("?")[1].split("=")[1];
	console.log(tm)
	t1=JSON.stringify(t1)
	var t2=(`${tm}(${t1})`)

	res.end(t2)
}
var script = document.createElement('script');
	    script.type = 'text/javascript';
	
	    // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
	    script.src = 'http://localhost:9000/t1?callback=handleCallback';
	    document.head.appendChild(script);
	
	    // 回调执行函数
	    function handleCallback(res) {
			console.log(res)
	        alert(JSON.stringify(res));
	    }
	
//script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
//handleCallback({"status": true, "user": "admin"})
  1. jquery ajax:
$.ajax({
    url: 'http://www.domain2.com:8080/login',
    type: 'get',
    dataType: 'jsonp',  // 请求方式为jsonp
    jsonpCallback: "handleCallback",    // 自定义回调函数名
    data: {}
});
  1. vue.js:
this.$http.jsonp('http://www.domain2.com:8080/login', {
    params: {},
    jsonp: 'handleCallback'
}).then((res) => {
    console.log(res); 
})
//后端node.js代码示例:

var querystring = require('querystring');
var http = require('http');
var server = http.createServer();

server.on('request', function(req, res) {
    var params = qs.parse(req.url.split('?')[1]);
    var fn = params.callback;

    // jsonp返回设置
    res.writeHead(200, { 'Content-Type': 'text/javascript' });
    res.write(fn + '(' + JSON.stringify(params) + ')');

    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

jsonp缺点:只能实现get一种请求。

# document.domain + iframe跨域

此方案仅限主域相同,子域不同的跨域应用场景。

实现原理:两个页面都通过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>
  1. 子窗口:(http://child.domain.com/b.html)
<script>
    document.domain = 'domain.com';
    // 获取父窗口中变量
    alert('get js data from parent ---> ' + window.parent.user);
</script>

# location.hash + iframe跨域

实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。

  1. a.html:(http://www.domain1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html传hash值
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);
    
    // 开放给同域c.html的回调方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>
  1. b.html:(http://www.domain2.com/b.html)
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>
  1. c.html:(http://www.domain1.com/c.html)
<script>
    // 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>

# window.name + iframe跨域

window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

  1. a.html:(http://www.domain1.com/a.html)
var proxy = function(url, callback) {
    var state = 0;
    var iframe = document.createElement('iframe');

    // 加载跨域页面
    iframe.src = url;

    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    iframe.onload = function() {
        if (state === 1) {
            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
            callback(iframe.contentWindow.name);
            destoryFrame();

        } else if (state === 0) {
            // 第1次onload(跨域页)成功后,切换到同域代理页面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        }
    };

    document.body.appendChild(iframe);

    // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
    function destoryFrame() {
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
};

// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
    alert(data);
});
  1. proxy.html:(http://www.domain1.com/proxy.... 中间代理页,与a.html同域,内容为空即可。

  2. b.html:(http://www.domain2.com/b.html)

<script>
    window.name = 'This is domain2 data!';
</script>

总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

# postMessage跨域

otherWindow.postMessage(message, targetOrigin, [transfer]);

otherWindow:其他窗口的一个引用,比如 iframe 的 contentWindow 属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。 targetOrigin:通过窗口的 origin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个 URI。

window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为 https),端口号(443 为 https 的默认值),以及主机 (两个页面的模数 Document.domain设置为相同的值) 时,这两个脚本才能相互通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。

从广义上讲,一个窗口可以获得对另一个窗口的引用(比如 targetWindow = window.opener),然后在窗口上调用 targetWindow.postMessage() 方法分发一个 MessageEvent 消息。接收消息的窗口可以根据需要自由处理此事件。传递给 window.postMessage() 的参数(比如 message)将通过消息事件对象暴露给接收消息的窗口。

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

  1. 页面和其打开的新窗口的数据传递
  2. 多窗口之间消息传递
  3. 页面与嵌套的iframe消息传递
  4. 上面三个场景的跨域数据传递

用法:postMessage(data,origin)方法接受两个参数 data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。

origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

# iframe嵌入通信

父组件给子组件发消息 iframeID.contentWindow.postMessage() 子组件给父组件爱你发消息 window.parent.postMessage() || top.postMessage()

<h1>父页面</h1>
<!-- <iframe id="iframe" src="http://127.0.0.1:8848/word/demo1/index2.html"></iframe> -->
<iframe id="iframe" src="./index2.html"></iframe>
<script>
	const iFrame = document.getElementById('iframe')
	//需要等到iframe中的子页面加载完成后才发送消息,否则子页面接收不到消息
	iFrame.onload = function(){
	  // <!-- iFrame.contentWindow获取到iframe的window对象 -->
	  iFrame.contentWindow.postMessage('父页面发送的消息','*');
	}
	
	
	window.addEventListener('message',e=>{
	    //<!-- 对消息来源origin做一下过滤,避免接收到非法域名的消息导致的xss攻击 -->
	    // if(e.origin===''){
			console.warn(e)
	        console.log(e.origin) //子页面URL,这里是http://b.index.com
	        console.log(e.source) // 子页面window对象,全等于iframe.contentWindow
	        console.log(e.data) //子页面发送的消息
	    // }
	},false)
	
</script>
<html>
	<button id='child'>child</button>
</html>
<script>
	// 有发送就有接收,与postMessage配套使用的就是message事件
	window.addEventListener('message',e=>{
	    //<!-- 对消息来源origin做一下过滤,避免接收到非法域名的消息导致的xss攻击 -->
	    // if(e.origin==='http://a.index.com'){
	        console.log(e.origin) //父页面URL,这里是http://a.index.com
	        console.log(e.source) // 父页面window对象,全等于window.parent/window.top
	        console.log(e.data)  //父页面发送的消息
	    // }
	},false)
	
	const child =document.getElementById('child')
	child.onclick =function(){
		window.parent.postMessage('子页面发送的消息','*')
	}
	
</script>

# 演示全代码

<!-- A页面 -->
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title></title>
	</head>
	<body>
		<button onclick="openHtml()">打开B页面</button>
		<button onclick="send()">发送消息</button>
	</body>
	<script>
		var win ;
	    function send(){
			// 可以配置具体允许哪个端口去获取信息
			// 如http://127.0.0.1:5501
	    	win.postMessage("hide",'*');
	    }
		
		function openHtml(){
			// 可以跨域场景,也可以同源页面
			// win = window.open("http://127.0.0.1:8848/word/demo2/index.html")
			win = window.open("http://127.0.0.1:5501/index.html")
		}
	</script>
</html>
<!-- B页面 -->
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
	</head>
	<body>
		
	</body>
	<script>
		window.addEventListener('message',function(e){
		    console.log(e.data);
		},false);
	</script>
</html>

# 跨域资源共享(CORS)withCredentials(浏览器凭证信息)

  1. Access-Control-Allow-Origin: 指定哪些源(origin)可以访问资源。如果设置为*,表示接受任何源的请求。如果设置为特定的URL,则只有来自该URL的请求被允许。
  2. Access-Control-Allow-Methods: 列出允许跨源请求使用的HTTP方法(如GET、POST、PUT、DELETE等)。
  3. Access-Control-Allow-Headers: 指定在跨源请求中允许携带的自定义HTTP头部字段。如果设置为*,表示接受任何头部字段。
  4. Access-Control-Allow-Credentials: 指示是否允许发送Cookie。当设置为true时,表示请求可以携带认证信息(如Cookies和HTTP认证信息)。
  5. Access-Control-Allow-Max-Age: 指定预检请求(preflight request)的结果能够被缓存多久。预检请求是在发送实际请求之前,浏览器自动发送的一个OPTIONS请求,用于检查服务器是否允许跨源请求。
  6. Access-Control-Expose-Headers: 允许服务器指定哪些头部字段可以被前端JavaScript访问。默认情况下,CORS请求中,只有简单的响应头部(如Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma)可以被前端访问。

普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。

需注意的是:由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。

# 前端设置:
  1. 原生ajax
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容

// 前端设置是否带cookie
xhr.withCredentials = true;

xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');

xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
    }
};
  1. jQuery ajax
$.ajax({
    ...
   xhrFields: {
       withCredentials: true    // 前端设置是否带cookie
   },
   crossDomain: true,   // 会让请求头中包含跨域的额外信息,但不会含cookie
    ...
});
  1. vue框架

a. axios设置:

axios.defaults.withCredentials = true
# 服务端设置:
var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var postData = '';

    // 数据块接收中
    req.addListener('data', function(chunk) {
        postData += chunk;
    });

    // 数据接收完毕
    req.addListener('end', function() {
        postData = qs.parse(postData);

        // 跨域后台设置
        res.writeHead(200, {
            'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
            'Access-Control-Allow-Origin': 'http://www.domain1.com',    // 允许访问的域(协议+域名+端口)
            /* 
             * 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
             * 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
             */
            'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'  // HttpOnly的作用是让js无法读取cookie
        });

        res.write(JSON.stringify(postData));
        res.end();
    });
});

server.listen('8080');
console.log('Server is running at port 8080...');

注意

服务端配置 Access-Control-Allow-Origin: '*' 或者前端的origin 这样接口能访问。但是不能获得cookie。还需要设置

Access-Control-Allow-Credentials:true 才能获得前端的cookie(注意:设为true时,Access-Control-Allow-Origin不能为'*',而是具体的某个域名)

# nginx代理跨域

  1. nginx配置解决iconfont跨域 浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。
location / {
  add_header Access-Control-Allow-Origin *;
}
  1. nginx反向代理接口跨域 跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

具体使用

# Nodejs中间件代理跨域

node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

1、 非vue框架的跨域(2次跨域) 利用node + express + http-proxy-middleware搭建一个proxy服务器。

  1. 前端代码示例:
var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
  1. 中间件服务器:
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();

app.use('/', proxy({
    // 代理跨域目标接口
    target: 'http://www.domain2.com:8080',
    changeOrigin: true,

    // 修改响应头信息,实现跨域并允许带cookie
    onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
        res.header('Access-Control-Allow-Credentials', 'true');
    },

    // 修改响应信息中的cookie域名
    cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
}));

app.listen(3000);
console.log('Proxy server is listen at port 3000...');
  1. Nodejs后台同(六:nginx)

2、 vue框架的跨域(1次跨域) 利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。

webpack.config.js部分配置:

module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口
            changeOrigin: true,
            secure: false,  // 当代理某些https服务报错时用
            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
        }],
        noInfo: true
    }
}

# WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。

原生WebSocket API使用起来不太方便,使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

前端代码:

<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功处理
socket.on('connect', function() {
    // 监听服务端消息
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });

    // 监听服务端关闭
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});

document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>

后端

var http = require('http');
var socket = require('socket.io');

// 启http服务
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

// 监听socket连接
socket.listen(server).on('connection', function(client) {
    // 接收信息
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });

    // 断开处理
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});

# 简单跨域和带预检跨域

CORS把HTTP请求分成两类,不同类别按不同的策略进行跨域资源共享协商。

  • 简单跨域请求。如果一个请求被认为是“简单请求”,那么浏览器将不会发送预检请求。

1). 请求是GET、HEAD或者POST,并且当请求方法是POST时,Content-Type必须是application/x-www-form-urlencoded, multipart/form-data或着text/plain中的一个值。(json会发起预检)

2). 请求中没有自定义HTTP头部。

对于简单跨域,浏览器要做的就是在HTTP请求中添加Origin Header,将JS所在域填充进去,向其他域的服务器请求资源。服务器端收到请求后,根据资源权限配置,在响应头中添加Access-Control-Allow-Origin Header。浏览器收到响应后,查看Access-Control-Allow-Origin Header,如果当前域已经得到授权,则将结果返回给Js,否则忽略此次响应。

TIP

在CORS(跨来源资源共享)策略中,简单请求的条件包括:

  • 请求方法只能是HEAD、GET或POST。
  • 不能包含自定义的HTTP请求头(除了几个特定的字段,如Accept、Accept-Language、Content-Language、Content-Type等)。
  • 如果包含Content-Type头,其值只能是application/x-www-form-urlencoded、multipart/form-data或text/plain。

如果一个GET请求不满足上述条件,比如包含了自定义的HTTP请求头,或者Content-Type被设置为了其他值(尽管对于GET请求来说,通常不会设置Content-Type头),那么这个GET请求就不再是简单请求,浏览器会在实际发送GET请求之前,先发送一个OPTIONS请求作为预检请求,以检查服务器是否允许该跨域GET请求。

因此,GET请求在特定条件下(即不满足简单请求的条件时)也会发送预检请求。这是CORS策略为了确保跨域请求的安全性而设计的一种机制。

  • 带预检(Preflighted)的跨域请求:这一机制确实有助于增强跨域请求的安全性,因为它允许服务器在允许跨域请求之前对请求进行安全检查。

1). 除GET、HEAD和POST(json格式会预检)以外的HTTP方法。

2). 请求中出现自定义HTTP头部。

带预检(Preflighted)的跨域请求需要浏览器在发送真实HTTP请求之前先发送一个OPTIONS的预检请求,检测服务器端是否支持真实请求进行跨域资源访问,真实请求的信息在OPTIONS请求中通过Access-Control-Request-Method 和Access-Control-Request-Headers 描述,此外与简单跨域请求一样,浏览器也会添加Origin Header。服务器端接到预检请求后,根据资源权限配置,在响应头中放入Access-Control-Allow-Origin 、Access-Control-Allow-Methods和Access-Control-Allow-Headers ,分别表示允许 跨域资源请求的域、请求方法和请求头 。此外,服务器端还可以加入Access-Control-Max-Age ,允许浏览器在指定时间内,无需再发送预检请求进行协商,直接用本次协商结果即可。浏览器根据OPTIONS请求返回的结果来决定是否继续发送真实的请求进行跨域资源访问。

XMLHttpRequest支持通过withCredentials属性实现在跨域请求携带身份信息(Credential,例如Cookie或者HTTP认证信息)。浏览器将携带Cookie Header的请求发送到服务器端后,如果服务器没有响应Access-Control-Allow-Credentials,那么浏览器会忽略掉这次响应。

这里讨论的HTTP请求是指由Ajax XMLHttpRequest对象发起的,所有的CORS HTTP请求头都可由浏览器填充,无需在XMLHttpRequest对象中设置。以下是CORS协议规定的HTTP头,用来进行浏览器发起跨域资源请求时进行协商:

Origin。HTTP请求头,任何涉及CORS的请求都必需携带。

Access-Control-Request-Method。HTTP请求头,在带预检(Preflighted)的跨域请求中用来表示真实请求的方法。

Access-Control-Request-Headers。HTTP请求头,在带预检(Preflighted)的跨域请求中用来表示真实请求的自定义Header列表。

Access-Control-Allow-Origin。HTTP响应头,指定服务器端允许进行跨域资源访问的来源域。可以用通配符*表示允许任何域的JavaScript访问资源,但是在响应一个携带身份信息(Credential)的HTTP请求时,Access-Control-Allow-Origin必需指定具体的域,不能用通配符。

Access-Control-Allow-Methods。HTTP响应头,指定服务器允许进行跨域资源访问的请求方法列表,一般用在响应预检请求上。

Access-Control-Allow-Headers。HTTP响应头,指定服务器允许进行跨域资源访问的请求头列表,一般用在响应预检请求上。

Access-Control-Max-Age。HTTP响应头,用在响应预检请求上,表示本次预检响应的有效时间。在此时间内,浏览器都可以根据此次协商结果决定是否有必要直接发送真实请求,而无需再次发送预检请求。

Access-Control-Allow-Credentials。HTTP响应头,凡是浏览器请求中携带了身份信息,而响应头中没有返回Access-Control-Allow-Credentials: true的,浏览器都会忽略此次响应。

总结:只要是带自定义header的跨域请求,在发送真实请求前都会先发送OPTIONS请求,浏览器根据OPTIONS请求返回的结果来决定是否继续发送真实的请求进行跨域资源访问。所以复杂请求肯定会两次请求服务端

//前端请求
// index.html
axios.defaults.baseURL = 'http://localhost:3000';
axios.get("/users", {headers:{'X-Token':'jilei'}})
//nodejs
else if (method == "OPTIONS" && url == "/api/users") {
	res.writeHead(200, {
	"Access-Control-Allow-Origin": "http://localhost:3000",
	"Access-Control-Allow-Headers": "X-Token,Content-Type",
	"Access-Control-Allow-Methods": "PUT"
	});
	res.end();
}

如果要携带cookie信息,则请求变为credential请求

// index.js
// 预检options中和/users接口中均需添加
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 设置cookie
res.setHeader('Set-Cookie', 'cookie1=va222;')
// index.html
// 观察cookie存在
console.log('cookie',req.headers.cookie)
// ajax服务
axios.defaults.withCredentials = true  
//[如果原生的ajax xhr.open("get",url,true);xhr.withCredentials = true;]

# 原生ajax加token


var xhr = window.XMLHttpRequest ? new XMLHttpRequest() 
			: ActiveXObject("microsoft.XMLHttp")

xhr.open("get",url,true);
xhr.setRequestHeader("Authorization",1111)//需要加在open后面
xhr.send();

xhr.onreadystatechange= () =>{
	
	if(xhr.readyState == 4){
		
		if(xhr.status == 200){
			console.log(xhr)
			var data =JSON.parse(xhr.responseText) ;
			let add=document.getElementsByClassName("add")[0];
			let ul=document.createElement("ul");
			
			for(let i in data){
				let li=document.createElement("li")
				console.log(data[i])
				li.innerHTML=JSON.stringify(data[i]) ;
				ul.appendChild(li)
				
			}
			add.appendChild(ul)
			
			
		}
	}
}    
    
if(method==="GET"&url==="/json"){
		const json =[{name:"zhangsan",age:18,job:"student"},{name:"lisi",age:28,job:"teacher"}]
		
		res.writeHead(200, {
			"Access-Control-Allow-Origin": "http://127.0.0.1:8848",
			'Content-Type': 'application/json'
		});
		
		res.end(JSON.stringify(json));
	}else if(method==="OPTIONS"&url==="/json"){
		res.writeHead(200, {
			"Access-Control-Allow-Origin": "http://127.0.0.1:8848",
			"Access-Control-Allow-Headers": "Authorization,Content-Type",
			"Access-Control-Allow-Methods": "PUT",
			"Access-Control-Max-Age":3600
			});
		res.end();
	}

# devserver配置跨域

借助webpack,可以解决在开发环境下的跨域问题,如vue.config.js中配置devserver

  • 前端端口9000 后端端口3000
# .env.development 文件名
# 开发环境配置
# ENV = 'development'
# VUE_APP_BASE_API = /dev-api
const port = process.env.port || process.env.npm_config_port || 9000 // 端口
......
devServer:{
    port: port,
    open: true,
    overlay: {
      warnings: false,
      errors: true
    },
    proxy: {
      '/dev-api': {
        target: `http://localhost:3000`,//这是请求的真正目标地址(后端地址)
        changeOrigin: true,
        pathRewrite: {
          '^/dev-api': '' // 思路是如果是开发环境,就给所有要代理的接口统一加上前缀,然后代理请求时再统一通过rewrite去掉
        }
      }
    }
}
  • 前台请求代码
//nodejs的地址:localhost://3000/1.json
fetch('http://localhost:9000/dev-api/1.json').then(res=>res.json()).then(res=>{
  console.log(res)
}).catch(e=>{
  console.log(e)
})
  • 可配置动态地址来适配请求
devServer: {
    host: '0.0.0.0',
    port: port,
    open: true,
    proxy: {
      // detail: https://cli.vuejs.org/config/#devserver-proxy
      [process.env.VUE_APP_BASE_API]: {
        target: `http://localhost:8080`,
        changeOrigin: true,
        pathRewrite: {
          ['^' + process.env.VUE_APP_BASE_API]: ''
        }
      }
    },
    disableHostCheck: true
  },

devserver配置跨域参考 (opens new window) postMessage (opens new window)

最后更新: 11/2/2024, 10:36:52 AM