一个不大不小的问题
-
假设服务器有一个接口,通过请求这个接口,可以添加一个管理员,但是,不是任何人都有权力做这种操作的;
-
那么服务器如何知道请求接口的人是有权力的呢?答案是:只有登录过的管理员才能做这种操作;
-
可问题是,客户端和服务器的传输使用的是 http 协议,http 协议是无状态的,什么叫无状态,就是 服务器不知道这一次请求的人,跟之前登录请求成功的人是不是同一个人;
-
由于 http 协议的无状态,服务器 忘记 了之前的所有请求,它无法确定这一次请求的客户端,就是之前登录成功的那个客户端;于是,服务器想了一个办法,它按照下面的流程来认证客户端的身份:
- 客户端登录成功后,服务器会给客户端一个出入证;
- 后续客户端的每次请求,都必须要附带这个出入证;
-
服务器发扬了认证不认人的优良传统,就可以很轻松的识别身份了;但是,用户不可能只在一个网站登录,于是客户端会收到来自各个网站的出入证,因此,就要求客户端要有一个类似于卡包的东西,能够具备下面的功能:
- 能够存放多个出入证:这些出入证来自不同的网站,也可能是一个网站有多个出入证,分别用于出入不同的地方;
- 能够自动出示出入证:客户端在访问不同的网站时,能够自动的把对应的出入证附带请求发送出去;
- 正确的出示出入证:客户端不能将肯德基的出入证发送给麦当劳;
- 管理出入证的有效期:客户端要能够自动的发现那些已经过期的出入证,并把它从卡包内移除;
-
能够满足上面所有要求的,就是 cookie ,cookie 类似于一个卡包,专门用于存放各种出入证,并有着一套机制来自动管理这些证件,卡包内的每一张卡片,称之为 一个 cookie;
cookie 的组成
-
cookie 是浏览器中特有的一个概念,它就像浏览器的专属卡包,管理着各个网站的身份信息;每个 cookie 就相当于是属于某个网站的一个卡片,它记录了下面的信息:
属性名 描述 key 键 value 值 domain 域,表示这个 cookie 是属于哪个网站的,比如 ricepudding.cn path 路径,表示这个 cookie 是属于该网站的哪个基路径的,比如 /news(后续详细解释) secure 是否使用安全传输(后续详细解释) expire 过期时间,表示该 cookie 在什么时候过期 -
当浏览器向服务器发送一个请求的时候,它会瞄一眼自己的卡包,看看哪些卡片适合附带捎给服务器,如果一个 cookie 同时满足以下条件,则这个 cookie 会被附带到请求中:
- cookie 没有过期;
- cookie 中的域和这次请求的域是匹配的,比如 cookie 中的域是
ricepudding.cn
,则可以匹配的请求域是ricepudding.cn
、www.ricepudding.cn
、blogs.ricepudding.cn
等等; - cookie 中的 path 和这次请求的 path 是匹配的,比如 cookie 中的 path 是
/news
,则可以匹配的请求路径可以是/news
、/news/detail
、/news/a/b/c
等等; - 验证 cookie 的安全传输,如果 cookie 的 secure 属性是 true ,则请求协议必须是 https ,否则不会发送该 cookie
-
如果一个 cookie 满足了上述的所有条件,则浏览器会把它自动加入到这次请求中:具体加入的方式是,浏览器会将符合条件的 cookie,自动放置到请求头中,例如,当我在浏览器中访问百度的时候,它在请求头中附带了下面的 cookie:
-
看到打马赛克的地方了吗?这部分就是通过请求头
cookie
发送到服务器的,它的格式是键=值; 键=值; 键=值; ...
,每一个键值对就是一个符合条件的 cookie; -
cookie 中包含了重要的身份信息,永远不要把你的 cookie 泄露给别人!!! 否则,他人就拿到了你的证件,有了证件,就具备了为所欲为的可能性;
如何设置 cookie
-
由于 cookie 是保存在浏览器端的,同时很多证件又是服务器颁发的:
-
所以 cookie 的设置有两种模式:
- 服务器设置:这种模式是非常普遍的,当服务器决定给客户端颁发一个证件时,它会在响应的消息中包含 cookie ,浏览器会自动的把 cookie 保存到卡包中;
- 客户端设置:这种模式少见一些,不过也有可能会发生,比如用户关闭了某个广告,并选择了「以后不要再弹出」,此时就可以把这种小信息直接通过浏览器的 JS 代码保存到 cookie 中;后续请求服务器时,服务器会看到客户端不想要再次弹出广告的 cookie ,于是就不会再发送广告过来了;
服务端设置 cookie
-
服务器可以通过设置响应头,来告诉浏览器应该如何设置 cookie ,响应头按照下面的格式设置:
set-cookie: cookie1 set-cookie: cookie2 set-cookie: cookie3 ...
-
通过这种模式,就可以在一次响应中设置多个 cookie 了,具体设置多少个 cookie ,设置什么 cookie ,根据需要自行处理;其中,每个 cookie 的格式如下:每个 cookie 除了键值对是必须要设置的,其他的属性都是可选的,并且顺序不限;
键=值; path=?; domain=?; expire=?; max-age=?; secure; httponly
-
当这样的响应头到达客户端后,浏览器会自动的将 cookie 保存到卡包中,如果卡包中已经存在一模一样的卡片(其他 path、domain 相同),则会自动的覆盖之前的设置;
-
下面依次说明每个属性值:
- path:设置 cookie 的路径;如果不设置,浏览器会将其自动设置为当前请求的路径;比如,浏览器请求的地址是
/login
,服务器响应了一个set-cookie: a=1
,浏览器会将该 cookie 的 path 设置为请求的路径/login
; - domain:设置 cookie 的域;如果不设置,浏览器会自动将其设置为当前的请求域,比如,浏览器请求的地址是
http://www.ricepudding.cn
,服务器响应了一个set-cookie: a=1
,浏览器会将该 cookie 的 domain 设置为请求的域http://www.ricepudding.cn
; - expire:设置 cookie 的过期时间;这里必须是一个有效的 GMT 时间,即格林威治标准时间字符串,比如
Fri, 17 Apr 2020 09:35:59 GMT
,表示格林威治时间的2020-04-17 09:35:59
,即北京时间的2020-04-17 17:35:59
;当客户端的时间达到这个时间点后,会自动销毁该 cookie; - max-age:设置 cookie 的相对有效期;expire 和 max-age 通常仅设置一个即可;比如设置
max-age
为 1000 ,浏览器在添加cookie 时,会自动设置它的 expire 为当前时间加上 1000 秒,作为过期时间;如果不设置 expire 又没有设置 max-age ,则表示会话结束后过期;对于大部分浏览器而言,关闭所有浏览器窗口意味着会话结束; - secure:设置 cookie 是否是安全连接;如果设置了该值,则表示该 cookie 后续只能随着 https 请求发送;如果不设置,则表示该 cookie 会随着所有请求发送;
- httponly:设置 cookie 是否仅能用于传输;如果设置了该值,表示该 cookie 仅能用于传输,而不允许在客户端通过 JS 获取,这对防止跨站脚本攻击(XSS)会很有用;
- path:设置 cookie 的路径;如果不设置,浏览器会将其自动设置为当前请求的路径;比如,浏览器请求的地址是
-
下面来一个例子,客户端通过 post 请求服务器
http://ricepudding.cn/login
,并在消息体中给予了账号和密码,服务器验证登录成功后,在响应头中加入了以下内容:set-cookie: token=123456; path=/; max-age=3600; httponly
:- 当该响应到达浏览器后,浏览器会创建下面的 cookie:
key: token value: 123456 domain: ricepudding.cn path: / expire: 2020-04-17 18:55:00 # 假设当前时间是 2020-04-17 17:55:00 secure: false # 任何请求都可以附带这个cookie,只要满足其他要求 httponly: true # 不允许 JS 获取该 cookie
- 于是,随着浏览器后续对服务器的请求,只要满足要求,这个 cookie 就会被附带到请求头中传给服务器:
cookie: token=123456; 其他 cookie...
-
删除浏览器的 cookie:只需要让服务器响应一个同样的域、同样的路径、同样的 key ,只是时间过期的 cookie 即可,所以删除 cookie 其实就是修改 cookie:浏览器按照要求修改了 cookie 后,会发现 cookie 已经过期,于是自然就会删除了;
set-cookie: token=; domain=ricepudding.cn; path=/; max-age=-1
无论是修改还是删除,都要注意 cookie 的域和路径,因为完全可能存在域或路径不同,但 key 相同的 cookie
因此无法仅通过 key 确定是哪一个 cookie
客户端设置 cookie
-
既然 cookie 是存放在浏览器端的,所以浏览器向 JS 公开了接口,让其可以设置 cookie;
document.cookie = "键=值; path=?; domain=?; expire=?; max-age=?; secure";
-
可以看出,在客户端设置 cookie 和服务器设置 cookie 的格式一样,只是有下面的不同:
- 没有 httponly ,因为 httponly 本来就是为了限制在客户端访问的,既然你是在客户端配置,自然失去了限制的意义;
- path 的默认值,在服务器端设置 cookie 时,如果没有写 path 使用的是请求的 path;而在客户端设置 cookie 时,也许根本没有请求发生,因此 path 在客户端设置时的默认值是当前网页的 path;
- domain 的默认值,和 path 同理,客户端设置时的默认值是当前网页的 domain;
- 其他:一样;
- 删除 cookie:和服务器也一样,修改 cookie 的过期时间即可;
cookie 存在的问题
-
cookie 会作为 http 的请求报文进行传递,所以要注意 cookie 字段的大小,一般小于 4kb ,如果 cookie 存储的字段过大,浪费流量,服务端也会耗费性能解析 cookie;
解决方案:静态资源的请求,无必要可以不携带 cookie ,合理设置 cookie ,不必要字段不放 cookie 里面;
-
cookie 保存在客户端,存在篡改或劫持的风险,故 cookie 不能存储重要信息,一般只保存凭证,服务端会根据 cookie 从数据库或 session 中查找具体信息(当给浏览器设置 cookie 时,可以增加签名,根据数据内容创建一个唯一的签名,读取 cookie 的时候验证签名,判断 cookie 是否被篡改);
const http = require('http'); const querystring = require('querystring'); const crypto = require('crypto'); const secret = 'adsadbAvhnrgreu657i76lu;oi3r'; // 自定义的秘钥 // 签名 const toSign = (value) => { // 当给浏览器设置 cookie 时,可利用 加盐算法 增加签名,根据数据内容创建一个唯一的签名 // 相同的秘钥签名的结果是相同,并且不能反解 let str = crypto.createHmac('sha256', secret).update(value).digest('base64'); // /、=、+ 浏览器不能识别,直接去掉,只是做签名没必要改成其他字符(和后端统一规则即可) return str.replace(/\/|\=|\+/, ''); } http.createServer((req, res) => { req.getCookie = function (key, options = {}) { let cookieObj = querystring.parse(req.headers.cookie, '; ', '='); if (options.signed) { // 是否验证签名 let [value, signCode] = (cookieObj[key] || '').split('.'); let newSign = toSign(value); if (newSign === signCode) { return value; // 签名一致,说明这次的内容是没有被改过的 } else { return undefined; // 签名被篡改了,不能使用了 } } return cookieObj[key]; } res.setCookie = function (key, value, options = {}) { let opts = []; if (options.domain) opts.push(`domain=${options.domain}`); if (options.path) opts.push(`path=${options.path}`); if (options.maxAge) opts.push(`max-age=${options.maxAge}`); if (options.httpOnly) opts.push(`httpOnly=${options.httpOnly}`); if (options.signed) value = value + '.' + toSign(value); res.setHeader('Set-Cookie', [`${key}=${value}; ${opts.join('; ')}`]); } if (req.url == '/read') { // 读取 cookie res.end(req.getCookie('name', { signed: true }) || 'empty'); } else if (req.url == '/write') { // 设置 cookie res.setCookie('name', 'zhangsan', { httpOnly: true, maxAge: 200, signed: true }); res.setCookie('age', '20'); res.end('write Ok'); } else { res.end('Not Found'); } }).listen(3000);
登录场景的使用
登录请求
- 浏览器发送请求到服务器,附带账号密码;
- 服务器验证账号密码是否正确,如果不正确,响应错误,如果正确,在响应头中设置 cookie 附带登录认证信息(至于登录认证信息是设么样的,如何设计,要考虑哪些问题,就是另一个话题了,可以百度 jwt)
- 客户端收到 cookie 浏览器自动记录下来;
后续请求
浏览器发送请求到服务器,希望添加一个管理员,并将 cookie 自动附带到请求中;
服务器先获取 cookie ,验证 cookie 中的信息是否正确,如果不正确,不予以操作,如果正确,完成正常的业务流程;
ES6+ Class
上一篇