开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情
你好,我是南一。这是我在准备面试八股文的笔记,如果有发现错误或者可完善的地方,还请指正,万分感谢🌹
Cookie、Session、token
1、从技术发展的角度看待Cookie、Session、token
HTTP协议诞生之初,作用是获取文档资源,经由浏览器进行展示,供用户浏览。连接之间毫无关联,造就其无状态的特性。
随着交互式Web的兴起,单纯的浏览网页已经满足不了人们的需求。复杂的用户操作出现,比如购物车。随着而来的问题是:购物车中的商品怎么与用户对应起来?所以需要一个机制来记录购物车与用户之间的关系,因此诞生了cookie。
1.1 Cookie
Cookie 是服务器发送到用户浏览器的一小块数据,浏览器会存储Cookie并在下一次向同一服务器发起请求时自动携带并发送到服务器上。
以购物车为例:用户往购物车里面添加商品,向服务端发送请求,服务端会将本次的商品id放进cookie里发送给浏览器,浏览器保存cookie在本地,下一次添加,浏览器又将原有的商品id放在cookie里面发给服务端,这样每个cookie储存着用户的购物车数据,就不会错乱或丢失。
局限性:这种方法虽说解决用户数据与用户的对应关系,但随着购物车内商品增多,cookie种携带的数据会越来越大,带来的性能开销也越来越大。
倘若将购物车数据储存在服务端,每次请求只发送一小段数据用于标识用户,然后操作对应用户的购物车数据即可,这样就能避免每次请求传送大量数据,增加不必要的负担。于是就诞生了Session
1.2 Session
用户登录,服务端进行身份验证,验证成功服务端会创建一个session,并为其分配唯一的sessionID,这个sessionID是与某个用户唯一绑定的,并设定会话结束时间,然后将此sessionID通过cookie传给浏览器。SessionID作为Cookie的值,结束时间作为Cookie的有效时间。浏览器在下一次向同一服务器发起请求时自动携带 SessionID 并发送到服务器上。服务端收到后,找出sessionID对应的session进行数据修改。(session可以认为是一个对象,存在服务端的文件,数据库或者是内存中)
以购物车为例,用户登录,服务端为用户生成一个session,并把sessionID通过cookie保存在浏览器。用户添加商品,将商品id传给服务端,同时cookie中带上sessionID,服务端通过sessionID找到对应session并修改数据。
session的痛点
看起来 session + cookie 的方式解决了问题,实际生产环境下,为保证高可用性(人话:用多台机器分担压力,防止机器挂掉服务全停的情况出现),服务器需要多台机器,通过负载均衡的方式决定请求发到哪台机器上。
假如客户端的请求发到A机器上,在机器A中生成session,并将sessionID返回给客户端。下一次客户端发了添加商品的请求到B或C上,服务端拿着sessionID找不到session,就得重新登录。解决此问题有以下几种方法:
1、session复制,将A中生成的session复制到BC中,这样不管到哪一台机器都可以找到session
这样做的弊端也很明显:
- 同一份session存储多次,数据冗余
- 机器多的话,大量复制session会造成性能浪费
2、session粘连,让相同用户发出的请求发到同一台机器上,比如第一次发到A机器,那在会话期间的请求就都发到A机器上就好了。
3、session共享,这也是目前大公司用的比较多的方法,将session保存在redis中,请求到来,机器再去redis取出session即可。为了高可用,还需要做redis集群。缺点是多了一次内部连接,消耗一点性能
服务端共享session可以达到用户身份定位的目的,但是实现一个身份校验还得搭建一个redis集群,实属大材小用。因此有了另一种方式实现身份校验:JWT
1.3 JSON Web Token
JSON Web Token(简称:JWT),用户输入账号密码登录,服务端接收到后,返回一个token,客户端可以存在localstorage,接下来的请求都要在请求头Authorization字段带上token,服务端接收到token就可以进行身份验证。
Token组成:token由三部分组成header,payload(载荷),signature(签名),中间用.隔开header指定签名算法,payload是一些非敏感的用户信息比如用户名用户id。header和payload会经过编码,变成base64格,所以是很容易被解码出来的。signature是服务端将base64编码用header里面指定的算法,加上密钥,进行签名运算,形成的签名字符串。
服务端收到token,将header和payload用密钥进行签名运算,得到的签名与signature比较,相同则token有效。
2、小试牛刀
这部分用简单的例子对这三个概念进行阐述,服务端采用express搭建。
const express = require('express')
const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.listen(80, () => {
console.log("express running at port 80...");
})
2.1 Cookie
Cookie相关属性:
maxAge:定义cookie的过期时间,一旦过期浏览器会自动删除cookie
secure:标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端。它永远不会使用不安全的 HTTP 发送(本地主机除外)。
httpOnly:JavaScript Document.cookie API 无法访问带有 HttpOnly 属性的 cookie
服务端通过响应报文的set-cookie字段传递cookie,浏览器在下一次向同一服务器发起请求时自动携带并发送到服务器上
app.post('/login', function (req, res) {
res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });
res.send({
code: 200,
status: 'success',
message: '登录成功'
})
})
app.get('/safe', function (req, res) {
res.send({
code: 200,
status: 'success'
})
})
在控制台可以看到存储的cookie
2.2 Session
这里用 express-session 验证sessionID
const sessions = require('express-session')
// 模拟数据库中的用户名和密码
const username_DB = 'Nanyi'
const password_DB = '123'
// 存储产生的session
let session_DB
app.use(sessions({
secret: '密钥',
name: 'sessionName',
cookie: { maxAge: 150000, expires: new Date('2024-1-10') },
resave: false,
saveUninitialized: false
}))
// 请求拦截,如果session里没有name字段,证明session失效,需要重新登录
app.use(function (req, res, next) {
if (req.originalUrl === '/login' || req.session.name !== undefined) {
next()
} else {
res.send({
code: 10101,
status: 'error',
message: '登录状态已过期,请重新登录'
})
res.end()
}
})
// 登录时,验证用户信息是否在数据库中,将session的信息保存到数据库,往session加入name字段
app.post('/login', function (req, res) {
if (req.body.name === username_DB && req.body.password === password_DB) {
session_DB = req.session
session_DB.name = req.body.name
res.send({
code: 200,
status: 'success',
message: '登录成功'
})
} else {
res.send({
code: 400,
status: 'error',
message: '用户名或密码错误'
})
}
})
app.get('/safe', function (req, res) {
console.log(req.session);
res.send({
code: 200,
status: 'success'
})
})
请求/safe服务端会打印出session如下:
2.3 JSON Web Token
这里关乎到设置请求头,前端写起来比较长,就用postman来代替前端发请求
const jwt = require('jsonwebtoken')
let JWT_SECRET = '密钥'
app.post('/login', function (req, res) {
let name = req.body.name
res.send({
code: 200,
status: 'success',
message: {
token: jwt.sign({ name }, JWT_SECRET, { expiresIn: '1d' })
}
})
})
app.get('/safe', function (req, res) {
const { authorization } = req.headers
const token = authorization.replace('Bearer ', '')
let user = jwt.verify(token, JWT_SECRET)
res.send({
code: 200,
status: 'success',
message: `欢迎登录${user.name}`
})
})
总结
- cookie是存在浏览器的一小块数据,从服务端发送而来。
- session是一种用户身份验证的机制,在服务端生成session存储用户状态,并生成对应的sessionID通过cookie发送给浏览器。缺点服务端需要存储大量session
- JWT提供了一种无需session存储就可以验证token正确性的方法。且可以在token中携带用户信息。