持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第24天,点击查看活动详情
什么是cookie?
- 存储在浏览器中的一段字符串,最大为4kb;
- 跨域不共享;
- 格式如k1=v1;k2=v2;因此可以存储结构化数据;
- 每次发送http请求,会将请求域的cookie发送给server端,比如在淘宝的页面请求百度的文件,那么就会发送百度的cookie,而不是淘宝的cookie;
- server端可以修改cookie并返回给浏览器;
- 浏览器中也可以通过js来修改cookie,但是是有限制的;
如果在淘宝页面访问百度,只会把百度的cookie带过去,不会把淘宝的cookie带过去。跨站请求伪造(CSRF) 的原理就是当你登录了一个目标网站,然后在浏览器中保留了目标网站的cookie信息,然后黑客诱惑你访问了钓鱼网站,然后在钓鱼网站向目标网站发送请求,这个请求就会带上目标网站的cookie信息,如果目标网站的服务端没有验证访问来源,那么这个请求就是伪造的了。
cookie如何做登录验证呢?
登录
客户端发送登录请求,服务端验证密码和用户名,如果都正确服务端就会设置cookie,key-value 为username=zhangsan
const getCookieExpires = () => {
const d = new Date()
d.setTime(d.getTime() + (24 * 60 * 60 * 1000))
return d.toGMTString()
}
res.setHeader('Set-Cookie', `username=zhangsan; path=/; httpOnly; expires=${getCookieExpires()}`)
浏览器获得响应头之后就会在浏览器中存储cookie信息,下次请求的时候就会带上存储的cookie,服务端获得请求头就开始解析cookie信息:
req.cookie = {}
const cookieStr = req.headers.cookie || ''
cookieStr.split(';').forEach(item => {
if (!item) {
return
}
const arr = item.split('=')
req.cookie[arr[0]] = arr[1]
})
console.log('req.headers', req.cookie)
解析完之后,就进行登录验证,验证很简单,就是看cookie中是否有username,如果有说明就已经登录过。这就是cookie是如何完成登录的过程。
什么是session?
上面cookie中有个致命的问题:暴露username等关键信息,很危险。解决方法是cookie中存储userid,服务端存储对应的username,那么服务端存储的username等信息就是session。
验证用户是否登录,就是拿着这个userid到session中去找对应的信息,如果找到了那么说明登录成功,如果没有那么就要去登录。
下面不适用第三方库,手写实现一个session功能:
- 在全局维护一个存储session的对象
SESSION_DATA,这样在整个进程中都能访问; - 如果请求头没有带userid,说明是一次访问,就让用户去登录;
- 如果请求头带有userid,那么就去
SESSION_DATA获取对应的用户信息,如果没有获取到让用户去登录; - 登录成功后把这个用户的信息写入到session对象
SESSION_DATA中;
// session数据 在全局维护一个session_data
const SESSION_DATA = {}
// 当用户发送请求的时候,解析session
// 解析session
let needSetCookie = false
let userId = req.cookie.userId
if (userId) {
if (!SESSION_DATA[userId]) {
SESSION_DATA[userId] = {}
}
} else {
userId = `${Date.now()}_${Math.random()}`
SESSION_DATA[userId] = {}
// 如果没有userid,说明是第一次登录,那么就去设置cookie
needSetCookie = true
}
req.session = SESSION_DATA[userId]
// 在登录接口,往session_data里面写数据
// 登录
if (method === 'POST' && req.path === '/api/user/login') {
const { username, password } = req.body
const result = login(username, password)
return result.then(data => {
if (data.username) {
// 设置session数据
req.session.username = data.username
req.session.realname = data.realname
return new SuccessModel('登录成功')
}
return new ErrorModel('登录失败')
})
}
从图中可以知道,session就是一个存储username等信息的变量。
但是会有两个问题:
- 既然是变量,就是在内存中的,但是对每个进程分配的内存大小是有限制的,当信息很多时,就会存不下。
操作系统会为每个进程分配一个内存空间,如上图比如说是从0x1000开始到0x8000结束,上面是栈内存,下面是堆内存,我们的session就放在堆内存中Heap,如果当用户很多的时候那么Heap就会越来越多,把整个内存都占满了,那么整个进程就崩溃了。
- 在线上是多进程部署的,进程和进程之间是无法访问的,所以B进程是无法访问到A进程里面的session的。
现在的计算机都是多核的,为了充分利用计算机的资源,一个应用往往会启用多个进程。如果每个进程都有session的话,进程之间的session是不能共享的。当你第一次进来,命中了第一个进程中的session,但是第二次进来命中了第二个进程中的session,但是这个session没有你的信息,就登录不了了。这是由于后面因为负载均衡导致的,它看哪个进程比较闲就分配哪个进程。
解决方案: redis
redis特点
- web server 最常用的缓存数据库,数据放在内存中;
- 相比于mysql访问速度快(内存和硬盘不是一个数量级);
- 但是成本更高,可存储的数据量更小(内存的硬伤);
现在web服务常见模型如图所示:
- 将web server、mysql、redis 拆分为两个单独的服务
- 双方都是独立的,都是可扩展的(都可以扩展为集群)
这个时候web server和redis就是两个服务了,session再多,也不关web server的事情了。就好比MySQL再大和web server 也没撒关系了,两者只不过是连接起来的。
现在web server 不管开几个进程,都是访问redis的数据,都是同一份数据。所以第二个问题也解决了。
session为何适用redis?
session 访问频繁,对性能要求极高
session访问频繁,因为我们在每个请求的时候都要验证是否登录,是一个访问的前置操作,所以就需要访问非常快,如果session都访问很慢后面的操作也就变慢,从而导致请求时间较长,因此对性能要求极高。
session 可不考虑断电数据丢失的问题(内存的硬伤)
session 如果断电数据丢失了,你再登录一次(再登录一次就再在session里面写一次,把用户相关的信息写进去,用户下次访问的时候会先去redis中获取数据,拿到数据后,再进行接下来的业务逻辑)就可以了,登录后请求其他接口就可以通过userid到session里面查,如果有就说明已经登录过了。
session 数据量不会太大(相比于mysql中存储的数据)
利用redis实现session
1. 安装redis并封装接口
const redis = require('redis')
const { REDIS_CONF } = require('../conf/db')
// 创建客户端
const redisClient = redis.createClient(REDIS_CONF.port, REDIS_CONF.host)
redisClient.on('error', err => {
console.error(err)
})
function set(key, val) {
if (typeof val === 'object') {
val = JSON.stringify(val)
}
redisClient.set(key, val, redis.print)
}
function get(key) {
return new Promise((resolve, reject) => {
redisClient.get(key, (err, val) => {
if (err) {
reject(err)
return
}
if (val == null) {
resolve(null)
return
}
try {
resolve(JSON.parse(val))
} catch (ex) {
// 说明val不是一个对象,那么就直接返回
resolve(ex)
}
})
})
}
module.exports = { set, get}
2. app.js
let needSetCookie = false
let userId = req.cookie.userId
// 如果cookie中没有userid,说明还没有set-cookie
if (!userId) {
// 接口里面要设置cookie
needSetCookie = true
// 放在cookie中的userid就是一个随机数字
userId = `${Date.now()}_${Math.random()}`
// 初始化redis中session
set(userId, {})
}
// 在redis中获取session,如果为空,初始化一个{}
req.sessionId = userId
get(req.sessionId).then(sessionData => {
if (sessionData == null) {
// 初始化redis中session
set(req.sessionId, {})
// 设置session
req.session = {}
} else {
// 设置session
req.session = sessionData
}
})
3. 登录成功往redis中写数据
const handleUserRouter = (req, res) => {
const method = req.method
// 登录
if (method === 'POST' && req.path === '/api/user/login') {
const { username, password } = req.body
const result = login(username, password)
return result.then(data => {
if (data.username) {
// 设置session
req.session.username = data.username
req.session.realname = data.realname
// 同步到redis中
set(req.sessionId, req.session)
return new SuccessModel()
} else {
return new ErrorModel('登录失败')
}
})
}
}
4. 校验sessionid
const loginCheck = req => {
if (!req.session.username) {
return Promise.resolve(new ErrorModel('尚未登录'))
}
}