博客项目登录(session、redis)

session 介绍

  • 上一篇笔记中使用cookie存储username是很危险的,会暴露,所有明文的个人信息都最好不要放到cookie中。
  • 解决方法:cookie中存储userid,server端对应username/个人信息。其实这就是session。
    • server端放置信息不会被前端获取到。
    • 且server端空间大,可存储更多信息。
  • session,即server端存储用户信息。
  • 图示

session 的使用

app.js解析session

  1. 定义一个全局变量SESSION_DATA(在整个项目中都有效):全局变量
    • 接下来项目所有路由中用到的req.session都是保存在该全局变量中的。
    • SESSION_DATA对象中保存的属性名为userId,属性值为对应的session对象,session对象中存储username和realname
    • 每次进入项目都要从该变量中取出和userId对应的属性值作为req.session来使用。
  2. 在解析cookie后解析session解析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]
  3. 将router-user.js中获取cookie的过期时间的函数剪切过来:获取cookie的过期时间的函数
  4. 处理user路由,设置cookie处理user路由,设置cookie
    • 将userId作为cookie传给客户端,客户端下次发送请求时就会使用cookie.userid

router-user.js处理路由

  1. 登录路由,设置session:设置session
  2. 测试登录路由,获取session并返回已显示在页面上:测试登录路由

测试

注意:永远都从app.js开始!

  1. 清空cookie后进入测试登录路由测试路由
    1. 进入app.js,解析session时,没有req.cookie.userid,所以needSetCookie设置为true,并使用${Date.now()}_${Math.random()}随机生成了userId放入SESSION_DATA[userId]再放到了req.session
    2. 处理user路由时(router-user.js),分配到登录验证,没有req.cookie.username所以返回“尚未登录”给app.js,由于needSetCookie此时为true,所以将userId(即${Date.now()}_${Math.random()})设置为 Set-Cookie中的userid用以返回。
    3. 浏览器拿到返回的Set-Cookie以后就会用来设置自己的cookie并用于下一次发送请求。
  2. 此时进入登录路由登录成功
    1. 进入app.js,解析session时,直接执行req.session = SESSION_DATA[userId];
    2. 然后处理路由(router-user.js),判断了用户名和密码正确后,将username, password设置到req.session上打印出的req.session
  3. 再次进入登录测试路由:登录测试路由
    1. req.session中有username,则返回req.session给到app.js显示在页面上,登录成功。

思路

打印SESSION_DATAreq.session可看到:打印情况

  1. 第一次进入测试路由时,没有userIdSESSION_DATA[userId],故生成userId放入SESSION_DATA对象中作为属性并设置属性值为空对象,最后将 SESSION_DATA[userId](即空对象)赋值给req.session
    • 此时**req.session{}**
    • 此时**SESSION_DATA{"时间_随机数":{}}**
  2. 第一次进入登录路由时,此时请求中已经有之前生成的userId,故将 SESSION_DATA[userId](即空对象)赋值给req.session。(他们指向一个地址,修改session则SESSION_DATA[userId]也会改变)通过路由(router-user.js)将username, realname 获取并设置到req.session.usernamereq.session.realname上。
    • 此时**req.session{username:输入的username,realname:输入的realname}**
    • 此时**SESSION_DATA{"时间_随机数":{username:输入的username,realname:输入的realname}}**
  3. 第二次进入测试路由时,解析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作为进程中的一个变量,他的最大可用内存也是有限的nodejs作为一个进程
  • 问题2:正式线上运行是多进程,进程之间内存无法共享
    • 以3个进程为例:以3个进程为例
      • 操作系统对一个进程分配的内存是有限的,但服务器内存很大,所以如果一次只执行一个进程任务就很浪费。
      • 现在都是多核处理器,双核/四核/六核,多少核就能并行处理多少个进程的任务执行,如果一次只执行一个进程任务就很浪费。
      • 多个进程之间内存无法共享
        • 比如,你同时开启了qq浏览器和百度,如果qq的进程想要去访问百度进程的内存中保存的变量,那安全隐患就大了。所以多个进程之间内存是无法共享的。

redis

  • redis是web server最常用的缓存数据库,数据存放在内存
  • 相比于 mysql,访问速度快
    • mysql是硬盘数据库,redis是内存数据库。
    • 内存和硬盘的速度不是一个数量级的,mysql无论做什么算法优化速度都没内存中的读取速度快。
  • 但是成本更高,可存储的数据量更小(内存的硬伤)
    image.png

解决方案: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

  1. 可参考菜鸟教程
  2. 做到这一步即可:cmd

使用redis

  1. redis安装目录下打开cmd,运行redis-server.exe redis.windows.conf启动redis
  2. redis目录下另启一个 cmd 窗口,原来的不要关闭(否则无法访问服务端),运行redis-cli.exe -h 127.0.0.1 -p 6379(接下来的操作都在这个cmd中)或者redis-cli -p 6379
  3. 例子
    1. 设置set 关键字 值
    2. 获取get 关键字
    3. 看到目前设置的所有key:keys *
    4. 删除del 关键字
      • 清空当前redis数据库缓存:flushdb
      • 清空整个redis缓存:flushal

nodejs链接redis的demo

  1. 启动redisredis安装目录下打开cmd,运行redis-server.exe redis.windows.conf启动redis
  2. 新建redis-test文件夹-执行npm init -y进行初始化
  3. 项目中安装redisnpm i redis --save --registry=https://registry.npm.taobao.org
    • 可以在package.json中看到,安装成功:package.json
  4. 新建index.jsindex.js
    1. 引入redis
    2. 创建客户端
    3. 测试
      • set()的参数3redis.print可以在设置成功后打印出Reploy OK
      • 注意:get()获取数据,参数2是异步的
    4. 到cmd中看看结果:cmd

nodejs连接redis 封装工具函数

  1. 进入blog-1项目中,安装redisnpm i redis --save --registry=https://registry.npm.taobao.org
  2. 在src-conf-db.js中,配置redis的连接信息src-conf-db.js
    • 新建变量REDIS_CONF,port是redis端口号,host是连接的主机ip地址,记得输出
  3. db文件夹下新建redis.js,使用conf中的配置信息 连接redisredis.js
    • 注意:
    1. set()的参数1、2都是字符串格式的,所以使用set()时需要判断参数是否为引用类型,如果参数2是引用类型就要先使用JSON.stringify()将其转换为字符串类型的数组/对象,否则会自动使用toString()方法进行转换,那结果会很麻烦。
    2. get()是异步的,所以需要使用Promise对象进行封装。(get请求需要node向redis请求后获取一个redis返回的值,这是IO操作,而在node中IO操作是异步的。)由于之前我们将对象转为JSON字符串了,所以输出数据时要先尝试将JSON字符串转回为JSON对象,而非JSON字符串则直接输出即可。同时要注意获取的值不存在时就返回空对象。
    3. 一样的,“结束”在这不能用,因为我们只连接一次但要使用多次,如果结束就无法使用多次。

session存入redis

  • 目前我们把session存在js中作为变量,所以每次刷新页面时数据就会丢失,但我们把session存入redis就不会有这个问题。
  • cookie是一个对象,里面存储的是userId、path等;session也是一个对象,其中存储的是username和password等。示意图
  • 流程总结
    1. 用户登录网页,在登录路由中将输入的username等数据放入session中,并使用redis的set方法将session数据同步存储到redis中
    2. 接下来每次访问网页时,首先执行的app.js就会根据用户请求中的cookie的userId,使用redis解析(读取)session得到username等数据,以此判断用户是否登录。

例子

  1. router-user.js中
    • 引入往redis中设置session的方法:router-user.js
    • 在登录路由中将输入的username等数据放入session中,并使用redis的set方法将session数据同步存储到redis中router-user.js
  2. app.js中
    • 引入使用redis的方法:app.js
    • 根据用户请求中的cookie的userId,使用redis解析(读取)session得到username等数据,以此判断用户是否登录。处理blog路由:app.js
  3. 测试postmancmd

完成server端登录的代码

  1. router-blog.js中,创建一个 统一的登录验证函数,调用该函数时,只要传入req,有返回数据就说明用户未登录:统一的登录验证函数
  2. 将这个验证函数放在各个需要登录验证的博客路由中,这样只要用户未登录就直接return,结束函数,不会继续执行
    • router-blog.js
    • router-blog.js
    • router-blog.js
  3. 从session中提取数据:router-blog.jsrouter-blog.js
  4. 修改router-user.js,将登录路由的get改回post,注释掉测试路由:router-user.js