浏览器的同源策略

同源策略是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。

它能帮助阻隔恶意文档,减少可能被攻击的媒介。例如,它可以防止互联网上的恶意网站在浏览器中运行 JS 脚本,从第三方网络邮件服务(用户已登录)或公司内网(因没有公共 IP 地址而受到保护,不会被攻击者直接访问)读取数据,并将这些数据转发给攻击者。

源的定义

如果两个 URL 的协议端口(如果有指定的话)和主机都相同的话,则这两个 URL 是同源的。这个方案也被称为“协议/主机/端口元组”,或者直接是“元组”。(“元组”是指一组项目构成的整体,具有双重/三重/四重/五重等通用形式。)

下表给出了与 URL http://store.company.com/dir/page.html 的源进行对比的示例:

URL 结果 原因
http://store.company.com/dir2/other.html 同源 只有路径不同
http://store.company.com/dir/inner/another.html 同源 只有路径不同
https://store.company.com/secure.html 失败 协议不同
http://store.company.com:81/dir/etc.html 失败 端口不同
http://news.company.com/dir/other.html 失败 子域名不同

同源政策 (Same-Origin Policy)

浏览器应用最常见的就是基于前端向后端发送请求等待相应结束后将响应结果呈现在前端,主流方法为使用浏览器提供的 fetch API 或 XMLHttpRequest 等方式。

但需要注意的是,基于JS使用这种方式发起 request,必须遵守同源政策 (Same-Origin Policy)。

在同源政策下,非同源的 request 则会因为安全性的考量受到限制。浏览器强制要求遵守 CORS (Cross-Origin Resource Sharing,跨域资源存取) 的规范,在通信开始前,会进行一次XHR(预检),如果预检过程失败则会提示CORS错误,导致无法完成正常的通信。

服务器常见报错为:

1
cess to XMLHttpRequest at 'https://AA.XXX.com/sql' from origin 'https://BB.XXX.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

可以看到报错提示两个网站之间的访问属于跨源关系,违反了同源政策,直接被浏览器拦截,导致通信失败

跨域请求

服务器端配置

Access-Control-Allow-Origin

必须设置

设置为*时意为允许来自所有跨域请求的跨域请求

设置为指定域名时为仅匹配指定域名的跨域请求

Access-Control-Expose-Headers

可选配置

不设置时浏览器发送请求不携带cookies

设置为true时则Access-Control-Allow-Origin的配置不允许为通配符*

Access-Control-Expose-Headers

可选配置

响应报头指示哪些报头可以公开为通过列出他们的名字的响应的一部分

可以存取的response header为Cache-Control Content-Language Content-Type Expires Last-Modified Pragma

简单跨域请求

简单跨域请求的条件

需要同时满足以下三个条件

  1. 请求头为HEAD/GET/POST三种方法中的一种
  2. 自订的request header为Accept/Accept-Language/Content-Language
  3. 自订的request header若为Content-Type,值为application/x-www-form-urlencoded / multipart/form-data / text/plain

简单跨域请求的处理

对于简单跨域请求,浏览器不进行预检,直接发出CORS请求,并在请求头上添加Origin字段表明请求来源

简单跨域请求不由浏览器控制,由服务器进行校验,由服务器校验返回部(response)内是否有Access-Control-Allow-Origin

非简单跨域请求

非简单跨域请求默认需要发送预检请求,在其他博客上提到一般为 PUT/DELETE方法,但在实际测试过程中,全部跨域请求都要发送预检请求,也就是说所有的请求都成为了非简单跨域请求,关于这一点需要更进一步的研究

非简单跨域请求相当于提前进行一次握手通信预检,并预检跨域请求是否匹配浏览器端-服务器端之间的通信配置,如果预检失败直接报错CORS问题,不进行实际通信,但此处有坑,在后文详细讲解

跨域请求对于Cookies的管理

一般的HTTP请求会自动带上cookies发送到服务器上,但对于跨域请求来说,是默认不携带cookies的。因为cookies的不安全性以及会影响服务器的session对于用户的鉴权,导致风险操作。所以如果需要实现对于cookies的操作必须要实现浏览器端-服务器端对于cookies的认证,并且服务器端的Access-Control-Allow-Origin不能配置为*,会被认为不安全,一定要完全匹配域名才能完成cookies的传输。常见报错为:

1
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when therequest's credentials mode is 'include'. Origin http://localhost:8080 is therefore not allowed access. Thecredentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

基于Axios-KOA框架的跨域实战

项目框架

由于顶级域名被其他服务占用,前端页面与服务器页面均使用二级域名,服务器端虽然已经通过nginx反代理,但因为后端是Node.js服务器必然占用一个端口导致了不可避免的跨域问题,故必须同步配置CORS配置。

image-20230411194743997
image-20230411194743997

前端Axios配置

Axios默认对于跨域请求不传参,如果浏览器端-服务器端CORS校验不通过导致预检失败亦会导致请求头直接不包含cookies

axios.defaults.withCredentials 设置跨域携带cookies 需注意后端Access-Control-Allow-Origin必须不为*

axios.defaults.crossDomain 设置允许跨域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 异步实现后端访问
* @param {string} link 后台访问地址
* @param {object} postData post请求数据
* @return {Promise<any>} 以期约方式返回查询结果
*/
function postData(link, postData) {
return new Promise((resolve, reject) => {
// 控制axios可传递cookie
axios.defaults.withCredentials = true;
axios.defaults.crossDomain = true;
axios.post(`${config.ip}/${link}`, postData)
.then(function(res) {
const data = res.data;
console.log(res);
resolve(data);
})
.catch(function(err) {
console.log(err);
reject(new Error(err));
});
});
}

后端KOA框架配置

KOA框架配置了CORS的配置项目,使用如下导入

1
const cors = require('koa2-cors');

app设置相关选项,首先导入cors包,其中配置origin,该处的origin实际为Access-Control-Allow-Origin,请勿在路由器部分重复配置,会导致CORS出错,credentials设置允许携带cookies

1
2
3
4
app.use(cors({
origin: 'https://AAA.XXX.com', // 指定允许访问的域名
credentials: true, // 允许跨域携带cookie
}));

服务器端Nginx配置

部分教程会提示将跨域配置直接配置在Nginx上,本人不推荐这种操作,会与KOA2-CORS的配置进行冲突,例如重复配置credentials的情况会导致浏览器在预检的过程种识别credentials为true,true导致预检失败,建议Nginx上不配置CORS相关的配置

1
2
3
4
5
6
7
8
9
location / {  
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

if ($request_method = 'OPTIONS') {
return 204;
}
}

Cookies注意事项

Cookies在两个子域名之间是无法传递的,比如在本项目中,即使成功传递了Cookies,也仅仅会将Cookies写入BBB.XXX.com而不是项目需要的AAA.XXX.com,所以这里的方法为直接将项目写入顶级域名XXX.com即可,代码配置为:

1
2
3
4
5
6
ctx.cookies.set(
'userLogin',
userID,
{domain: 'XXX.com', maxAge: 1000 * 60 * 60 * 2,
signed: true, httpOnly: false, overwrite: true},
);

CORS引入的后遗症

因为CORS的预检操作,如果是服务器上的代码出错或其他原因导致没有相应包传回浏览器,浏览器会直接判断是CORS预检失败,报错为常见的预检失败报错

1
cess to XMLHttpRequest at 'https://AA.XXX.com/sql' from origin 'https://BB.XXX.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

但在前文中我们已经配置完成了这个CORS问题,而且其他页面也是正常工作,初次碰到这个问题会很难分析出正确的结果

例如,在本项目的实际开发中,此问题的触发顺序为:

  1. 前端Axios封装的POST请求发出,没有配置axios.defaults.crossDomai,仅配置了axios.defaults.withCredentials,导致cookies没有发出
  2. 后端收到校验包,解析cookies失败
  3. 功能异常,无法返回相应包
  4. 前端XHR校验失败,判断为CORS出错,但实际未出错

因此,如碰到该问题,因查看服务器端的响应情况查看通信问题,而并不是CORS跨域问题