前言

Cookie 和 LocalStorage 是非常基础的东西. 我是学编程后, 第 3 年才开始写博客的, 所以很多在第 1, 2 年学的知识完全都没有记入下来. (比如 C#, JS 语法等等)

Cookie 和 LocalStorage 也是其中的一个. 今天就补上呗.

参考:

面试不再怕:史上最全的cookie知识点详解

cookie 详解

Cookie 的作用

HTTP 是无状态的. 前一个请求和后一个请求没有任何关联. 服务端无法判断是同一个 "人" 发出的请求. 这就导致了很多功能没有办法实现. 比如用户登入.

要解决这个问题不能从 HTTP 协议着手, 那只能靠游览器搞一些额外的潜规则了. Cookie 就是这么一个存在.

Cookie 如何实现 "有状态" 的 HTTP

上面提到了游览器的潜规则. 它的过程是这样的.

当游览器发请求给服务端时, 服务端可以在 response 的 header 里加入一个特别的 header 叫 "Set-Cookie"

当游览器接收 response 时会看看有没有这个特别的 header, 如果有, 那就表示服务器想搞一个 "状态". 比如: Set-Cookie : "key=value". 游览器会把这个 key value 记入起来.

在下一次游览器发请求给服务端时, 游览器会把之前记入起来的 key value 放入 header "Cookie" 中.

通过这样的 "潜规则", 游览器和服务端就利用 HTTP 协议的 header 让原本没有状态的 HTTP 变成了 "有状态".

其实 HTTP 只是一种通信协议. 只要在内容上做出规则. 双边是很容易 "认出" 对方的. Cookie 只是游览器替我们封装好的一个方式而已.

比如 Mobile App 就没有 Cookie 但依然可以靠 HTTP + bearer token 来实现 OAuth 登入.

服务端 "Set Cookie" Header

ASP.NET Core

HttpContext.Response.Cookies.Append("Key", "Value");

效果

这个就是一个最简单的服务端 response with Cookie

一个 Set-Cookie 表达一个 key value. 如果想返回多个. 那么就返回多个 header "Set-Cookie".

HttpContext.Response.Cookies.Append("Key", "Value");
HttpContext.Response.Cookies.Append("Key1", "Value1");

游览器 "Cookie" Header

当游览器接收到服务端返回的 Cookie 以后就会记入起来.

下一次发送请求就会把这些 Cookie 发送出去.

Cookie 的体积

由于游览器每一次请求都会把 Cookie 发到服务端. 所以 Cookie 不可以太大. 不然会影响网速.

不同游览器有不同的标准, 但大部分是每个 domain 只能有 1xx 个 Cookie, 每一个最多 4kb.

总之, 尽可能用的少就对了.

Cookie 的各种配置

Cookie 本质上就是一个 header value. 也就是一个字符串. 但它是有 format 的. 它里面其实表达了很多东西. 不仅仅只是 key value. 我们一个一个看.

key & value

最基本的就是 key value.

cookie: Key=Value; Key1=Value1

通过等于 = 把 key value 分开. 通过分号 ; 把多个 key value 分开. 这就是一个基本的 format.

key

key 不可以包含一些特殊符号. 比如 等于, 逗号, 分号, 空格, 等等. 理所当然丫, 不然游览器要怎么 split 呢.

想要了解详情的可以看这篇: Stack Overflow – What are allowed characters in cookies?

通常我是建议取这种 key 的名字就要顺风水, 不要搞一些奇奇怪怪的符号. 尽可能用 a-z 配 hypen 或 underscore 就好了.

ASP.NET Core 如果放入了不合法的 key name, 它会直接报错.

value

value 就不可能避开各种符号了. 它的解决方法是 encode.

在 ASP.NET Core, cookie value 会被 encode

HttpContext.Response.Cookies.Append("Key", "= ,;");

效果

encode 的方式是 JS 的 encodeURIComponent

expires & max-age

Cookie 有一个过期机制. 当 Set-Cookie 时可以指定一个有效期.

当游览器发现 Cookie 过期后, 它就会删除掉. 服务端也是通过这种方式来实现删除 Cookie 的哦.

如果没有指定有效期, 游览器会在关闭的时候直接删除掉 Cookie, 所以想 Cookie 持久就必须设定 expires 或 max-age

expires

HttpContext.Response.Cookies.Append("Key", "value", new CookieOptions
{
Expires = new DateTimeOffset(2023, 1, 9, 5, 55, 0, 0, TimeSpan.FromHours(8))
});

指定 Cookie 过期时间为 2023年 1月9号 5点 55分 +08:00

虽然 ASP.NET Core 支持 timezone 但其实 Cookie 本身是不支持的, 这里是 ASP.NET Core 替我们做了转换.

游览器接收的是 UTC 时间. 相等于 JavaScript 的 new Date().toUTCString()

max-age

相比于 expires 提供一个绝对时间, max-age 则是提供一个相对时间. 只是另一个表达手法而已. 游览器都明白你的意思.

HttpContext.Response.Cookies.Append("Key", "value", new CookieOptions
{
MaxAge = TimeSpan.FromSeconds(30)
});

它表示从现在开始 30 秒后这个 Cookie 失效.

注: 它的单位是 second(秒) 哦. 不是 ms(毫秒) 哦 (而已不支持小数点. 所以没有 100ms 0.1s 这种冬冬)

HttpOnly

上面我们都只谈到服务端 Set Cookie 和游览器 send Cookie. 其实 JavaScript 也是可以读写 Cookie 的哦.

如果服务端不希望 Set Cookie 被 JavaScript 读取, 那么可以附上一个 "httponly". 这样 JS 就读取不到了 (但游览器是可以读取到的...废话)

Secure

secure 表示, 这个 Cookie 只允许在 HTTPS 加密通信中才可以使用.

Samesite

samesite 是一个比较新的东西 (其实好多年了...), 它是用来防 CSRF 的.

从前没有 samesite 机制, 相等于现在设定 samesite=none.

游览器在发送跨域请求 (ajax) 时会带上 Cookie. 这导致了许多安全隐患. 虽然可以用 Anti-Forgery Tokens 来防御.

但是有很多开发人员不太注重安全就没有做. 后来游览器决定修改这个机制. 就有了 samesite

samesite=lax 是当前默认的. 跨域时只有 GET 请求会附上 cookie. POST 不会.

samesite=strict 表示不管 GET, POST 只要是跨域一概不允许发这个 Cookie.

Path

path=/ 表示任何路径都附上 Cookie.

path=/about 表示这个 Cookie 只有在请求 /about 这个路径才附上.

如果没有声明, 那就表示是当前 request 路径.

注: 只能 set parent path 哦, 比如 request 是 domain.com/a/b/c

默认就是 /a/b/c, 我们可以 set 成 /, /a, /a/b. 但是不能 set 成 x/y/z 或者 /a/b/c/d

Domain

服务端当然可以返回任何 Domain 值, 只是游览器不会处理而已...哈哈.

domain 是用来设置 subdomain 访问的.

domain=.example.com 前面加了一个点, 表示这个 Cookie 适用于 example.com 和其下所有 sub domain.

domain=sub.example.com 表示 Cookie 只用于 sub.example.com 访问.

如果没有声明, 那么 Set-Cookie 就表示当前 request 的 domain.

注: 只能 set parent domain. 比如 request 是 sub.domain.com 可以 set 成 .domain.com

完整版

HttpContext.Response.Cookies.Append("Key", "value", new CookieOptions
{
Domain = ".example.com",
Path = "/",
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Lax,
Expires = DateTimeOffset.Now.AddHours(1),
MaxAge = TimeSpan.FromSeconds(TimeSpan.FromHours(1).TotalSeconds),
});

默认值

JavaScript Cookie

上面提到的都是服务端和游览器间的 Cookie. 其实 JS 也是可以读取到这些 Cookie 的. (只要 Cookie 没有指定 httponly)

Read Cookie

console.log(document.cookie);

效果

它返回的是一个 string. 里头包含了所有能访问到的 Cookie key and value.如果我们只想获取某些 key 那么需要自己从字符串中提取.

另外, JS 只能读取到 key value 而已. 像 expires 这些其它信息是无法读取到的.

Write Cookie

JS 也是可以创建 Cookie 哦

document.cookie = `key2=value2; max-age=3600; secure; path=/;`;
document.cookie = `key3=value3; max-age=3600; secure; path=/;`;

每一次 document.cookie = 都会创建一个 key value 的 Cookie.

调用多次就创建多个. 这里的语法很不直观. 如果改成 document.cookie.add('...') 才比较符合它的行为.

如果要删除 Cookie 就设置 max-age=-1 或者 expires=一个过期的时间 (注: 确保 key, domain, path 相同哦)

value 记得用 encodeURIComponent encode 一下

expires 则用 toUTCString()

附上一个以前写的读写 Cookie

function setCookie(
key: string,
value: string,
config?: {
domain?: string;
path?: string;
expires?: Date;
maxAge?: number;
secure?: boolean;
sameSite?: 'none' | 'lax' | 'strict';
}
): void {
const { expires, maxAge, domain, path = '/', secure = true, sameSite } = config ?? {};
const strings: string[] = [];
const keyValue = `${key}=${encodeURIComponent(value)}`;
strings.push(keyValue);
if (expires) {
strings.push(`expires=${expires.toUTCString()}`);
}
if (maxAge !== undefined) {
strings.push(`max-age=${maxAge}`);
}
if (domain !== undefined) {
strings.push(`domain=${domain}`);
}
strings.push(`path=${path}`);
if (secure) {
strings.push('secure');
}
if (sameSite !== undefined) {
strings.push(`samesite=${sameSite}`);
}
document.cookie = strings.join('; ');
} function getAllCookie(): Record<string, string> {
const allCookie: Record<string, string> = {};
const cookieString = document.cookie;
for (const keyValue of cookieString.split(';')) {
const [key, value] = keyValue.trim().split('=');
allCookie[key] = decodeURIComponent(value);
}
return allCookie;
} function getCookie(key: string): string | null {
const allCookie = getAllCookie();
return allCookie[key] ?? null;
} function deleteCookie(key: string, config?: { domain?: string; path?: string }): void {
const { domain, path = '/' } = config ?? {};
const strings: string[] = [];
const keyValue = `${key}=value`;
strings.push(keyValue);
strings.push(`max-age=-1`);
if (domain !== undefined) {
strings.push(`domain=${domain}`);
}
strings.push(`path=${path}`);
document.cookie = strings.join('; ');
}

第三方 Cookie

参考: 知乎 – 当浏览器全面禁用三方 Cookie

做 marketing 的人唯一可能听过跟技术相关的词就是第三方 Cookie. 因为前几年苹果为了用(进)户(军)隐(广)私(告) 决定静止第三方 Cookie.

什么是第三方 Cookie 呢?

上面有提到, 如果服务端返回 Set-Cookie 其它 domain 游览器是不理的

JS document.cookie = 其它 domain 也是无效的.

但是如果有一个 <img src="其它 domain" > 而这个请求有 Set-Cookie 其它 domain 确实可以的.

这个就是所谓的第三方 Cookie 了. 在 a.com 请求 b.com 得到 b.com 的 Cookie. 这个 Cookie 就是第三方的.

为什么要第三方 Cookie?

第三方 Cookie 是用来做广告的. 上面的例子

a.com 要想识别 "一个人" 就给他 Cookie 咯. 这个叫第一方

b.com 是广告公司, 它也想识别 "一个人" 在 a.com, 那么它也需要 Cookie 咯.

b.com 无法使用 a.com 的 Cookie, LocalStorage 等等. 所以它只能想办法让这个人访问 b.com 这样才能返回 b.com 的 Cookie.

所以就用到了 img src 这类的方式.

苹果禁止了第三方 Cookie

苹果禁止第三方 Cookie 后, b.com (广告公司) 就无法通过 img src 在 a.com 创建出 b.com 的 Cookie 了.

整个 tracking 就失败了. 而为了解决这个问题, 目前各大广告公司会要求网站创建 a.com 的 Cookie 并且把这个数据发送到广告公司的服务器.

本来是前端干的事, 变成了服务端... 当然这对于网站安全是比较危险的. 毕竟前端插入广告公司的代码不会有非常大的安全隐患. 但如果是服务端必须安装广告公司的 dll 就比较危险了.

LocalStorage 介绍

Cookie 体积小, 不适合存放大数据. 于是 LocalStorage 就诞生了.

它和 Cookie 本质上是不同的东西, 也不是互相替代的.只是它俩有点雷同, 所以经常会放到一起聊.

和 Cookie 的雷同和区别

1. LocalStorage 不会发送到服务端, 服务端也无法创建 LocalStorage. 它完全就是前端的东西.

2. 它们都是用 key value 来存资料

3. Cookie 体积很小, LocalStorage 很大 (好像是 5mb)

4. 它们都是跨域保护, 不同 domain, subdomain 都不可以访问到 LocalStorage

5. Cookie 有 expires, LocalStorage 没有

LocalStorage 使用

set key value

localStorage.setItem('key', 'value');

value 必须是 string. 不需要 encode.

get value

localStorage.getItem('key');

找不到返回 null 而不是 undefined 哦

remove key

localStorage.removeItem('key');

Cookie 通过 set expires 来实现删除. LocalStorage 则有删除接口

remove all

localStorage.clear()

一个方便的接口, 直接删除所有 key

get all keys

const keys = Object.keys(localStorage);
for (const [key, value] of Object.entries(localStorage)) {}

get value by index

const value = localStorage.key(0);

这个接口最好是不要用, 因为 localStorage 的顺序是不可靠

localStorage.key(0);
// 相等于
localStorage.getItem(Object.keys()[0]);

get length

console.log(localStorage.length);

返回当前有多少 keys.

把 localstorage 当 object 调用

get

const value = localStorage['key']; // 找不到返回 undefined 而不是 null 哦
// 相等于
const value = localStorage.getItem('key') ?? undefined;

set

localStorage.key = true;
// 相等于
localStorage.setItem('key', String(true));

自动强转成 string

delete

delete localStorage.key;
// 相等于
localStorage.removeItem(key);

LocalStorage with Expiration

LocalStorage 最大的不方便就是它没有过期机制.

一个常见的 workaround 是把 expires 写入 value 里. 比如

localStorage.setItem('key', 'value; max-age=3600')

当然, 这完全是个人的实现, 要用什么规范都可以. 你可以模拟 Cookie 的方式, 也可以把 value 做成 JSON

localStorage.setItem('key', JSON.stringify({
value: 'value',
expires : new Date()
}))

重点是在 getItem 时需要从 value 中取出时间并且检查过期与否等后续的操作.

这里附上我以前写的一个版本, 支持 expires

interface ExpirableLocalStorage extends Storage {
setItem(key: string, value: string, expireDate?: Date): void;
} interface Data {
expirationDate: Date;
value: string;
}
type FromParseJsonObject<T> = {
[P in keyof T]: T[P] extends Date
? string
: keyof T[P] extends never
? FromParseJsonObject<T[P]>
: T[P];
}; function isData(value: object): value is FromParseJsonObject<Data> {
return Object.keys(value).length === 2 && 'expirationDate' in value && 'value' in value;
} const internalExpirableLocalStorage: ExpirableLocalStorage = {
setItem(key: string, value: string, expirationDate?: Date): void {
if (!expirationDate) {
localStorage.setItem(key, value);
} else {
const data: Data = {
expirationDate,
value,
};
const jsonValue = JSON.stringify(data);
localStorage.setItem(key, jsonValue);
}
}, getItem(key: string): string | null {
const maybeJsonValue = localStorage.getItem(key);
if (maybeJsonValue === null) return null;
try {
const maybeData = JSON.parse(maybeJsonValue);
if (isData(maybeData)) {
if (new Date(maybeData.expirationDate) <= new Date()) {
this.removeItem(key);
return null;
}
return maybeData.value;
} else {
return maybeJsonValue;
}
} catch {
return maybeJsonValue;
}
}, get length(): number {
return Object.keys(localStorage).filter(key => this.getItem(key) !== null).length;
}, key(index: number): string | null {
return (
Object.keys(localStorage)
.map(key => this.getItem(key))
.filter(v => v !== null)[index] ?? null
);
}, removeItem(key: string): void {
localStorage.removeItem(key);
},
clear() {
localStorage.clear();
},
}; const standardKeys = ['setItem', 'getItem', 'length', 'key', 'removeItem', 'clear'];
export const expirableLocalStorage = new Proxy(internalExpirableLocalStorage, {
get(target, prop: string) {
if (standardKeys.includes(prop)) return target[prop];
return target.getItem(prop) ?? undefined;
},
set(target, prop: string, value) {
if (standardKeys.includes(prop)) {
target[prop] = value;
return true;
}
target.setItem(prop, String(value));
return true;
},
deleteProperty(target, key: string) {
target.removeItem(key);
return true;
},
});

BOM – Cookie 和 LocalStorage的更多相关文章

  1. 用cookie实现localstorage功能

    在项目中需要利用到html5的localstorage.但在利用这个属性的时候却发现无法达到预定目标.经过不断的检查及排除,最后发现原因所在: 项目中使用的浏览器是支持localstorage的,但是 ...

  2. 本地存储 cookie,session,localstorage( 一)基本概念及原生API

    http://www.w3school.com.cn/html5/html_5_webstorage.asp http://adamed.iteye.com/blog/1698740 localSto ...

  3. 浏览器本地储存方式有哪些?cookie、localStorage、sessionStorage

    现阶段,浏览器提供的储存方式常用的有三种,cookie.localStorage.sessionStorage 1.cookie 概念:cookie 是浏览器中用于保存少量信息的一个对象 基本特征: ...

  4. jquery访问浏览器本地存储cookie,localStorage和sessionStorage

    前言:cookie,localStorage和sessionStorage都是浏览器本地存储数据的地方,其用法不尽相同:总结一下基本的用法. 一.cookie 定义: 存储在本地,容量最大4k,在同源 ...

  5. JS 详解 Cookie、 LocalStorage 与 SessionStorage

    基本概念 Cookie Cookie 是小甜饼的意思.顾名思义,cookie 确实非常小,它的大小限制为4KB左右.它的主要用途有保存登录信息,比如你登录某个网站市场可以看到"记住密码&qu ...

  6. Cookie、LocalStorage 与 SessionStorage的区别在哪里?

    基本概念 Cookie Cookie 是小甜饼的意思.顾名思义,cookie 确实非常小,它的大小限制为4KB左右.它的主要用途有保存登录信息,比如你登录某个网站市场可以看到“记住密码”,这通常就是通 ...

  7. 深入了解浏览器存储:对比Cookie、LocalStorage、sessionStorage与IndexedDB

    摘要: 对比Cookie.LocalStorage.sessionStorage与IndexedDB 作者:浪里行舟 Fundebug经授权转载,版权归原作者所有. 前言 随着移动网络的发展与演化,我 ...

  8. session,cookie,sessionStorage,localStorage的区别及应用场景

    session,cookie,sessionStorage,localStorage的区别及应用场景 浏览器的缓存机制提供了可以将用户数据存储在客户端上的方式,可以利用cookie,session等跟 ...

  9. 缓存session,cookie,sessionStorage,localStorage的区别

    https://www.cnblogs.com/cencenyue/p/7604651.html(copy) 浅谈session,cookie,sessionStorage,localStorage的 ...

  10. js中cookie,localStorage(sessionStorage)的存取

    一.cookie (原生的不好用,自己简单封装) 1. 存cookie的方法: function setCookie(c_name,value,expiredays) { var exdate=new ...

随机推荐

  1. oeasy教您玩转vim - 30 - # 屏位行号

    ​ 屏位行号 回忆上节课内容 上次我们主要讲的翻页 :set scrolloff=2 控制上下留天留地 上下翻页(这个最常用) ctrl + f 向下一屏 ctrl + b 向上一屏 上下移屏一行 c ...

  2. ABC352

    A link \(x\)停不到,\(y\)能停到. 要先判断是从前往后还是从后往前. 点击查看代码 #include<bits/stdc++.h> using namespace std; ...

  3. linux常用命令(每日积累)

    linux查看应用程序的进程号和端口号 lsof -i :port,查看指定端口运行的程序,同时还有当前连接. netstat -nupl  (UDP类型的端口)netstat -ntpl  (TCP ...

  4. 【Java】PDF模板生成PDF文档

    一.需求背景 客户要求一份文书,文书内容有一些表单项,例如: 1.基本的是和否 (单选框或复选框) 2.备注内容(纯文本信息) 3.单位,机构组织,人员,字典项(下拉选择) 4.用户数字签名(图片信息 ...

  5. 【Uni-App】其他配置笔记

    manifest.json manifest.json 文件是应用的配置文件,用于指定应用的名称.图标.权限等.HBuilderX 创建的工程此文件在根目录,CLI 创建的工程此文件在 src 目录. ...

  6. 预处理共轭梯度算法(Preconditioned Conjugate Gradients Method)的代码实现

    前文: 预处理共轭梯度算法(Preconditioned Conjugate Gradients Method) 给出代码: import numpy as np # from rllab.misc. ...

  7. 分享某Python下的mpi教程 —— A Python Introduction to Parallel Programming with MPI 1.0.2 documentation

    如题: 无意中发现了一个Python下的mpi教程<A Python Introduction to Parallel Programming with MPI 1.0.2 documentat ...

  8. from joblib.pool import MemmapingPool 报错

    修改方法: 将 from joblib.pool import MemmappingPool   修改为: from joblib.pool import MemmappingPool     === ...

  9. WSL启动nvidia-docker镜像:报错libnvidia-ml.so.1- file exists- unknown

    参考: https://blog.csdn.net/xujiamin0022016/article/details/124782913 https://zhuanlan.zhihu.com/p/361 ...

  10. 新版的Django Docker部署方案,多阶段构建、自动处理前端依赖

    前言 前几天的文章中,我们已经把使用 pdm 的项目用 docker 搞定了,那么下一步就是把完整的 DjangoStarter v3 版本用 docker 部署. 现在不像之前那么简单直接一把梭了, ...