BOM – Cookie 和 LocalStorage
前言
Cookie 和 LocalStorage 是非常基础的东西. 我是学编程后, 第 3 年才开始写博客的, 所以很多在第 1, 2 年学的知识完全都没有记入下来. (比如 C#, JS 语法等等)
Cookie 和 LocalStorage 也是其中的一个. 今天就补上呗.
参考:
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
做 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; }, });
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· 【全网最全教程】使用最强DeepSeekR1+联网的火山引擎,没有生成长度限制,DeepSeek本体
2019-01-08 Asp.net core 学习笔记 SignalR