likes
comments
collection
share

跨域和跨域携带cookie失败的N种情形详解

作者站长头像
站长
· 阅读数 19

最近在做项目工程中遇到了不少与跨域和跨域携带cookie失败的问题,又从头详细研究了下跨域和跨域携带cookie的相关知识点,在此和大家分享一下。

遇到的问题

  1. 预检请求设置不正确导致跨域失败 跨域和跨域携带cookie失败的N种情形详解
  2. 在 APP 上 登录成功后后端产生了 cookie,发起请求能正确携带 cookie,但是将 H5 页面分享到微信上,在微信里打开页面,登录成功后后端产生了 cookie,但是发起请求未能正确携带 cookie,后端报鉴权失败,同样的代码为什么在 APP 的 webview 里没问题,在微信上就有问题?
  3. 本地起的前端工程没有用域名,而是 ip 地址,如 http://127.0.0.1:3000 ,前后端都设置了跨域且能携带 cookie,登录成功后后端成功产生了 cookie,但是在请求接口 test.gf.com.cn 时携带 cookie 失败了,将 ip 地址 127.0.0.1 映射到域名 localhost.gf.com.cn 后,用 localhost.gf.com.cn 打开页面,请求接口时能正确携带 cookie,只是将 ip 地址改为了用域名访问为什么能携带 cookie 成功?

为了解析以上的问题,我们从跨域的概念开始一步步分析。

跨域的概念

同源策略

同源策略是一个重要的安全策略,它用于限制一个 origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

简单说,当我们访问一个网站时,浏览器会对源地址的不同部分(协议://域名:端口)做检查。只有协议、域名、端口完全相同时才是同源,比如当前浏览器访问地址:http://domain/url

url结果
http://domain/other同源
http://domain2跨域,域名不同
http://domain:8080跨域,端口不同
https://domain跨域,协议不同

同源策略的目的是为了保护本地数据不被JavaScript代码获取回来的数据污染,因此,浏览器拦截的是客户端发出的请求回来的数据接收,即请求发送了,服务器响应了,但是无法被浏览器接收。

也就是浏览器拦截的是服务端发出的响应。跨域的本质是为了阻止用户读取到另一个域名下的内容,同时也说明了浏览器跨域机制并不能完全阻止 CSRF,因为请求毕竟是发出去了,服务端也响应了。

跨域 CORS

但凡被浏览器识别为不同源,浏览器都会认为是跨域,默认是不允许的。

比如:试图在 http://127.0.0.1:3000 中,请求 http://127.0.0.1:9000 的资源会出现如下错误: 跨域和跨域携带cookie失败的N种情形详解 这也是前端 100% 在接口调试中会遇到的问题。

简单请求和复杂请求

在浏览器的 Network 中有时候会看到两个同样地址的请求,有没有想过这是为什么呢?这是因为在请求中,会分为 简单请求 和 复杂请求 。

简单请求:满足如下条件的,

  • 请求方法为:GET 、POST 、 HEAD
  • 请求头:AcceptAccept-LanguageContent-LanguageContent-Type

其中 Content-Type 限定为 :text/plain、multipart/form-data、application/x-www-form-urlencoded

复杂请求:不满足简单请求的都为复杂请求。例如请求头为 PUTDELETE或有自定义的请求头,在发送请求前,会使用 options 方法发起一个 预检请求(Preflight)  到服务器,以获知服务器是否允许该实际请求。

如何实现跨域请求

实现跨域请求关键在后端,服务端在响应头中可以设置有如下属性:

  • Access-Control-Allow-Origin:用于指定允许访问资源的源或源列表,要携带Cookie,响应头中的"Access-Control-Allow-Origin"不能设置为通配符"*",而必须指定具体的源地址,例如:http://127.0.0.1:3000

  • Access-Control-Allow-Methods:预检请求(preflight request)的响应头,指定允许的请求方法,由于判断简单请求之一的 HTTP 方法默认为 GET , POST , HEAD ,所以这些即使不在 Access-Control-Allow-Methods的约定中,浏览器也是支持的,所以简单请求和复杂请求的请求方法为GET , POST , HEAD之一都不必设置这个响应头。但是如果浏览器发出了一个 PUT 请求,此时是一个复杂请求,浏览器会先发送一个预检请求,在对预检请求的响应中,响应头 Access-Control-Allow-Methods 必须包含 PUT,否则会跨域失败。

  • Access-Control-Allow-Headers:预检请求(preflight request)的响应头,用于指定允许的请求头。它的值应该是包含实际请求可能包含的自定义请求头的逗号分隔列表,例如,浏览器发出请求的请求头中有一个自定义字段 “x-data-source”,那么在对预检请求的响应中,响应头 Access-Control-Allow-Headers 必须包含 “x-data-source”。

  • Access-Control-Max-Age:预检请求(preflight request)的响应头,它的值单位为秒,由于复杂请求都会先发送预检请求,所以每请求一次复杂请求浏览器都会发出 2 个请求,设置了 Access-Control-Max-Age 的值后,浏览器在规定时间内不会再发预检请求,只发正式请求,例如 Access-Control-Max-Age: 600,浏览器在 10 分钟内不会再发预检请求。

简单请求实现跨域

简单请求实现跨域就比较简单了,只要在服务端接口的响应头设置 Access-Control-Allow-Origin 即可,例如前端页面发出一个简单请求:

 fetch('http://127.0.0.1:9000/get/simple', {
   method: "GET",
 })

服务端响应:

import KoaRouter from "koa-router";
const router = new KoaRouter();

router.get("/get/simple", async (ctx, next) => {
  // 从请求头中获取源
  const { origin } = ctx.headers;
  //设置响应头
  ctx.response.set({
    "Access-Control-Allow-Origin": origin,
  });
  ctx.body = {
    success: true,
    data: {
      name: "zhangsan",
      age: 1000,
    },
  };
});

复杂请求实现跨域

复杂请求实现跨域稍微复杂一些,因为首先需要处理 预检请求,预检请求是浏览器自动发出的,预检请求的请求头中有两个非常重要的请求头:

  1. Access-Control-Request-Method:用于通知服务器在真正的请求中会采用哪种 HTTP 方法 ,因为预检请求所使用的方法总是 OPTIONS,与实际请求所使用的方法不一样,所以这个请求头是必要的,例如:如果真正的请求所使用的是 PUT 方法,那么这个值就是 PUT。

  2. Access-Control-Request-Headers:用于通知服务器在真正的请求中会采用哪些请求头,例如真正的请求中有一个自定义的请求头 “x-data-source”,那么它的值就是 “x-data-source”。

下面以代码来说明预检请求该如何实现跨域,例如浏览器发出了一个复杂请求:

fetch('http://127.0.0.1:9000/get/complex', {
   method: "PUT",
   headers: {
     "x-data-source": 'data1',
     "custom-header": 'data2',
   }
 })

可以看到浏览器发出了两个请求,下面那个是预检请求

跨域和跨域携带cookie失败的N种情形详解 预检请求头中自动带上了 Access-Control-Request-MethodAccess-Control-Request-Headers

跨域和跨域携带cookie失败的N种情形详解

再看看服务端应该如何设置响应:

import KoaRouter from "koa-router";
const router = new KoaRouter();

// 预检请求的响应
router.options("/get/complex", async (ctx, next) => {
  console.log(ctx.headers, '获取请求的请求头');
  // 从预检请求的头中获取跨域的相关信息
  const origin = ctx.headers['origin'];
  const headers = ctx.headers['access-control-request-headers'];
  const method = ctx.headers['access-control-request-method'];
  // 设置响应头,允许跨域
  ctx.set({
    "Access-Control-Allow-Origin": origin,
    // 注意这里如果正式请求的请求方法为 GET、POST、HEAD之一,可以不设Access-Control-Allow-Methods
    "Access-Control-Allow-Methods": method,
    "Access-Control-Allow-Headers": headers,
    // 预检请求在 100秒内不会再发出
    "Access-Control-Max-Age": 100,
  });
  // 预检请求的成功状态码为 204
  ctx.status = 204; 
})

// 正式请求的响应
router.put("/get/complex", async (ctx, next) => {
  // 从请求头中获取源
  const { host, origin } = ctx.headers;
  //设置响应头
  ctx.response.set({
    // 正式请求只需要设置 Access-Control-Allow-Origin 即可
    "Access-Control-Allow-Origin": origin,
  });
  ctx.body = {
    success: true,
    data: {
      name: "zhangsan",
      age: 1000,
    },
  };
});

正式请求响应成功

跨域和跨域携带cookie失败的N种情形详解

跨域携带cookie

跨域请求如果需要携带 cookie 需要前后端都设置允许跨域,前端设置如下:

fetch('http://127.0.0.1:9000/get/simple', { 
  method: "GET", 
  // credentials 属性有 3 种值
  // include: 告诉浏览器在同域和跨域请求中包含 cookie,并始终使用在响应中发回的任何 cookie
  // same-origin: 告诉浏览器在对同源 URL 的请求中包含 cookie,并使用从同源 URL 的响应中返回的任何 cookie。这是默认值。
  // omit: 告诉浏览器从请求中排除 cookie,并忽略在响应中发回的任何 cookie   
  credentials: 'include',
 })

后端在响应头中需要设置 "Access-Control-Allow-Credentials": 'true'

import KoaRouter from "koa-router";
const router = new KoaRouter();

router.get("/get/simple", async (ctx, next) => {
  const { origin } = ctx.headers;
  //设置响应头
  ctx.response.set({
     // 允许携带 cookie 
     "Access-Control-Allow-Credentials": true,
     "Access-Control-Allow-Origin": origin,
  });
  ctx.body = {
    success: true,
    data: {
      name: "zhangsan",
      age: 1000,
    },
  };
});

在早期的浏览器版本中(谷歌80版本之前),这样做一般没什么问题,但是之后的版本就不行了,很容易 cookie 就会携带失败,这与浏览器的默认行为和 cookie 的一些属性有关。

cookie的属性

Cookie 是存储在客户端(通常是浏览器)中的小型文本数据,由服务端产生,用于在客户端和服务器之间传递信息。Cookie 可以具有各种属性,这些属性控制着它们的行为和使用。以下是常见的 Cookie 属性以及它们的含义:

属性含义
NameCookie 的名称,是一个唯一的标识符,用于在服务器和客户端之间识别 Cookie
ValueCookie 的值,包含了实际的数据。这是 Cookie 存储的主要信息
Domaincookie绑定的域名,属性值是服务器端的域名,指定了可以访问该 Cookie 的域名,如果不指定,该属性默认为同一 host 设置 cookie,不包含子域名,比如如果设置了 domain 为 example.com,那么这个Cookie将在example.com域名下的所有子域名(如www.example.com、api.example.com等)中可见和访问。需要注意的是"domain"属性值不应该包含协议和端口号。
Path指定了可以访问 Cookie 的路径,属性默认值是'/',例如,如果将 Path 设置为 "/example",那么只有在路径以 "/example" 开头的请求中才会发送 Cookie
Expires指定了 Cookie 的过期日期和时间。一旦过期,浏览器将删除该 Cookie。
Max-Age指定了 Cookie 的最大存活时间,以秒为单位,Max-Age 表示 Cookie 在指定的时间段内有效,如果设置为 0,当关闭浏览器后浏览器会删除 cookie
Secure如果设置为 true,请求只会在通过 HTTPS 安全连接发送时才会携带 cookie
HttpOnly如果设置为 true,JavaScript 将无法访问 Cookie。这有助于防止跨站点脚本攻击(XSS)窃取 Cookie 数据
SameSite控制了 Cookie 是否会在跨站点请求中发送。可以设置为 "Strict"、"Lax" 或 "None"。"Strict" 表示只在同一站点请求中发送,"Lax"(谷歌80版本之后的默认值) 表示在部分跨站点请求中发送,"None"(谷歌80版本之前的默认值) 表示在所有请求中发送(但是在谷歌 80 版本之后如果设置为 'None', 那么 Secure 必须为 true)。

上面的属性中影响 cookie 是否能携带成功的属性有 Domain、Path、Expires/Max-Age、Secure、SameSite。

Domain

cookie绑定的域名,属性值是服务器端的域名,指定了可以访问该 Cookie 的域名,如果不指定,该属性默认为同一 host 设置 cookie,不包含子域名,例如浏览器发起请求的 url 为 “example.com.cn/url”, 那么只有向域名为 “example.com.cn” 的服务器发起请求时才会带上cookie,如果服务端设置了 domain 的值,例如:

import KoaRouter from "koa-router";
const router = new KoaRouter();

router.get("/get/simple", async (ctx, next) => {
  // 获取请求头
  const { host, origin } = ctx.headers;
  //设置响应头
  ctx.response.set({
     "Access-Control-Allow-Credentials": true,
     "Access-Control-Allow-Origin": origin,
  });
  // 设置 cookie
 ctx.cookies.set("my-cookie", 'mycookie', {
    domain: 'example.com.cn',
  });
});

那么在向域名 ”*.example.com.cn” 的服务器发起请求时都能带上 cookie

Path

指定了可以访问 Cookie 的路径,属性默认值是'/',例如浏览器发起请求的 url 为 “example.com.cn/get/url”, 如果不设值那么对 url 的路径没有限制,都能携带 cookie, 如果设了值,例如值设为 “/get”,那么只有以 "/get" 开头的请求路径才能携带 cookie

Expires/Max-Age

这个好理解,就是给 cookie 设置一个过期时间,时间到了浏览器就删除 cookie,之后的请求不再携带 cookie

Secure

如果设置为 true,请求只会在通过 HTTPS 安全连接发送时才会携带 cookie,需要注意的是在谷歌 80 版本之后,如果设置 SameSite 为 None,Secure 必须设置为 true,即只有 HTTPS 请求才能携带 cookie。

SameSite

控制了 Cookie 是否会在跨站点请求中发送。可以设置为 "Strict"、"Lax" 或 "None"。

  • Strict: 表示只在同一站点请求中发送,
  • Lax谷歌80版本之后的默认值,表示在部分跨站点请求中发送,
  • None: 表示在所有请求中发送(但是在谷歌 80 版本之后如果设置为 'None', 那么 Secure 必须为 true)。

需要注意的是上面所说的跨站和跨域不是同一个概念,先来看看什么是跨站

同源策略作为浏览器的安全基石,其「同源」判断是比较严格的,相对而言,Cookie中的「同站」判断就比较宽松:只要两个 URL 的 eTLD+1 相同即可,不需要考虑协议和端口。其中,eTLD 表示有效顶级域名,注册于 Mozilla 维护的公共后缀列表(Public Suffix List)中,例如,.com、.com.cn、.github.io 等。eTLD+1 则表示,有效顶级域名+二级域名,例如 taobao.com 等。

也就是说当 顶级域名+二级域名 相同时就属于同站,举几个例子,www.taobao.comwww.baidu.com 是跨站,www.a.taobao.comwww.b.taobao.com 是同站,a.github.io 和 b.github.io 是跨站(注意是跨站)。

跨站之后3个属性值携带 cookie 的表现如下:

跨域和跨域携带cookie失败的N种情形详解

可以看到,跨站之后谷歌80版本之后的浏览器发出的 AJAX 请求都不会携带 cookie。

分析到这里再回过头来看最开始的问题 2 和 问题 3

问题2解答

问题2是这样的:

在 APP 上 登录成功后后端产生了 cookie,发起请求能正确携带 cookie,但是将 H5 页面分享到微信上,在微信里打开页面,登录成功后后端产生了 cookie,但是发起请求未能正确携带 cookie,后端报鉴权失败,同样的代码为什么在 APP 的 webview 里没问题,在微信上就有问题?

这个问题在解决过程中看了一下服务端设置的 cookie 属性,发现服务端没有设置 cookie 的 path 属性,猜想可能是APP 的 webview 内核在 cookie 没有设置 path 属性时默认为 ‘/’,但是在微信端可能就是当没有设置 cookie 的 path 属性时任何请求路径都不能携带 cookie ,所以通知后端显示加了 cookie 的 path 属性为 '/'时,问题解决了!

问题3解答

问题3是这样的:

本地起的前端工程没有用域名,而是 ip 地址,如 http://127.0.0.1:3000 ,前后端都设置了跨域且能携带 cookie,登录成功后后端成功产生了 cookie,但是在请求接口 test.gf.com.cn 时携带 cookie 失败了,将 ip 地址 127.0.0.1 映射到域名 localhost.gf.com.cn 后,用 localhost.gf.com.cn 打开页面,请求接口时能正确携带 cookie,只是将 ip 地址改为了用域名访问为什么能携带 cookie 成功?

问题3实际上就是 cookie 的属性 SameSite 的问题,在谷歌 80 版本之后 SameSite 的默认值为 Lax,上面分析过了,所有的 AJAX 请求都不能携带 cookie,当将 ip 地址映射成域名 localhost.gf.com.cn 后携带 cookie 成功,是因为请求的服务端的域名为 test.gf.com.cn,页面的域名为 localhost.gf.com.cn,他们属于同站了,自然就能携带 cookie 成功了。

转载自:https://juejin.cn/post/7283708928348848165
评论
请登录