date: 2019-12-31 18:09:11
跨年是web开发中。。奥,错了。跨域是web开发中常见的一个问题,其中CORS是比较常用的解决跨域的方式。现在通过
new XMLHttpRequest()
和简单实现一个nodeserver
,彻底搞懂CORS(Cross-Origin Resource Sharing)。

几个相关响应头头信息
-
Access-Control-Allow-Origin
:
允许哪些地址访问。可以指定某些URL访问。也可以通过设置通配符*
,允许所以地址访问,不过在设置Credentials
时,不允许设置通配符,解决方法下面说。 -
Access-Control-Allow-Headers
:
允许客户端设置约定好的请求头参数。 -
Access-Control-Allow-Credentials
:
响应头是否可以将请求的响应暴露给页面。主要是指的是Cookie -
Access-Control-Allow-Methods
:
允许哪些方法访问我。 -
Access-Control-Max-Age
:
设置多久内不需要再进行预检请求
几个实际的错误
实际开发中会浏览器会之间提示响应的错误信息,根据信息内容,可以很容易的推断出是什么原因跨域,解决方案是什么。
先写一个简单的xhr客户端:
// 用http-server静态服务器端口 8080 启动
<button id='btn'>发送ajax</button>
<script>
btn.addEventListener('click',()=>{
doRequest()
})
// 发送请求
function doRequest(){
let xhr = new XMLHttpRequest();
// 。。。请求处理
let xhr = new XMLHttpRequest();
xhr.open('GET','http://localhost:3000/user',true)
// todo请求设置
xhr.responseType = 'json'; // 设置服务器的响应类型
xhr.onload = function(){console.log(xhr.response)}
xhr.send()
}
</script>
一个简单的node服务端:
// nodemon server.js启动
http.createServer((req,res)=>{
let {pathname} = url.parse(req.url)
let method = req.method
if(req.headers.origin){ // 如果跨域了 才走跨域逻辑
// TODO CORS处理
}
// 接口逻辑处理
if (pathname == '/user') {
}
}).listen('3000')
1、 第一个请求,被拒绝了。
-
客户端请求:
function doRequest(){ let xhr = new XMLHttpRequest(); xhr.open('GET','http://localhost:3000/user',true) xhr.responseType = 'json'; // 设置服务器的响应类型 xhr.onload = function(){console.log(xhr.response)} xhr.send('a=1&b=2') }
-
出现错误:
当我们通过一个简单的get请求
'http://localhost:3000/user'
时,错误提示很明确,从http://127.0.0.1:8080
地址请求http://localhost:3000/user
的接口被拒绝了。要设置Access-Control-Allow-Origin
header响应头。 -
解决方法:
服务端设置响应头(node http模块写法):// ... res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:8080') // or 允许全局访问 设置通配符 * res.setHeader('Access-Control-Allow-Origin', '*') // or 允许全局访问 动态获取请求源地址。( * 在某些情况下不能用,下面会说) res.setHeader('Access-Control-Allow-Origin', req.headers.origin) // ...
2. 请求头内携带token信息被拒绝了
-
客户端请求:
// ... // 设置请求头token xhr.setRequestHeader('token','xxxx'); // ... }
-
出现错误:
设置请求头信息token。服务器应该要允许,如需其他参数另行添加
-
解决方法: 服务端设置
// ... res.setHeader('Access-Control-Allow-Headers','token'); // ...
其实现在再去请求又会出现另一个错误,看下面。
3. 非简单请求的预检请求 OPTIONS
客户端请求时(包括GET、POST请求)增加自定义请求头信息时或者非GET、POST请求,都是非简单请求。非简单请求触发时,会先发送一次OPTIONS预检请求,
-
客户端:设置自定义请求头字段,或者PUT请求等非GET和POST请求。
function doRequest(){ // ... xhr.open('PUT','http://localhost:3000/user',true) // or xhr.open('GET','http://localhost:3000/user',true) xhr.setRequestHeader('token','xxxx'); // ... }
-
出现错误:
OPTIONS请求未处理,所以404 同时出现跨域错误 -
解决方法:
服务端设置:同时考虑到不需要每次都进行预检请求,响应成功一次后,服务器可以告诉客户端多少秒内不需要继续OPTIONS请求了。// 设置options请求的间隔事件 res.setHeader('Access-Control-Max-Age','10'); if(method === 'OPTIONS'){ res.end(); // 浏览器就知道了 我可以继续访问你 return; }
4. 设置Cookie时出现到问题
有时候服务端需要给客户端返回Cookie用于身份凭证,客户端收到Cookie后,再次请求可以携带该Cookie表明身份。
- 客户端:
function doRequest(){
// ...
xhr.withCredentials = true; // 强制携带服务器设置的cookie
// ...
}
-
出现错误:
服务端未允许cookie设置
-
解决方法: 服务器端设置,允许设置凭证(此处作者有疑问🤔️)
// ... res.setHeader('Access-Control-Allow-Credentials',true) // ...
下面给出完整把端客户端和服务端的代码
客户端:
可以用http-server
指定8080端口启动访问,模拟 http://localhost:8080/index.html
访问端口 3000
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<button id='btn'>发送ajax</button>
<script>
btn.addEventListener('click',()=>{
let xhr = new XMLHttpRequest();
xhr.open('POST','http://localhost:3000/user',true)
xhr.setRequestHeader('token','xxxx'); // 设置token 服务器需要同意设置token
xhr.withCredentials = true // 请求携带cookie,服务器需要同步设置允许携带
xhr.responseType = 'json'; // 设置服务器的响应类型
xhr.onload = function(){ // xhr.readyState == 4 + xhr.status == 200
// xhr.response 对象等数据
console.log(xhr.response)
}
xhr.send('a=1&b=2')
})
</script>
</body>
</html>
node服务器:
const http = require('http');
const fs = require('fs');
const url = require('url');
const path = require('path');
http.createServer((req,res)=>{
// 动态服务
let {pathname,query} = url.parse(req.url);
// pathname 有可能是客户端发起的接口请求
let method = req.method;
// 允许那个域 来访问我
if(req.headers.origin){ // 如果跨域了 才走跨域逻辑
res.setHeader('Access-Control-Allow-Origin',req.headers.origin); // 和 * 是一样
// 允许哪些方法访问我
res.setHeader('Access-Control-Allow-Methods','GET,PUT,DELETE,POST,OPTIONS');
// 允许携带哪些头
res.setHeader('Access-Control-Allow-Headers','token');
// 设置options请求的间隔事件
res.setHeader('Access-Control-Max-Age','10');
res.setHeader('Access-Control-Allow-Credentials',true)
// 跨域 cookie 凭证 如果跨域是不允许携带cookie
if(method === 'OPTIONS'){
res.end(); // 浏览器就知道了 我可以继续访问你
return;
}
}
if(pathname === '/user'){ // 你发送的是api接口
switch(method){
case 'GET':
// res.setHeader('Set-Cookie','name=zf');
res.end(JSON.stringify({name:'zf'}))
break;
case 'POST':
let arr = [];
console.log('log')
req.on('data',function(chunk){
arr.push(chunk);
})
req.on('end',function(){
console.log(Buffer.concat(arr).toString());
res.end('{"a":1}')
})
}
return
}
// 静态服务
let filePath = path.join(__dirname,pathname);
fs.stat(filePath,function(err,statObj){
if(err){
res.statusCode = 404;
res.end();
return
}
if(statObj.isFile()){
// mime header
fs.createReadStream(filePath).pipe(res);
return
}else{
res.statusCode = 404;
res.end();
return
}
})
}).listen(3000);