Koa 依赖的库 cookies(koa使用)
在使用 koa 时,我们可以使用 context 下面的 cookies 来进行 cookie 管理,在源码可以看到,context 下面的 cookies 实际上就是一个 Cookies 的实例,它是由 cookies 库提供的。
我们操作 cookie 最常用的方法是 get 和 set,服务器可以在 response header 中通过 Set-Cookie 来给浏览器添加 cookie,浏览器收到 Set-Cookie header 后会将 cookie 内容保存,当用户使用浏览器再次请求同域资源时,浏览器会自动在 request header 中添加 cookie 信息。因此 cookies 库中的 get 和 set 的实现原理就是控制这两个 header。
常规 cookie get 和 set 逻辑并不复杂,和其他 header 的处理方式一样,get 时使用正则表达式匹配 cookie header,在 set 时把参数处理成字符串设置到 Set-Cookie header 上,由于 Cookie 的结构比较复杂,在 cookies 内部定义了一个 Cookie 类型来封装 Cookie 格式数据:
function Cookie(name, value, attrs) { if (!fieldContentRegExp.test(name)) { throw new TypeError('argument name is invalid'); } if (value && !fieldContentRegExp.test(value)) { throw new TypeError('argument value is invalid'); } this.name = name this.value = value || "" for (var name in attrs) { this[name] = attrs[name] } if (!this.value) { this.expires = new Date(0) this.maxAge = null } if (this.path && !fieldContentRegExp.test(this.path)) { throw new TypeError('option path is invalid'); } if (this.domain && !fieldContentRegExp.test(this.domain)) { throw new TypeError('option domain is invalid'); } if (this.sameSite && this.sameSite !== true && !SAME_SITE_REGEXP.test(this.sameSite)) { throw new TypeError('option sameSite is invalid') } } Cookie.prototype.path = "/"; Cookie.prototype.expires = undefined; Cookie.prototype.domain = undefined; Cookie.prototype.httpOnly = true; Cookie.prototype.sameSite = false; Cookie.prototype.secure = false; Cookie.prototype.overwrite = false; 复制代码
在上面我们可以看到设置 cookie 时我们可以添加的属性信息,这些内容会创建一个 Cookie 对象,最终通过 toHeader 方法来把对象转为字符串格式:
Cookie.prototype.toHeader = function() { var header = this.toString() if (this.maxAge) this.expires = new Date(Date.now() + this.maxAge); if (this.path ) header += "; path=" + this.path if (this.expires ) header += "; expires=" + this.expires.toUTCString() if (this.domain ) header += "; domain=" + this.domain if (this.sameSite ) header += "; samesite=" + (this.sameSite === true ? 'strict' : this.sameSite.toLowerCase()) if (this.secure ) header += "; secure" if (this.httpOnly ) header += "; httponly" return header }; 复制代码
由于 cookie 常用于保存用户身份信息,因此对 cookie 有很高的安全要求,cookies 库提供了签名相关逻辑,这里是使用 keygrip 库实现的。在 get 方法中传入参数 { signed: true }
可以获取签名 cookie,签名 cookie 的 key 为原始名后面加 .sig 后缀,匹配签名 cookie 有几种情况:
如果签名 cookie 哈希与第一个 key 匹配,则返回原始 cookie 值。
如果签名 cookie 哈希与任何其他 key 匹配,则返回原始 cookie 值并将签名cookie 的值更新为第一个 key 的哈希。
如果签名 cookie 哈希与任何 key 都不匹配,则不返回任何内容并删除 cookie。
Cookies.prototype.get = function(name, opts) { var sigName = name + ".sig" , header, match, value, remote, data, index , signed = opts && opts.signed !== undefined ? opts.signed : !!this.keys header = this.request.headers["cookie"] if (!header) return match = header.match(getPattern(name)) if (!match) return value = match[1] if (!opts || !signed) return value remote = this.get(sigName) if (!remote) return data = name + "=" + value if (!this.keys) throw new Error('.keys required for signed cookies'); index = this.keys.index(data, remote) if (index < 0) { this.set(sigName, null, {path: "/", signed: false }) } else { index && this.set(sigName, this.keys.sign(data), { signed: false }) return value } }; 复制代码
在 set 方法的 opts 参数中我们可以设置 cookie 的属性信息:maxAge
、expires
、path
、 domain
、 secure
、 httpOnly
、sameSite
、signed
、overwrite
等,其中对于设置了 secure 为 true 的 cookie 需要检查是否为安全环境,非安全环境不可以发送:
req.protocol === 'https' || req.connection.encrypted 复制代码
对于设置 signed 为 true 的 cookie,在设置 cookie 前会添加签名,即 key 后面添加 .sig 后缀,值使用 keygrip 签名处理。
处理好的 cookie 会调用 pushCookie 方法,使用 toHeader 把 Cookie 对象转为字符串信息保存,如果 overwrite 为 true 会删除同名的 cookie。
最后调用 response 上的 setHeader 方法添加 Set-Cookie 来为浏览器设置 cookie。这里有一个应该是版本兼容的判断,如果 response 上面有 set 方法此时应该没有 setHeader 方法,这种情况调用的是 http.OutgoingMessage.prototype 的 setHeader 方法,OutgoingMessage 是 response 的父类,相关方法可以在 node 文档中查到。
var setHeader = res.set ? http.OutgoingMessage.prototype.setHeader : res.setHeader setHeader.call(res, 'Set-Cookie', headers) 复制代码
至此 cookies 库内部的处理就完成了,在实际开发中使用 cookie 很方便,但是有时也会带来限制和安全问题,因此实际使用时需要结合具体的场景。
作者:丨隋堤倦客丨
链接:https://juejin.cn/post/7031711750416416776