最近在看cookie相关知识,简单写一个总结笔记,方便以后复习查看。
cookie解决什么问题
http是"无状态"协议,为了记住客户端的登录状态,服务端需要在客户端保存一些信息。这些信息有很多种保存方式,比如cookie localstorage 等等
cookie是浏览器最早的保存客户端登陆状态的方式,其兼容性好,并且从服务器设置cookie到浏览器携带cookie都是全自动完成,对于前端开发人员可以说是透明的。
你可以将cookie理解成一个数据的载体,服务端在完成客户端的登录检验之后会自动向客户端的浏览器内种入cookie信息,其保存的具体内容通常由服务端决定。
当浏览器再次发起请求时,会根据cookie的配置信息,比如 路径 domain域 是否过期等等 来决定携带哪些cookie到请求中,服务端收到再次发来的请求后,通过检验cookie中保存的信息,来判断客户端是否有访问资源的权限。
需要注意,cookie是明文传输和保存的,切勿将敏感信息(如用户密码)保存到cookie中。
如何使用cookie
有两种方式可以设置cookie
- 一种是服务端通过响应头中的 setCookie字段设置
- 另外一种是通过Javascript调用document.cookie接口进行设置。
通常,我们通过服务器对cookie进行设置,浏览器只起到保存并且携带cookie的作用。
设置cookie的格式一般如下,通过 “;” 来分割各项配置的信息
key=value;domain=xxx;path=xxx; httponly;scure;max-age=xxx
如,你可以通过Js直接设置 cookie
你也可以通过服务器在相应头中,设置setCookie字段来在浏览器中种cookie
比如在Koa中,通过设置ctx.cookies 即可设置相应头中的set-Cookie字段, 如下为一个简易的登录检验并且设置cookie的node代码
/** 登陆 */
async login({ username, password }: LoginType) {
const { id, expire_date } = await this.checkUser({
username,
password,
});
const userToken = await this.tokenService.generateToken({
username,
id,
});
// 设置token cookie
this.ctx.cookies.set(UDA_TOKEN_KEY, userToken, {
//h maxAge 单位是 毫秒
maxAge: expire_date * 24 * 3600 * 1000,
// 只能http中使用 js无法获得
httpOnly: false,
// 开发环境中可以使用 http 生产环境必须使用 https
secure: false,
// 是否允许跨站 Lax
sameSite: "Lax",
// path base路径
path: "/",
});
return "OK"
}
其会在登录请求的响应头中设置cookie如下
浏览器会自动保存其中的cookie,并且在后续请求中携带这个cookie给后端进行身份认证。
cookie所携带的信息通常由后端服务器确定,你可以携带任何非敏感的信息,比如sessionId,JWTToken 等等,只要这个数据可以用来检验用户身份,并且是非敏感的即可。
⚠️⚠️⚠️ 切记 不要将敏感信息直接存入cookie,会造成用户隐私泄露!
cookie属性
你可以将cookie理解成一个 key=value 字典,其中保存着cookie的key, value ,以及描述其本身特性的元数据信息
key: cookie的键 通常由 key=value 的方式设置
value: cookie的值 通常由 key=value 的方式设置
expires: 绝对过期时间,传入一个时间戳或者格式化的Date时间
max-age: 相对过期时间,传入一个数字,单位秒,表示xxx 秒后过期,一般用 60*60*24*day设置
http-only: 表示只有服务端可以访问到这个cookie,用户通过javascript的方式无法访问
secure: 表示当前的cookie只在 https中生效,对于http请求不会携带本cookie
domain: cookie的域,浏览器在发送请求的时候,会携带匹配domain的域。 这里分两个步骤
1. 当服务器设置cookie的时候,设置的域必须是当前请求所在的域或者主域(父域)才可以生效。 比如 当前www.baidu.com 中,只能设置
domain = www.baidu.com 或者
domain = baidu.com
但是你不能设置 domain=wenku.baidu.com 或者 www.jd.com
因为前者为www.baidu.com的兄弟域 后者跨站了
2. 当浏览器发送请求的时候,会携带匹配domain的cookie,如何定义匹配domain的cookie,即当前请求所在的的域和cookie的domain相等 或者为cookie domain的子域。
比如,当前cookie设置的domain为 baidu.com
那么,wenku.baidu.com , zhidao.baidu.com 都是baidu.com的子域 cookie都会被携带
再比如,当前cookie设置的domain为 wenku.baidu.com 那么 zhidao.baidu.com的cookie就不会被携带,只有 a.wenku.baidu.com / b.wenku.baidu,com 这些wenku.baiodu.com 的子域才可以携带
总结一下就是,cookie设置domain的时候,只能设置 当前域或当前域父域 -> 更宽松的设置
cookie匹配的时候,只能匹配当前域或者当前域子域, -> 更严格的匹配
path 当前cookie的路径,这个其实和domain的逻辑类似
1. 当设置cookie的时候,path只能设置为当前请求路径或者当前请求路径的父路径
比如,当前请求的路径为 /web/home 你可以设置path为 /web/home , /web , / 这些父路径
但是你不能设置path为 /web/home/xxxx 即可以向宽松的方向设置,但是不能向更严格的方向设置
2. 当浏览器发送请求匹配cookie路径的时候,只有请求的路径为cookie的path或者为cookie path的子路径时,才能匹配到并且携带cookie 比如说
当前cookie的path为 /web 当请求 /web/home /web/about /web/home/xxx的时候,都会携带此 cookie 但是当你访问 / 的时候,由于/是 /web的父路径,不会匹配到。 即匹配的时候只能向严格的方向匹配,不能向宽松的方向匹配。
domain和path的设置和匹配,大致如图所示
cookie的修改
如何区分出一条cookie? 一条cookie是通过 key domain path 三个属性共同区分的,你可以理解成一个cookies列表中,通过组合 key domain path作为一条cookie记录的主键
所以 如果我们需要修改cookie,只需要保证 key domain path都相同,并且修改其他值即可。
一个很常用的修改cookie的方式就是删除cookie,当用户登出账户的时候,我们通常设置cookie的max-age为-1 来让浏览器从cookies列表中删除这一条 cookie 如下:
/** 登出 */
async logout() {
// 设置token cookie
this.ctx.cookies.set(UDA_TOKEN_KEY, "", {
// 默认过期时间1h maxAge 单位是 毫秒
maxAge: -1,
// 只能http中使用 js无法获得
httpOnly: false,
// path base路径
path: "/",
});
}
CRSF
通过上面我们可以看到,浏览器对于cookie发送的限制是比较宽松的,这就带来了很多的安全隐患,其中一个比较经典的就是 CRSF 即跨站的请求伪造攻击。
比如在用户在某个银行站点 www.bank.com 登陆过,其cookie已经被保存在用户浏览器中。
如果恶意用户在某个论坛中,通过伪造,破坏标签的方式插入了一个form表单,其请求地址被设置成了 www.bank.com/submit-money 当用户访问 www.blog.com的时候,其页面中渲染的form表单会向www.bank.com发送转账的请求。
由于当前用户已经登陆过银行的网站并且其浏览器内保存了还没过期的cookie认证信息,所以银行会认为当前转账请求是用户发起的正常转账行为,这就在用户不知情的情况下,发生了恶意转账。 这就是CRSF攻击。
CSRF攻击能够发生的本质在于,浏览器对于跨站的(即非相同站点)的请求携带cookie相对宽松,www.blog.com中,发送www.bank.com这样不在一个域下的请求本不应该能携带cookie,但是是否携带cookie取决于发送请求的 domain和path 是否匹配 而不会检查这个请求是不是在相同的浏览器地址栏中的域中发出的。
也就是说 当发送 www.bank.com 这个请求时,我们需要检查当前发出这个请求的站点(www.blog.com)和发出的请求( www.bank.com/submit-money )是否同站,如果不是需要做一些限制。
跨站与SameSite配置
如何定义跨站? 我们需要先了解一个概念 "有效顶级域名"
有效顶级域名 eTLD:
你可以理解为,用户不能再注册的域名,比如 .cn .com
需要注意,有一些特殊的有效顶级域名 比如 github.io .com.cn
可以将其看成一个整体,比如 a.github.io 其有效顶级域名不是 .io 而是 github.io
我们搭建网站购买域名时,一般会在某个有效顶级域名下,购买一个 eTLD+1的子域名,比如 baidu.com 就是在 .com 顶级域名下,购买了 baidu这个子域。对于 baidu.com 下的所有子域,都是同站的 如 zhidao.baidu.com wenku.baidu.com ...
所以我们一般把 eTLD+1相同的域名,定义为同站的域名
比如 a.example.com b.example.com
wenku.baidu.com zhidao.baidu.com
需要注意, a.github.com 和 b.github.com不同站 因为github.io是有效顶级域名,a b 是github.io下的两个子站点。
上面我们提到,在其他站点允许请求携带cookie会带来安全隐患,为了解决这个bug,引入了SameSite属性。
SameSite属性在种cookie的时候设置,用来限制跨站请求发送时携带cookie的行为。这个属性包含三种属性值
- Strict严格 跨站的情况下 严格禁止携带cookie
- Lax宽松 跨站的情况下 只有在<a>标签这类导航跳转请求,可以携带cookie (默认值)
- None不限制 不论是否跨站 都可以携带cookie 需要注意,开启此模式下,secure必须是 true 也就是必须在 https的情况下,才允许开启。
为什么设置了Lax宽松模式?
因为有时候我们需要保证用户可以在其他站点跳转到本站点的操作,比如邮件中收到一个连接,如果禁止携带cookie,那么跳转过来的时候无法检验其权限,造成用户体验变差。
而a标签跳转链接本质上是个 Get请求,在restapi规范中,Get请求本质上是读取数据,不会对服务器造成破坏,所以折中设置了Lax这么一个模式。
但是,这种设定要求开发者必须遵循Restapi规范,如果开发者将Get请求设置成一些会影响后台服务本身的操作,比如用Get请求修改数据库,那么即使设置了Lax,也会造成安全隐患。
ajax携带cookie
最后,我们说一下 跨域 和 跨站
这两个状态很容易混淆, 有效顶级域名 + 1 即 eTLD+1相同即可满足同站,反之是跨站
而跨域 要求 协议 域名 端口 三者必须相同,才不会跨域。
可以得出结论 跨域不一定跨站 跨站一定跨域
跨域主要影响的,一般是在浏览器中通过javascript发送ajax请求。默认情况下,ajax(不论是XMLHttpRequest还是fetch)发出的请求同域请求默认携带cookie。对于跨域的请求不携带cookie的,不论是否同站。
如果需要ajax在跨域请求中携带cookie,需要设置 withCredential属性为 true (Xhr) fetch中需要设置 credentials: 'included' 这样在发出的跨域ajax请求中,如果存在匹配的cookie就会被携带
(注意 withCredentials只是允许跨域的ajax请求携带cookie,至于是否携带,还要看cookie和请求是否匹配)
在请求返回的时候,服务器需要设置Access-Control-Allow-Credentials: true 表示服务器允许跨域携带cookie,这样浏览器才不会对跨域请求进行拦截。 如果没有这个属性在相应头中,浏览器会对携带cookie的跨域请求进行跨域限制。