主题
Cookie JS 实操
浏览器 JS
写入、读取、更新与删除
document.cookie 只返回当前页面可访问的键值对。一次赋值只设置一个 Cookie。以下示例使用当前主机的根路径保存界面偏好,并约定用 URI 编码保存值。
js
/** 读取示例使用 URI 编码保存的偏好值 */
function readTheme() {
const entry = document.cookie
.split(';')
.map((part) => part.trim())
.find((part) => part.startsWith('theme='))
return entry === undefined ? undefined : decodeURIComponent(entry.slice('theme='.length))
}
document.cookie = 'theme=' + encodeURIComponent('浅色') + '; Path=/; Max-Age=86400; SameSite=Lax'
console.log(readTheme())
document.cookie = 'theme=' + encodeURIComponent('深色') + '; Path=/; Max-Age=86400; SameSite=Lax'
document.cookie = 'theme=; Path=/; Max-Age=0; SameSite=Lax'更新和删除要复用原来的名称、域和路径。示例省略 Domain,使用 host-only 作用域。HttpOnly Cookie 由服务端设置和清除。
跨源请求
本例的页面源是 http://localhost:5173,API 是 http://localhost:3000。二者跨源但同站,适合观察凭证模式和 CORS。请求中的自定义头会触发预检。
js
const api = 'http://localhost:3000'
await fetch(api + '/cookies', {
method: 'POST',
credentials: 'include',
headers: { 'X-Demo': '1' },
})
const response = await fetch(api + '/cookies', { credentials: 'include' })
console.log(await response.json())
const xhr = new XMLHttpRequest()
xhr.open('GET', api + '/cookies')
xhr.withCredentials = true
xhr.send()浏览器按策略填充 Cookie 请求头,脚本不能手动设置它,也无法通过 response.headers 或 XHR 读取 Set-Cookie。用浏览器开发者工具的 Network 和 Cookie 存储面板分别检查响应头、存储结果和下一次请求。
Node.js 服务端
读取、设置、删除与 CORS
将以下代码保存为 server.mjs,通过 node server.mjs 运行。服务只监听本机,写入的是演示 Cookie。响应中的数组让 Node.js 发送独立的 Set-Cookie 字段。DELETE /cookies 只清除 theme,保留 density。
js
import { createServer } from 'node:http'
const allowedOrigin = 'http://localhost:5173'
const cookieScope = 'Path=/; SameSite=Lax'
/** 返回同名 Cookie 的全部原始值 */
function readCookieValues(header, name) {
const prefix = name + '='
return (header ?? '')
.split(';')
.map((part) => part.trim())
.filter((part) => part.startsWith(prefix))
.map((part) => part.slice(prefix.length))
}
/** 处理演示 Cookie 和该接口的 CORS 请求 */
function handleCookies(req, res) {
const { method, url, headers } = req
res.setHeader('Vary', 'Origin')
res.setHeader('Cache-Control', 'no-store')
if (url !== '/cookies') {
return res.writeHead(404).end()
}
if (headers.origin && headers.origin !== allowedOrigin) {
return res.writeHead(403).end()
}
if (headers.origin === allowedOrigin) {
res.setHeader('Access-Control-Allow-Origin', allowedOrigin)
res.setHeader('Access-Control-Allow-Credentials', 'true')
}
if (method === 'OPTIONS') {
res.setHeader('Vary', 'Origin, Access-Control-Request-Method, Access-Control-Request-Headers')
const requestedMethod = headers['access-control-request-method']
const requestedHeaders = (headers['access-control-request-headers'] ?? '')
.toLowerCase()
.split(',')
.map((name) => name.trim())
.filter(Boolean)
if (
headers.origin !== allowedOrigin ||
!['GET', 'POST', 'DELETE'].includes(requestedMethod) ||
requestedHeaders.some((name) => name !== 'x-demo')
) {
return res.writeHead(403).end()
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE')
res.setHeader('Access-Control-Allow-Headers', 'X-Demo')
return res.writeHead(204).end()
}
if (method === 'GET') {
res.setHeader('Content-Type', 'application/json')
return res.end(JSON.stringify({ themes: readCookieValues(headers.cookie, 'theme') }))
}
if (method === 'POST' || method === 'DELETE') {
if (headers.origin !== allowedOrigin || headers['x-demo'] !== '1') {
return res.writeHead(403).end()
}
const cookies =
method === 'POST'
? ['theme=dark; ' + cookieScope + '; Max-Age=86400', 'density=compact; ' + cookieScope + '; Max-Age=86400']
: ['theme=; ' + cookieScope + '; Max-Age=0']
res.setHeader('Set-Cookie', cookies)
return res.writeHead(204).end()
}
return res.writeHead(405, { Allow: 'GET, POST, DELETE, OPTIONS' }).end()
}
createServer(handleCookies).listen(3000, 'localhost')跨源删除请求使用与设置请求相同的 credentials: 'include' 和 X-Demo。生产环境的认证 Cookie 应经 HTTPS 发送,使用 Secure; HttpOnly,并根据应用的部署关系设置 SameSite。真正跨站的凭证请求仍受浏览器的第三方 Cookie 策略约束。
