饼干的结构解析

本篇 blog 介绍了 Cookie 的结构,以及 Cookie 的各个属性的作用与关联,之后可通过一个 flask 服务来简单实践。

前置环境

  • python
  • flask
  • 一个浏览器
  • 一个 http 请求工具,如 curl
属性作用
Domain标记 cookie 的域,会对比请求中 cookie domain 属性与目标服务器的域名比较,一致或者为子域才会进行后续 path 匹配
Pathcookie 作用的 URL 路径
Expirescookie 过期时间
Max-Agecookie 过期时间,单位为秒
Securecookie 只能通过 https 传输,该属性只能在 https 站点设置
HttpOnlycookie 只能通过 http 传输,不能通过 js 访问
SameSitecookie 只能在同站点下使用,防止跨站攻击
Namecookie 名称
Valuecookie 值如果值为Unicode字符,需要为字符编码。如果为二进制数据,则需要使用BASE64编码
Prioritycookie 优先级,值为 Low、Medium、High,当 cookie 超量后优先级低的可能不会被发送

cookie 属性

生命周期

  • Expires
    格式为 http-date GMT 格式,如 Fri, 01 Dec 2023 12:01:00 GMT
    用于强制删除 cookie,可通过将 expires 设置为过去的时间来实现。
  • Max-Age
    为 cookie 过期时间,单位为秒

Expires 和 Max-Age 二者只能存在一个,如果同时存在,优先使用 Max-Age。
未设置过期时间的 cookie 称为 session-cookie,浏览器会在会话结束时删除该 cookie,会话何时结束则取决于浏览器的定义。

GMT
格林尼治标准时间。在 HTTP 协议中,时间都是用格林尼治标准时间来表示的,而不是本地时间

cookie 的作用范围由 DomainPath 共同决定,浏览器会先检测请求域名和 Domain 是否匹配,如果匹配则再检测 Path 是否匹配。

  • Domain
    如当前响应中 set-cookie 设置了 Domain=nikunokoya.com 则访问子域名 vps.nikunokoya.com 时会携带该 cookie,如果为设置 Domain 则默认会将设置该 cookie 的服务器作为 Domain
  • Path
    Path 为 cookie 的作用路径,如 Path=/index/ 则访问 nikunokoya.com/index/a/b/c 时会携带该 cookie,如果不设置 Path 则默认为 /,即所有路径都会携带该 cookie。

SameSite

用于判断该 cookie 是否应该与跨站请求一起发送,以防止跨站请求伪造攻击(CSRF),SameSite 有三种取值:

  • Strict
    最严格模式,会禁止跨域请求携带该 cookie,例如一些修改密码或购物的服务请求,需要使用该模式保证服务的处理符合安全预期,而不是从某个邮件链接跳转过来并携带 cookie 造成安全隐患。
  • Lax
    会允许一部分跨域请求携带该 cookie,比如从在一个网站浏览另一个网站的图片时并不会携带 cookie,而跳转到该网站是会携带 cookie。 chrome 浏览器在未设置 SameSite 时默认为 Lax 模式。
  • None 允许所有跨站请求携带该 cookie,但是需要同时设置 Secure 属性,即只能通过 https 传输。
CSRF/origin/site
  • Same-Origin
    请求的协议、域名、端口号任意一个不同都会被认为是跨域请求。
    1. 比如从 https://www.a.com:443 发起一个请求到 https://www.b.a.com:443,这个请求就是跨域请求。
    2. https://www.a.com:443https://www.a.com:80 也是跨域请求。
  • Same-Site
    判断是否跨站的标准更加宽松,会根据 eTLD+1 是否相同并且协议相同来判断是否跨站。 https://vps.nikunokoya.com:443https://www.nikunokoya.com:443 为同站,a.comb.com 为跨站。
  • CSRF
    比如用户在 a.com 登录了账号,然后在不退出 a.com 的情况下访问了 b.com,此时 b.com 可以通过 a.com 的 cookie 伪造成用户,向 a.com 发起请求。
  • eTLD
    有效顶级域名,公共后缀列表可在 publicsuffix.org 查看。

前缀语义

cookie 前缀语义用于通知浏览器该 cookie 的使用场景,浏览器会根据前缀判断是否应该发送该 cookie。

__Host- 前缀

__Host- 开头的 cookie,表示该 cookie 必须与 SecurePath=/ 属性一起使用,并且不能设置 Domain 属性。该前缀检查最严格,cookie 不会发送给任何子域,只会发送给当前域名的不同 path

__Secure- 前缀

当 cookie name 以 __Secure- 开头时,表示该 cookie 必须与 Secure 属性一起使用。当需要向不同的子域名发送 cookie 时,可以使用该前缀,但是需要注意 Domain 属性的设置。

当需要跨域携带 cookie 时需要设置 third-party cookie,三方 cookie 通常用于个性化推荐,广告投放等场景,记录用户访问习惯,但是也会造成用户隐私泄露。
三方 cookie 通常需要调用第三方的 cookie 服务来设置。

服务端和浏览器允许跨域请求

当发送 fetch 请求时如果是跨域请求,浏览器会在请求头中添加 Origin 字段标记请求来源,后端服务中需要在响应 Header 中设置 Access-Control-Allow-Origin 字段来允许跨域请求,如果设置为 * 则表示允许所有跨域请求,如果设置为 null 则表示不允许跨域请求。

w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")

同时在前段的 fetch 请求中需要设置 credentials 字段为 include,表示允许跨域请求携带 cookie。

  • credentials=include
    表示允许跨域请求携带 cookie,但是需要设置 Access-Control-Allow-Origin 字段必须为当前请求的源,不能为 *
  • credentials=same-origin
    同源时发送 cookie。
  • credentials=omit 表示不允许跨域请求携带 cookie。

通过代理

请求先发送给代理插件(此时未发生浏览器跨域),再由代理服务发送跨域请求到目标服务,目标服务响应后再由代理服务返回给浏览器。

注意
跨域仅发生在浏览器中,如果是服务端发起的请求则不会发生跨域。

demo

目录结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.
├── cookies
├── flask_app.py
├── mycookies
├── npx_server
│   ├── index.html
│   ├── index.js
│   ├── node_modules
│   ├── package.json
│   └── package-lock.json
├── static
│   └── index.js
└── templates
    └── index.html

flask 服务

以下用 flask 起了一个简易的服务,当访问本地的 /get-cookie/ 时会设置一个名为 id 的 cookie 并存储在浏览器中。
同时通过 CORS 设置允许跨域请求,当访问 /api/auth/ 时会检测 cookie 中的 id 是否为 123,如果是则返回一个 json,否则返回一个错误信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from flask import Flask, make_response, request, jsonify, render_template
from flask_cors import CORS

app = Flask(__name__)
CORS(app=app, supports_credentials=True)


@app.route("/", methods=["GET"])
def index():
    return render_template("index.html")


@app.route("/get-cookie/", methods=["GET"])
def get_cookie():
    response = make_response("my cookie: ")
    response.set_cookie("id", "123", path="/api/auth", max_age=600, httponly=True)
    return response


@app.route("/about/", methods=["GET"])
def about():
    print(request.cookies)
    return "about"


@app.route("/contact/", methods=["GET"])
def contact():
    print(request.cookies)
    return "contact"


@app.route("/api/auth/", methods=["GET"])
def auth():
    if request.cookies["id"] == "123":
        res = [{"name": "niku", "id": 1}, {"name": "xx", "id": 2}]
        return jsonify(res)
    return jsonify(msg="Ops!")

启动服务:

1
FLASK_ENV=development FLASK_APP=flask_app.py flask run

curl -I http://127.0.0.1:5000/get-cookie/ --cookie cookies

1
2
3
4
5
6
7
HTTP/1.1 200 OK
Server: Werkzeug/3.0.1 Python/3.9.7
Date: Mon, 04 Dec 2023 09:11:22 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 11
Set-Cookie: id=123; Expires=Mon, 04 Dec 2023 09:21:22 GMT; Max-Age=600; HttpOnly; Path=/api/auth
Connection: close

cookie 传递作用域可在浏览器的开发者工具中验证,如 chrome 的开发者工具中的 Application -> Storage -> Cookies

html 和 js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<button>FETCH</button>
</body>
<script src="{{ url_for('static', filename='index.js') }}"></script>
</html>

发送 fetch 请求设置 cookie:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const button = document.getElementsByTagName("button")[0];

button.addEventListener("click", function() {
  getACookie().then(() => getData());
});

function getACookie() {
  return fetch("/get-cookie/").then(response => {
    // make sure to check response.ok in the real world!
    return Promise.resolve("All good, fetch the data");
  });
}

function getData() {
  fetch("/api/auth/")
    .then(response => {
      // make sure to check response.ok in the real world!
      return response.json();
    })
    .then(json => console.log(json));
}

npx serve 启动另一个不同源的服务

在另一个目录下将下面 index.html 通过 npx serve 启动另一个服务 http://localhost:3000/,在该服务下发送跨域请求到 flask 服务。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<button>FETCH</button>
</body>
<script src="index.js"></script>
</html>

注意这里需要在 fetch 请求中配置 credentials:"include"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const button = document.getElementsByTagName("button")[0];

button.addEventListener("click", function() {
  getACookie().then(() => getData());
});

function getACookie() {
  return fetch("http://localhost:5000/get-cookie/", {
    credentials: "include"
  
  }).then(response => {
    // make sure to check response.ok in the real world!
    return Promise.resolve("All good, fetch the data");
  });
}

function getData() {
  fetch("http://localhost:5000/api/auth/", {
    credentials: "include"
  
  })
    .then(response => {
      // make sure to check response.ok in the real world!
      return response.json();
    })
    .then(json => console.log(json));
}

0%