[前端漫谈]浅谈 Cookie

导读

这篇文章基于[RFC 6265],简单说明 Cookie 的使用和特性。大概包括以下四个内容:1)介绍 Cookie 的使用;2)详解 Cookie 的格式;3)测试 Cookie 在各类状况下的反应;4)CSRF 攻击说明html

环境、工具及前置知识

  1. 系统:macOS Mojava
  2. IDE:IDEA
  3. SwitchHosts
  4. OpenSSL
  5. Chrome
  6. js 基础
  7. NodeJS 基础
  8. HTTP 基础

0x001 Cookie 的使用

cookie 的使用很是简单,能够概括为 4 步:前端

  1. 前端发送 HTTP 请求到后端
  2. 后端生成要放到 cookie 中的信息并设置到响应中的 Set-Cookie 头部
  3. 前端取出响应中的 Set-Cookie 的内容,保存 cookie 信息到本地
  4. 前端继续访问后端页面,将保存到本地的 cookie 放到请求的 Cookie 头部

用图说明: java

Cookie 简单使用

用代码说明:node

const http = require('http');

http.createServer((req, res) => {
    res.writeHead(200, {'Set-Cookie': 'name=123'})
    res.write(`
        <script>document.write(document.cookie)</script>
    `)
    res.end()
}).listen(3000)
复制代码

这段代码的功能很简单:ios

  1. 建立一个 HTTP 服务器
  2. 为每个响应添加一个 Set-Cookie 头部,它的值是 name=123
  3. 响应的正文是一个 html,其中只有一个 script 标签,标签内的脚本会将 cookie 输出到当前页面

启动这个脚本,而后打开浏览器访问:git

$ node index.js
$ open http://localhost:3000
复制代码

就能够看到:github

  1. 页面上显示 name=123,正是咱们 Set-Cookie 的内容
  2. HTTPresponse 头部有 Set-Cookie: name=123

Cookie 简单使用

打开 Chrome 调试工具的 Application -> cookies -> localhost:3000,就能够看见咱们设置的 cookie 了:ajax

存储在本地的 cookies

此时再打开一个 tab,而后再访问这个页面(或者直接刷新一下就好,不过为了对比,能够再开一个 tab):axios

在 request 中携带 cookie

能够看到,此时,对比第一次访问的时候,在 request 中多了一个 Cookie,而 Cookie 的内容就是咱们 Set-Cookie 的内容(这不表示 Cookie === Set-Cookie,只是说明 Cookie 的内容来自 Set-Cookie,在后续会有详细说明 Set-CookieCookie 的转化)。后端

这就是最简单的 cookies 使用了。

0x002 cookies 的格式详解

cookies 的属性

Chromecookies 管理工具能够看出一条 cookie 的属性是有不少的:

  • Name
  • Value
  • Domain
  • Path
  • Expire / Max-Age
  • Size(忽略,应该只是前端统计 Cookie 键值对的长度,好比"name"和"123"的长度是 7,若是有大神知道请告知)
  • HttpOnly
  • Secure
  • SameSite

在接下来的章节,将会慢慢解释这些属性。

1. Expire

Expires 是用来为一个 cookie 设置过时时间,它是一个 UTC 格式的时间,过了这个时间之后,这个 cookie 就失效了,浏览器不会将这条失效的 cookie 包含在 Cookie 请求头部中,而且会删除这条过时的 cookie

代码:

const http = require('http');

http.createServer((req, res) => {
    const cookie =req.headers['cookie']
    const date = new Date();
    date.setMinutes(date.getMinutes()+1)
    if (!cookie || !cookie.length){
        res.writeHead(200, {'Set-Cookie': `name=123; expires=${date.toUTCString()}`})
    }
    res.write(`
        <script>document.write(document.cookie)</script>
    `)
    res.end()
}).listen(3000)

复制代码

上面的代码在给 response 添加的 Set-Cookie 中多了一个 expires属性,就是用来设置过时时间,这里将它设置为一分钟之后:

const date = new Date();
    date.setMinutes(date.getMinutes()+1)
复制代码

同时作了一个判断,若是 request 中有 cookie 了,就不添加 Set-Cookie 头部,不然永远不会过时。

打开浏览器(先删除以前的 cookies),访问localhost:3000

带 epires 的 cookie
能够看见第一次访问的时候

Date: Sun, 01 Dec 2019 15:24:47 GMT
Set-Cookie: name=123; expires=Sun, 01 Dec 2019 15:25:47 GMT
复制代码

Set-Cookie 的格式变了,多了一个 expires 属性,而且时间是 Date 属性的 1分钟之后。在这一分钟以内,若是咱们刷新页面,会发现 request 中有 Cookie,而 response 中没有 Set-Cookie

expires 内的 request

而在 1 分钟之后,则会从新生成一个 Set-Cookie,而且 request 中的 Cookie 没了:

从新生成的 cookie

expires 属性的的格式是:

expires-av = "Expires=" sane-cookie-date
    sane-cookie-date = <rfc1123-date, 定义在 [RFC2616], 章节 3.3.1>
复制代码

sane-cookie-date 是一个时间格式,大概以下:

Sun, 06 Nov 1994 08:49:37 GMT
复制代码

js 可使用以下得到:

$ new Date().toUTCString()
    "Sun, 01 Dec 2019 15:18:12 GMT"
复制代码

expires 的默认值是 session,也就是在当前浏览器关闭之后就会删除。

2. Max-Age

Max-Age 也是用来设置 cookie 的过时时间,可是它设置的是相对于资源获取的时间的相对秒数。好比,第一次访问的时候,responseDateSun, 01 Dec 2019 15:44:43 GMT,那么这条 cookie 的过时时间就是 Sun, 01 Dec 2019 15:45:43 GMT

第一次请求的时候,能够看到,cookie 的格式是:Set-Cookie: name=123; max-age=60,多了一个 max-age=60。

response 中带 max-age

若是在 60 s 内访问,则会看到 request 中包含了一个 Cookie: name=123,而 response 中没有Set-Cookie

max 有效期 request 中带 cookie

在 60 s 之后访问,则又建立了新的 cookie。

max-age 的默认值是 session,当前浏览器关闭之后就会被删除。

max-age 的格式是:

max-age-av = "Max-Age=" non-zero-digit *DIGIT
复制代码

3. Domain

Domain 限制在哪一个域名下会将这个 cookie 包含到 requestCookie 头部中。

好比,若是咱们设置一个 cookie 的 Domain 属性是 example.com,则用户代理会在发往 example.comwww.example.comwww.corp.example.com 的请求的 Cookie 中携带这个 cookie

上代码:

const http = require('http');

http.createServer((req, res) => {
    const cookie =req.headers['cookie']
    const date = new Date();
    date.setMinutes(date.getMinutes()+1)
    if (!cookie || !cookie.length){
        res.writeHead(200, {'Set-Cookie': 'name=123;domain=example.com'})
    }
    res.write(`
        <script>document.write(document.cookie)</script>
    `)
    res.end()
}).listen(3000)
复制代码

这里咱们添加了一个 domain=example.com。而后使用 SwitchHost 配置几个 host

127.0.0.1 example.com
127.0.0.1 www.example.com
127.0.0.1 www.corp.example.com
复制代码

访问 example.com:3000,第一次访问的时候,会返回

Set-Cookie: name=123;domain=example.com
复制代码

而后咱们刷新页面,就会发现 cookie 被携带到 request 中:

访问 www.example.com:3000,也存在这个 cookie

访问 www.corp.example.com:3000,也存在这个 cookie

可是若是访问 exampel2.com:3000,就不存在了,而且,由于 cookie 中的 domain 和当前的 domain 不一样,用户代理会拒绝存储这个 cookie,因此咱们看不到 cookie 的输出:

cookie 管理中,也不存在这条 cookie

domain 的默认值是当前的域名。

domain 的格式是:

domain-av = "Domain=" domain-value
复制代码

4. Path

相对于 Domain 限于 cookie 的域名,Path 限制 cookie 的路径,好比,若是咱们设置 cookie 的路径是 /a,则在 / 或者 /b 就没法使用

上代码:

const http = require('http');

http.createServer((req, res) => {
    const cookie =req.headers['cookie']
    const date = new Date();
    date.setMinutes(date.getMinutes()+1)
    if (!cookie || !cookie.length){
        res.writeHead(200, {'Set-Cookie': 'name=123;path=/a'})
    }
    res.write(`
        <script>document.write(document.cookie)</script>
    `)
    res.end()
}).listen(3000)

复制代码

这里添加了一个 path=/a,其余就没有变化了,访问 localhost:3000/a,获得一个 cookie

刷新页面,发送刚刚获得的 cookie

访问 /a/b,却能够:

访问 /b,不会发送 cookie,而且会生成一个新的 cookie

可是因为和当前路径不匹配,被拒绝:

访问 /,则和访问 /b 同样的结果:

所以,Path 能够限制一个 cookie 在哪一个路径及其子路径下可用,祖先路径和兄弟路径则不在容许之列,若是没有这个值,则是 /

Path 的默认值是 /,也就是对整个站是有效的。

path 的格式是:

path-av = "Path=" path-value
复制代码

5. Secure

Secure 用来指示一个 cookie 只在“安全”通道发送,这里的安全通道,通常指的就是 https。意思就是只有在 https 的时候才发送,http 不发送。

上代码:

let https = require("https");
let http = require("http");
let fs = require("fs");

const options = {
    key: fs.readFileSync('./server.key'),
    cert: fs.readFileSync('./server.pem')
};

const app = (req, res) => {
    const cookie =req.headers['cookie']
    const date = new Date();
    date.setMinutes(date.getMinutes()+1)
    if (!cookie || !cookie.length){
        res.writeHead(200, {'Set-Cookie': 'name=123;secure'})
    }
    res.write(`
        <script>document.write(document.cookie)</script>
    `)
    res.end()
}

https.createServer(options, app).listen(443);
http.createServer(app).listen(80);
复制代码

这里同时兼通 80、443 端口,提供 httphttps 服务,其中,https 服务所需的证书由 openssl 生成,这里免去不讲。访问 https://example.com,获得 cookie

刷新页面,发送 cookie

访问 http://example.com,获得 cookie

可是被拒绝:

secure 的默认值是 false,也就是 http/https都能访问。

secure 的格式是:

secure-av = "Secure"
复制代码

6. HttpOnly

Secure 限制 cookie 只能在安全通道中使用,别觉得 HttpOnly 就是只能在 Http 中使用的意思,HttpOnly 的意思是只能经过 http 才能访问,在前面的例子中,咱们一直经过document.cookie来在前端访问 cookie,可是若是指定了这个,就没法经过document.cookie来操做 cookie 了。

上代码:

const http = require('http');

http.createServer((req, res) => {
    const cookie =req.headers['cookie']
    const date = new Date();
    date.setMinutes(date.getMinutes()+1)
    if (!cookie || !cookie.length){
        res.writeHead(200, {'Set-Cookie': 'name=123;httpOnly'})
    }
    res.write(`
        <script>document.write(document.cookie)</script>
    `)
    res.end()
}).listen(3000)
复制代码

多了一个 httpOnly,访问 localhost:3000,获得 cookie

刷新,发送 cookie

使用 document.cookie 操做 cookie

> document.cookie = 'name=bar'
< "name=bar"
> document.cookie
< ""
复制代码

而后查看 cookie,能够看见 value 依旧是 123,而 HttpOnly 则被打勾,无情。

httpOnly 的默认值是 fasle,也就是能够用document.cookie来操做 cookie

httpOnly 的格式是

httponly-av = "HttpOnly"
复制代码

7. SameSite

注意,在说这个属性以前,有一点须要说明,那就是请求头中发送 cookie 并非只有在地址栏输入这个网址的时候才会发送这个 cookie,而是在整个浏览器的全部页面内,发送的全部 http 请求,好比 imgsrcscriptsrclinksrciframsrc,甚至 ajax 配置以后,只要知足 上面提到的条件,这个 http 请求都会包含这个请求地址的 cookie,就算你是在其余网站访问也是同样,这也就是后面讲的 csrf 有机可趁的缘由。

上代码:

const http = require('http');

http.createServer((req, res) => {
    if (req.headers.host.startsWith('example')) {
        res.writeHead(200, {'Set-Cookie': `host=${req.headers.host}`})
    }
    if (req.headers.host.startsWith('localhost')) {
        res.write(`
            <script src="http://example.com:3000/script.js"></script>
            <img src="http://example.com:3000/img.jpg"/>
            <iframe src="http://example.com:3000/"/>
        `)
    }
    res.end()
}).listen(3000)

复制代码

若是访问的地址是 example 开头,设置了一个 cookie,名字为 host,值为当前访问的地址。 若是访问的地址是localhost开头,就返回 3 个元素,他们的 src 指向 example.com:3000 三个不一样的资源(尽管不存在,可是足够了)。 访问 example.com:3000,获得 cookie:host=example.com:3000

刷新页面,正常发送 cookie

此时访问 localhost:3000,由于前面的代码,因此会返回 scriptimgiframe,而后用户代理会加载这三个资源:

打开这三个资源,能够看到,每一个资源都携带了一个 cookie

  • script.js

  • img.jpg

  • index.html

SameSite 就是用来限制这种状况:

Strict

SameSiteStrict 的时候,用户代理只会发送和网站 URL 彻底一致的 cookie,就算是子域名都不行。

上代码:

const http = require('http');

http.createServer((req, res) => {
    if (req.headers.host.startsWith('example')) {
        res.writeHead(200, {'Set-Cookie': `host=${req.headers.host};SameSite=Strict`})
    }
    if (req.headers.host.startsWith('localhost')) {
        res.write(`
            <a href="http://example.com:3000/index.html">http://example.com:3000/index.html</a>
            <script src="http://example.com:3000/script.js"></script>
            <img src="http://example.com:3000/img.jpg"/>
            <iframe src="http://example.com:3000/index.html"/>
        `)
    }
    res.end()
}).listen(3000)

复制代码
  • 访问 example.com:3000 得到 cookie:

  • 刷新正常发送 cookie

  • 访问 localhost:3000 任何指向 example.com:3000 的资源都不会发送 cookie

  • 包括从这个页面跳转过去:

  • 子域名

Lax

新版浏览器默认的 SameSiteLax,若是 SameSite 是 Lax,则将会为一些跨站子请求保留,如图片加载或者 frames 的调用,但只有当用户从外部站点导航到URL时才会发送。如 link 连接。(我没能测试出来,具体看阮一峰-Cookie 的 SameSite 属性MDN-HTTP cookies

上代码:

const http = require('http');

http.createServer((req, res) => {
    if (req.headers.host.startsWith('example')) {
        res.writeHead(200, {'Set-Cookie': `host=${req.headers.host};SameSite=Lax`})
    }
    if (req.headers.host.startsWith('localhost')) {
        res.write(`
            <a href="http://example.com:3000/index.html">http://example.com:3000/index.html</a>
            <form action="http://example.com:3000/form" method="post">
                <button>post</button>
            </form>
            <form action="http://example.com:3000/form" method="get">
                <button>get</button>
            </form>
            <script src="http://example.com:3000/script.js"></script>
            <img src="http://example.com:3000/img.jpg"/>
            <iframe src="http://example.com:3000/index.html"/>
        `)
    }
    res.end()
}).listen(3000)

复制代码

如下是个人测试结果

  • a 连接跳转:带 cookie
  • form[action=get]:带 cookie
  • form[action=post]:不带 cookie
  • iframe:不带 cookie
  • script:不带 cookie
  • img:不带 cookie

也就是简单导航跳转带,而资源加载不带。

None

在新版浏览器,若是要让 cookie 支持同站和跨站,须要明确指定 SameSite=None(我测试出来的结果是和没有添加这个属性是一致的)。

上代码:

const http = require('http');

http.createServer((req, res) => {
    if (req.headers.host.startsWith('example')) {
        res.writeHead(200, {'Set-Cookie': `host=${req.headers.host};SameSite=None`})
    }
    if (req.headers.host.startsWith('localhost')) {
        res.write(`
            <a href="http://example.com:3000/index.html">http://example.com:3000/index.html</a>
            <form action="http://example.com:3000/form" method="post">
                <button>post</button>
            </form>
             <form action="http://example.com:3000/form" method="get">
                <button>get</button>
            </form>
            <script src="http://example.com:3000/script.js"></script>
            <img src="http://example.com:3000/img.jpg"/>
            <iframe src="http://example.com:3000/index.html"/>
            
        `)
    }
    res.end()
}).listen(3000)
复制代码

8. Set-Cookie 的格式

其实 Set-Cookie 的格式是由 cookie 键值对和属性列表构成的,因此能够用以下表示(不用 ABNF):

Set-Cookie: cookie-pair ";" cookie-av ";" cookie-av....
复制代码

其中 cookie-pair 就是咱们上面写的相似 name=123 的格式:

cookie-pair = cookie-name "=" cookie-value
复制代码

后面能够跟一系列的属性,cookie-paircookie-av 之间使用 ; 分割,而 cookie-av 能够表示为:

cookie-av = expires-av / max-age-av / domain-av /
                path-av / secure-av / httponly-av /
                extension-av
复制代码

其中的expires-avmax-age-avdomain-avpath-avsecure-avhttponly-av 就对应前面一张图的各个属性,可是有点区别,Chrome 实现了 same-origin属性,可是在 RFC 6265 并无这个属性,能够看做是 extension-av

再作一次对应:

  • Name:cookie-name
  • Value:cookie-value
  • Domain:domain-av
  • Path:path-av
  • Expire / Max-Age :expires-av / max-age-av
  • Size(忽略,应该只是前端统计 Cookie 键值对的长度,好比"name"和"123"的长度是 7,若是有大神知道请告知)
  • HttpOnly:httponly-av
  • Secure:secure-av
  • SameSite:extension-av

能够看到其实 cookie 的格式并非和 Chrome 中的 cookies 一致,它将 cookie-paircookie-av 平铺开来。

0x003 Cookie 在各类状况下的反映

1. 每种状态码下 Set-Cookie 都会被处理吗?包括 301404500

根据 RFC 6265,用户代理可能忽略包含在 100 级别的状态码,可是必须处理其余响应中的 Set-Cookie(包括 400 级别和 500 级别)。

根据测试,除了 100101,还有一个 407(未知为啥)不会处理 Set-Cookie,甚至 999 都会处理

2. 如何发送多个 cookie 到 Set-Cookie,Cookie 又是如何处理的?

上代码:

const http = require('http');

http.createServer((req, res) => {
    res.setHeader('Set-Cookie', ['cookie1=1','cookie1=2','cookie2=2','cookie3=3'])
    res.end()
}).listen(3000)
复制代码

访问 localhost:3000,获得 cookie

能够看到,cookie 被放到多个 Set-Cookie 头部中,而在 RFC 7230 中其实规定,一个报文中不该该出现重复的头部,若是一个头部有多个值,应该使用 , 分隔,就像下面的Accept-EncodingAccept-Language。但问题是,, 能够做为 cookie-value 的合法值存在,若是降它做为分隔符会破坏 cookie 的解析😢,因此就这样咯。

刷新发送 Cookie,能够看到,被乖乖折叠了,而且重复的 cookie-namecookie 被按顺序覆盖了:

3. ajax 如何发送 Cookie,ajax 返回的 Set-Cookie 会被接受吗?

答案是会,这里以 axios 为例子,上代码:

const http = require('http');

http.createServer((req, res) => {
    res.writeHead(200, {'Set-Cookie': 'name=ajax'})
    res.write(`
        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
        <script >
        axios.get('http://example.com:3000/', {
            withCredentials: true
        })        
        </script>
    `)
    res.end()
}).listen(3000)
复制代码

访问 localhost:3000,获得 cookie

查看 cookie 存储,已经存上了:

查看 ajax 请求,已经发送了:

这里遵循同源策略,若是是同源,则默认会发送,若是是跨域,则须要添加withCredentials: true才行,XMLHttpRequestFetch 配置可能有所不一样。

暂时想不到了

0x004 CSRF

例子

  • 打开登录页面 localhost:3000/login,登录以后,后端返回一个 Set-Cookie: name=bob,后端读取 Cookie 判断用户是否登录,若是登录,就会显示帐户金额,和一个转帐表单

  • 转帐是经过 http://localhost:3000/transfer?name=lucy&money=1000 来转帐的,name 是转帐目标用户,money 是金额。这里为了方便,直接使用 GET

  • 从新开一个服务,伪装是另外一个站点,这个站点只有一个 img,该 imgsrc 指向了上面的转帐地址:
const http = require('http');
http.createServer((req, res) => {
    res.writeHead(200, {'Content-Type':'text/html'})
    res.write(`
        <img src="http://localhost:3000/transfer?name=bad&money=10000000">
    `)
    res.end()
}).listen(3001)
复制代码
  • 已经在 localhost:3000登录的 bob 访问 example.com:3001,就会发现,虽然只是一个图片指向了 localhost:3000,可是由于可以获取到 localhost:3000cookie,因此会被带过去,而这个地址根据 cookie 判断用户,并执行转帐操做:

  • 若是再 bob 回到本身的帐户页面,就会发现,他已然破产,被限制高消费:

防护

(本文章主要讲 cookiecsrf 原理和防护不是重点)

  • 检测 Referrer 头:可是一些浏览器能够禁用这个头部(我也没找到哪一个浏览器能够设置)
  • SameSite:还没有彻底普及
  • csrftoken:通常 csrf 发起攻击都是相似上面,搭建一个钓鱼网站,其实该钓鱼网站是没法访问源站的 cookie 的,发送 cookie 是浏览器自带的行为,因此只要生成一个随机的 token,在每一个表单发送的时候都带上就好了,由于钓鱼网站没法预测这个 token 的值。甚至能够将这个 token 直接存储在 cookie 中,在表单发送的时候取出来和表单一块儿发送。也能够后端生成表单的时候注入到一个 inputp[typehidden] 域。
  • 二次验证,好比验证码、支付密码等

0x005 资源

0x006 带货

最近发现一个好玩的库,做者是个大佬啊--基于 React 的现象级微场景编辑器

相关文章
相关标签/搜索