Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

跨域系列 - 非简单请求 与 预检测优化方案 #35

Open
HXWfromDJTU opened this issue Nov 5, 2020 · 0 comments
Open

跨域系列 - 非简单请求 与 预检测优化方案 #35

HXWfromDJTU opened this issue Nov 5, 2020 · 0 comments

Comments

@HXWfromDJTU
Copy link
Owner

HXWfromDJTU commented Nov 5, 2020

前言

上一篇笔记了解了浏览器同源策略的方方面面,随着web服务的多元化,以及前后端分离的大环境,页面与资源的分离已经是必然的事情。这次则先聊聊工作中用得最多的 CORS 策略(Corss-origin Resource Sharing)

CORS 与 请求类型

CORS 策略允许浏览器向跨源服务器发出获取资源,但请求既然是来源于不同于服务器的非同域。最容易出现以下问题:

  1. 跨域说明有可能是恶意站点发起,则处理请求前的 同域检测 就十分有必须要了。(你是否想起了CSRF Token的概念,后文会做相应比较)。
  2. 每次都携带全量数据访问服务端接口,但却因为最基础的 同域检测 都通过不了,则十分浪费网络带宽
  3. 服务器执行 同域检测 的逻辑复杂程度不一,每一次都需要重新判断,也是对服务器资源的浪费

针对此需求,w3c 在提出了一个预检查请求 (CORS-preflight) 的概念。但由于向下兼容的需要,只在非简单请求中启用预检查请求,而对简单请求不作额外处理。

服务端服务器可以根据这个预检查请求,来告知是否允许浏览器对原接口发起请求。

简单请求

正如前文👆所提到的,其实 简单请求 仅仅是为了 w3c 为了退出新策略而强行划定的标准。在 CORS 标准推出前,浏览器与服务器的数据交互大多数是使用 <Form> 发起的,那么为了最大程度地兼容已存在的服务,则以此为界定。

参考 👉DOM Form - MDN 和 👉简单请求 - MDN, 二者的描述 和 定义也十分相近。

使用form标签是否可以发起这一标准,大多数情况下我们就不需要去强行记忆区分简单请求的那些复杂的methodcontent-typeheader了。

非简单请求 与 预检查请求

很好理解的是, 非简单请求 字面意思就是 除了 简单请求 以外的其他请求。这时候,浏览器会先行发送一个请求的预检查请求

预检测请求的有效时间

预检测请求存在的意义之一,则是减少服务器频繁执行 检查同域 逻辑,其起作用的核心则是:

Access-Control-Max-Age: xxx

在有效时间内,浏览器无须为同一请求再次发起预检请求。这一机制有效地减少了服务端执行 同域检测 逻辑的时间,节省了服务器的资源。

如果值为 -1,则表示禁用缓存,每一次请求都需要提供预检请求,即用OPTIONS请求进行检测。

其他的 CORS 头信息

Access-Control-Allow-OriginAccess-Controll-Allow-MethodAccess-Controll-Allow-HeaderAccess-Control-Allow-Credentials 这几个的用法已经是老生常谈了。这里则不再赘述了。

跨域与安全

CSRF

同源策略不能直接防范CSRF❌
  • 通过恶意连接,"借用"用户cookie以实现盗用用户登录态的行为,便是大家熟知的CSRF攻击。
  • 通过👆上文可知,浏览器的同源策略仅仅只是拦截了请求的返回,但并不会阻止跨域请求的发送。
借助参数防范
  • 前后端配合,使用 CSRF token 方案进行防范
    • 检测到不合法后,须入口层面 (比如nginx)处拒绝掉请求,否则请求仍然会被服务器处理
  • 若非必要不开启CORS访问、或者不开启Access-Control-Allow-Credentials
  • 允许访问的域,使用指定白名单的方式,而不直接使用通配符 *
    • 如果服务器未使用“*”,而是指定了一个域,那么为了向客户端表明服务器的返回会根据Origin 请求头而有所不同,必须在Vary响应头中包含 Origin

关于 CSRF 原理与防范实战,请看博主的另一篇笔记,传送门👉

君子协定 CORS

与 HTTP 协议相类似,w3c 提出的 CORS 也是一种约定,需要浏览器和服务器共同配合实现。对于预检测请求的结果,仍然后很多种可能的处理流程:

  1. 浏览器在收到预请求结果为失败的情况下,仍去发起原非简单请求
  2. 对于简单请求,是否就可以绕过 同源检测 了呢?
  3. 同域策略 和 CORS 只在浏览器中存在约定,对于其他客户端,类似 curlpostman工具 和 其他脚本等发起的请求,是否不用检测了呢?

OPTION检测 与 CSRF 预防相结合

在👉 CSRF 实战 笔记中,我们讲到过对于非同源请求的拦截与处理方法。结合起来 CORS 的 OPTION 预检查请求用于浏览器缓存检测结果,而CSRF Token的检测则用于做最后的防御。

OPTION 处理 (Koa)

服务端的跨域处理,简单可以表示为以下的流程,感兴趣的话也可以看看kosjs/cors插件的实现过程。

// 集中处理错误
const handler = async (ctx, next) => {
  // log request URL:
  ctx.set("Access-Control-Allow-Origin", "yourdmain.com");
  ctx.set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
  ctx.set("Access-Control-Max-Age", "86400");
  ctx.set("Access-Control-Allow-Headers", "x-requested-with,Authorization,Content-Type,Accept");
  ctx.set("Access-Control-Allow-Credentials", "true");
  
  // 若有必要,请添加上其他 CSRF TOKEN 所需的响应头字段
  ctx.set("X-Yourdomain-Timestamp", "x-requested-with,Authorization,Content-Type,Accept");

  // 统一处理预请求
  if (ctx.request.method == "OPTIONS") {
    ctx.response.status = 204
  }

  console.log(`Process ${ctx.request.method} ${ctx.request.url}`);

  try {
    await next();
    console.log('handler通过')
  } catch (err) {
    console.log('handler处理错误')
    ctx.response.status = err.statusCode || err.status || 500;
    ctx.response.body = {
      message: err.message
    };
  }
};

小结

  1. 客户端(浏览器)可以做的是判断请求目标源是否跨域,服务端可以做的是收到请求后,是否拒绝这一个请求,注意服务端判断的过程,消耗的资源可大可小。
  2. 非简单请求可以触发 preflight 机制,使用 Access-Control-Max-Age 响应头实现预请求的缓存,减少服务器压力
  3. 作为网站开发者,尽量使用非简单请求,触发 preflight 机制而减小服务器压力。
  4. 预检查请求不能作为安全防护策略,仍然需要做好 CSRF防护。

参考文章

[1] 跨源资源共享(CORS) - MDN
[2] koa跨域 - mapplat
[3] CORS 为什么要区分『简单请求』和『预检请求』? - 奇舞团

@HXWfromDJTU HXWfromDJTU changed the title 浏览器原理 - CORS 与 非简单请求 跨域系列 - 非简单请求 与 预检测优化方案 Dec 17, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant