session 介绍
- 上一篇笔记中使用cookie存储username是很危险的,会暴露,所有明文的个人信息都最好不要放到cookie中。
- 解决方法:cookie中存储userid,server端对应username/个人信息。其实这就是session。
- server端放置信息不会被前端获取到。
- 且server端空间大,可存储更多信息。
- session,即server端存储用户信息。
session 的使用
app.js解析session
- 定义一个全局变量
SESSION_DATA
(在整个项目中都有效):- 接下来项目所有路由中用到的req.session都是保存在该全局变量中的。
SESSION_DATA
对象中保存的属性名为userId,属性值为对应的session对象,session对象中存储username和realname。- 每次进入项目都要从该变量中取出和userId对应的属性值作为
req.session
来使用。
- 在解析cookie后解析session:
req.cookie.userid
作为**SESSION_DATA
的属性名userId
**存储在全局变量中- 没有
userId
时随机生成一个 - 没有
SESSION_DATA[userId]
属性值时就使用{}
空对象作为属性值 - 有
SESSION_DATA[userId]
属性值时就将其赋值给req.session
,则接下来session变化时SESSION_DATA[userId]
也会发生变化(即使用req.session
作为SESSION_DATA[userId]
)
- 将router-user.js中获取cookie的过期时间的函数剪切过来:
- 处理user路由,设置cookie:
- 将userId作为cookie传给客户端,客户端下次发送请求时就会使用
cookie.userid
。
- 将userId作为cookie传给客户端,客户端下次发送请求时就会使用
router-user.js处理路由
- 登录路由,设置session:
- 测试登录路由,获取session并返回已显示在页面上:
测试
注意:永远都从app.js开始!
- 清空cookie后进入测试登录路由:
- 进入app.js,解析session时,没有
req.cookie.userid
,所以把needSetCookie
设置为true,并使用${Date.now()}_${Math.random()}
随机生成了userId放入SESSION_DATA[userId]
再放到了req.session
里。 - 处理user路由时(router-user.js),分配到登录验证,没有
req.cookie.username
所以返回“尚未登录”给app.js,由于needSetCookie
此时为true,所以将userId(即${Date.now()}_${Math.random()}
)设置为Set-Cookie
中的userid
用以返回。 - 浏览器拿到返回的
Set-Cookie
以后就会用来设置自己的cookie并用于下一次发送请求。
- 进入app.js,解析session时,没有
- 此时进入登录路由:
- 进入app.js,解析session时,直接执行
req.session = SESSION_DATA[userId];
。 - 然后处理路由(router-user.js),判断了用户名和密码正确后,将username, password设置到req.session上:
- 进入app.js,解析session时,直接执行
- 再次进入登录测试路由:
req.session
中有username
,则返回req.session
给到app.js显示在页面上,登录成功。
思路
打印SESSION_DATA
和req.session
可看到:
- 第一次进入测试路由时,没有
userId
和SESSION_DATA[userId]
,故生成userId放入SESSION_DATA对象中作为属性并设置属性值为空对象,最后将SESSION_DATA[userId]
(即空对象)赋值给req.session
。- 此时**
req.session
为{}
** - 此时**
SESSION_DATA
为{"时间_随机数":{}}
**
- 此时**
- 第一次进入登录路由时,此时请求中已经有之前生成的userId,故将
SESSION_DATA[userId]
(即空对象)赋值给req.session
。(他们指向一个地址,修改session则SESSION_DATA[userId]
也会改变)通过路由(router-user.js)将username, realname 获取并设置到req.session.username
、req.session.realname
上。- 此时**
req.session
为{username:输入的username,realname:输入的realname}
** - 此时**
SESSION_DATA
为{"时间_随机数":{username:输入的username,realname:输入的realname}}
**
- 此时**
- 第二次进入测试路由时,解析session时将
SESSION_DATA[userId]
(即{username:输入的username,realname:输入的realname}
)赋值给req.session
,然后在路由(router-user.js)中因为有req.session.username
,所以返回req.session
显示在页面上。
从 session 到 redis
目前使用session会造成的问题
- 目前session直接是js变量,放在nodejs进程内存中。
- 计算机基础中的“进程内存模型”,可以看到,操作系统会给每个进程分配这样一个有限的内存块,session作为对象时存储在堆中的,当他越来越多以至于接触到栈或者导致内存崩了就完了:
- 问题1:进程内存有限,访问量过大,内存暴增怎么办?
- nodejs作为一个进程,他的内存是有限的,所以session作为进程中的一个变量,他的最大可用内存也是有限的:
- 问题2:正式线上运行是多进程,进程之间内存无法共享。
- 以3个进程为例:
- 操作系统对一个进程分配的内存是有限的,但服务器内存很大,所以如果一次只执行一个进程任务就很浪费。
- 现在都是多核处理器,双核/四核/六核,多少核就能并行处理多少个进程的任务执行,如果一次只执行一个进程任务就很浪费。
- 但多个进程之间内存无法共享:
- 比如,你同时开启了qq浏览器和百度,如果qq的进程想要去访问百度进程的内存中保存的变量,那安全隐患就大了。所以多个进程之间内存是无法共享的。
- 以3个进程为例:
redis
- redis是web server最常用的缓存数据库,数据存放在内存中
- 相比于 mysql,访问速度快
- mysql是硬盘数据库,redis是内存数据库。
- 内存和硬盘的速度不是一个数量级的,mysql无论做什么算法优化速度都没内存中的读取速度快。
- 但是成本更高,可存储的数据量更小(内存的硬伤)
解决方案:redis保存session
- 将web server和redis拆分为两个单独的服务
- 这样一来,session就不是存储在进程中的变量,那么访问量过大时也不会造成nodejs的进程内存暴增了。(因为此时session是存储在redis里的,独立于web server)
- 多个进程之间也可以同时访问redis,解决了多进程之间内存无法共享的问题。
- 双方都是独立的,都是可扩展的(例如都扩展成集群)
- 所以如果访问量暴增也不怕,可以通过扩展成集群等方式扩展内存。
- 包括mysql,也是一个单独的服务,也可扩展
- 但是存放在redis中的数据会断电丢失(这是内存数据库的硬伤)
为何使用redis而不是mysql
- session访问频繁,对性能要求极高
- session作为访问的入口,每次访问网页都要经过session。mysql是只有更新的时候才执行,是比较滞后的。
- session可不考虑断电丢失数据的问题(内存的硬伤)
- session丢了没关系,就相当于没登录,再登录即可。
- 其实session也可以断电不丢失(需要额外配置,此处不考虑)
- session数据量不会太大(相比于mysql中存储的数据)
- 所以没必要放到mysql中进行存储
为何网站数据不适合用redis
- 操作频率不是太高(相比于session操作),所以放在mysql中更合适
- 断电不能丢失,必须保留,如果使用redis还得另外配置来恢复数据,比较麻烦,所以mysql更合适
- 数据量太大,放在redis中则内存成本太高
redis 介绍
安装redis
- 可参考菜鸟教程
- 做到这一步即可:
使用redis
- redis安装目录下打开cmd,运行
redis-server.exe redis.windows.conf
启动redis - redis目录下另启一个 cmd 窗口,原来的不要关闭(否则无法访问服务端),运行
redis-cli.exe -h 127.0.0.1 -p 6379
(接下来的操作都在这个cmd中)或者redis-cli -p 6379
- 设置:
set 关键字 值
- 获取:
get 关键字
- 看到目前设置的所有key:
keys *
- 删除:
del 关键字
- 清空当前redis数据库缓存:
flushdb
- 清空整个redis缓存:
flushal
- 清空当前redis数据库缓存:
- 设置:
nodejs链接redis的demo
- 启动redis:redis安装目录下打开cmd,运行
redis-server.exe redis.windows.conf
- 新建redis-test文件夹-执行
npm init -y
进行初始化 - 项目中安装redis:
npm i redis --save --registry=https://registry.npm.taobao.org
- 可以在package.json中看到,安装成功:
- 新建index.js:
- 引入redis
- 创建客户端
- 测试
- set()的参数3
redis.print
可以在设置成功后打印出Reploy OK
- 注意:get()获取数据,参数2是异步的
- set()的参数3
- 到cmd中看看结果:
nodejs连接redis 封装工具函数
- 进入blog-1项目中,安装redis:
npm i redis --save --registry=https://registry.npm.taobao.org
- 在src-conf-db.js中,配置redis的连接信息
- 新建变量
REDIS_CONF
,port是redis端口号,host是连接的主机ip地址,记得输出
- 新建变量
- db文件夹下新建redis.js,使用conf中的配置信息 连接redis:
- 注意:
- set()的参数1、2都是字符串格式的,所以使用set()时需要判断参数是否为引用类型,如果参数2是引用类型就要先使用
JSON.stringify()
将其转换为字符串类型的数组/对象,否则会自动使用toString()
方法进行转换,那结果会很麻烦。 - get()是异步的,所以需要使用Promise对象进行封装。(get请求需要node向redis请求后获取一个redis返回的值,这是IO操作,而在node中IO操作是异步的。)由于之前我们将对象转为JSON字符串了,所以输出数据时要先尝试将JSON字符串转回为JSON对象,而非JSON字符串则直接输出即可。同时要注意获取的值不存在时就返回空对象。
- 一样的,“结束”在这不能用,因为我们只连接一次但要使用多次,如果结束就无法使用多次。
session存入redis
- 目前我们把session存在js中作为变量,所以每次刷新页面时数据就会丢失,但我们把session存入redis就不会有这个问题。
- cookie是一个对象,里面存储的是userId、path等;session也是一个对象,其中存储的是username和password等。
- 流程总结:
- 用户登录网页,在登录路由中将输入的username等数据放入session中,并使用redis的set方法将session数据同步存储到redis中
- 接下来每次访问网页时,首先执行的app.js就会根据用户请求中的cookie的userId,使用redis解析(读取)session得到username等数据,以此判断用户是否登录。
例子
- router-user.js中,
- 引入往redis中设置session的方法:
- 在登录路由中将输入的username等数据放入session中,并使用redis的set方法将session数据同步存储到redis中:
- app.js中,
- 引入使用redis的方法:
- 根据用户请求中的cookie的userId,使用redis解析(读取)session得到username等数据,以此判断用户是否登录。处理blog路由:
- 测试:
完成server端登录的代码
- router-blog.js中,创建一个 统一的登录验证函数,调用该函数时,只要传入req,有返回数据就说明用户未登录:
- 将这个验证函数放在各个需要登录验证的博客路由中,这样只要用户未登录就直接return,结束函数,不会继续执行:
- 从session中提取数据:
- 修改router-user.js,将登录路由的get改回post,注释掉测试路由: