第四天:使用 session cookie redis完善登录验证及登录路由

本文详细介绍了如何在Node.js中实现登录功能,包括使用cookie、session以及优化过程。首先,通过简单的登录检查,然后引入cookie进行登录状态的初步验证。接着,为了避免cookie的安全问题,使用session存储用户信息,同时探讨了使用redis来存储session以提高性能和解决多进程问题。最后,文章提供了实际的代码示例和测试步骤,帮助读者理解登录验证的完整流程。
摘要由CSDN通过智能技术生成

上一篇

一、登录功能(跟上一篇博客模块对接数据库方法类似)
//controller/user.js

const loginCheck = (username,password) => {
  //先用假数据
  // if (username === 'zhangsan' && password === '123') {
  //     return true
  // }
  // return false
  let sql = `select username, realname from users where username='${username}' and password='${password}'`
  return exec(sql).then(rows => {
    return rows[0] || {}
  })
}
//router/user.js

   //登录
    if (method === "POST" && req.path === "/api/user/login") {
        const postData = req.body
        const {username,password} = postData
        const result =loginCheck(username,password)

        return result.then(data => {
            if (data.username) {
                return new SuccessModel()
            }
            return new ErrorModel('登录失败')
        })
    }
//app.js

  //处理user路由
      const userResult = handleUserRouter(req, res)
      if (userResult) {
         userResult.then(userData => {
            res.end(
               JSON.stringify(userData)
            )
         })
         return
      }
      // const userData = handleUserRouter(req, res)
      // if (userData) {
      //    res.end(
      //       JSON.stringify(userData)
      //    )
      //    return
      // }

在这里插入图片描述


二、完善登录功能前期知识点及准备
  • 核心:登录校验&登录信息存储
  • cookie和session
  • session写入redis(内存数据库) 补:mysql是硬件数据库
  • 开发登录功能,和前端联调(用到nginx反向代理)

什么是cookie

  • 存储在浏览器的一段字符串(最大5kb
  • 跨域不共享(访问不同的域名,之间的cookie不共享
  • 格式如k1 = v1 , k2 = v2;因此可以存储结构化数据
  • 每次发送http请求,会将请求域的cookie一起发送给server
  • server 可以修改cookie并返回给浏览器
  • 浏览器中也可以通过javascript修改cookie(有限制 比如server端可以将某一段cookie进行锁死,防止浏览器进行修改)

  • server 端nodejs操作cookie
//获取cookie (字符串)
const cookieStr = req.headers.cookie || ''  //格式 k1 = v1;k2 = v2
  • 回到项目中,在app.js中的serverHandle()方法添加获取cookie
 //解析cookie
   req.cookie = {}
   const cookieStr = req.headers.cookie || ''//k1=v1;k2=v2
   cookieStr.split(';').forEach(item =>{  //item 格式是 k1 =v1
      if (!item) {
         return
      }
      const arr = item.split('=')
      const key = arr[0].trim()
      const val = arr[1].trim()
      req.cookie[key] = val
   })
  • 测试
    在这里插入图片描述

三、使用cookie完善登录验证
  • 思路:用户登录成功后,会将用户名存放在cookie中,并返回到客户端。当对博客进行增删查改时,会查看cookie中是否存在用户名来进行判断是否登录防止用户绕过登录,对博客进行不法操作
  • 为了方便调试,重新创建一条GET和其他路径的请求
// router/user.js

 //登录验证的测试
    if (method === 'GET' && req.path === '/api/user/login-test') {
        if (req.cookie.username) {
          return Promise.resolve(new SuccessModel())
        }
        return Promise.resolve(new ErrorModel('尚未登录'))
    }
  • 测试
    在这里插入图片描述
    如果我们在浏览器手动添加cookie中的username字段值为zhangsan时(这是个弊端,下面内容会对其进行解决)
    在这里插入图片描述
  • 修改真正登录路由
//登录
    if (method === "POST" && req.path === "/api/user/login") {
        const postData = req.body
        const { username, password } = postData
        const result = login(username, password)
        return result.then(data => {
            if (data.username) {
                //操作cookie
                res.setHeader('Set-Cookie',`username=${data.username};path=/`)
                //path=/所有网页都生效

                return new SuccessModel()
            }
            return new ErrorModel('登录失败')
        })
    }
  • 此时,就暴露出一个问题就是客户端可以对cookie中的username进行修改,伪造身份,绕过登录,直接对博客进行操作
    在这里插入图片描述

  • 解决方法:服务端对cookie进行限制。
    在这里插入图片描述

  • 操作cookie的过期时间

//获取cookie过期时间  router/user.js

const getCookieExpires = () => {
   const d =new Date()
   //当前时间加上一天
   d.setTime(d.getTime() + (24 * 60 * 1000))
   console.log('d.toGMTString() is',d.toGMTString())
   return d.toGMTString()
}
  • 在设置cookie时添加过期时间
    在这里插入图片描述
    在这里插入图片描述

四、使用session再完善登录验证

接着第三点使用cookie完成登录功能时,会暴露出一个问题:username暴露出来,很危险。(cookie尽量不要存放比较敏感的信息

  • 如何解决:cookie中存储userid,server端对应username
  • 解决方案:session,即server端存储用户信息
  • 代码演示(在app.js文件中)
//session数据
const SESSION_DATA ={} //全局环境

//serverHandle()方法中

//解析 session
   let needSetCookie = false //是否需要设置cookie中的userid
   let userId = req.cookie.userid
   if (userId) {//cookie存在该用户的userid (理解为客户端存放了userid)
   
      if (!SESSION_DATA[userId]) {//sessin没有存放username
         SESSION_DATA[userId] = {}//赋值为空对象
      }

   } else {//cookie不存在该用户的userid (理解为客户端没有存放了userid)
      console.log(3)
      needSetCookie = true 
      userId =`${Date.now()}_${Math.random()}`//设置成随机字符串
      SESSION_DATA[userId] = {}
   }
   req.session = SESSION_DATA[userId] //每一个用户对应一个名为userId的session
  • 设置cookie (在app.js路由转发,返回数据后进行设置)
  //处理user路由
      const userResult = handleUserRouter(req, res)
      if (userResult) {
         userResult.then(userData => {
            if (needSetCookie) {
               //操作cookie
               res.setHeader('Set-Cookie', `userid=${userId}; path=/; httpOnly; expires=${getCookieExpires()}`)
            }
            res.end(
               JSON.stringify(userData)
            )
         })
         return
      }
//blog路由也是要设置cookie,跟user路由一样
  • 因为app.js已经设置了cookie,因此原先路由层的设置cookie不用设置,应该将数据(从数据库中取出)存放在seesion中,以便后期判断用户是否登录成功
 //登录
    if (method === "POST" && req.path === "/api/user/login") {
        const postData = req.body
        const { username, password } = postData
        //    const username = req.query.username
        //    const password = req.query.password
        const result = login(username, password)
        return result.then(data => {
            if (data.username) {
                //操作cookie
                //res.setHeader('Set-Cookie', `username=${data.username}; path=/; httpOnly; expires=${getCookieExpires()}`)//path=/所有网页都生效
                req.session.username = data.username
                req.session.realname = data.realname
                return new SuccessModel()
            }
            return new ErrorModel('登录失败')
        })
    }

在这里插入图片描述
在这里插入图片描述

  • 测试路径
 //登录验证的测试
    if (method === 'GET' && req.path === '/api/user/login-test') {
        if (req.session.username) {

            return Promise.resolve(new SuccessModel({session:req.session}))
        }
        return Promise.resolve(new ErrorModel('尚未登录'))
    }
 }
  • 进行测试
    在这里插入图片描述
    在这里插入图片描述
五、使用redis最终完善登录验证

(1)到目前为止还存在一个问题是:

  • 目前session直接是js变量,放在nodejs进程内存中
  • 第一,进程内存内存有限,访问量过大,内存暴增怎么办?
  • 第二,正式上线运行是多线程,进程之间内存无法共享

(2)解决方案 redis

  • web server 最常见的缓存数据库,数据存放在内存中(读写快,价格贵,存储少,一断电内存就丢失
  • 相对于mysql(硬盘数据库,价格便宜,存储大,读写慢),访问速度快

(3)为何 session 适合用redis

  • session 访问频繁,对性能要求极高
  • session可不考虑断电丢失数据的问题(临时数据
  • session数据量不是很大(相对于mysql中存放的数据)

(4)为何网站数据不适合用redis

  • 操作频率不是太高(相对于session操作)
  • 断电不能丢失,必须保留
  • 数据量太大,内存成本太高

安装redis教程

5.1、用redis存储session(小案例)
  • 在命令行中启动 redis 输入命令 redis-server.exe redis.windows.conf
    在这里插入图片描述

  • 在项目中下载redis插件
    在这里插入图片描述

  • 代码

const redis = require('redis')

//创建客户端
const redisClient = redis.createClient(6379,'127.0.0.1')

//报错处理
redisClient.on('error', err => {
    console.error(err)
})

//测试
redisClient.set('username','zhangsan',redis.print)
redisClient.get('username',(err,val) => {
    if (err) {
        console.error(err)
    }
    console.log('val',val)

    //退出
    redisClient.quit()
})

在这里插入图片描述

5.2、回到项目中使用nodejs连接redis-封装工具函数
  • 配置redis参数(conf/db.js文件)
let REDIS_CONF //redis
if (env === "dev") {
  REDIS_CONF = {
      REDIS_CONFport:6379,
      host:'127.0.0.1'
    }
}
if (env === "production") {
   //线上配置
}
module.exports = {
   REDIS_CONF
}
  • 封装redis为工具函数
// db/redis.js

const {REDIS_CONF} = require('../conf/db')
const redis = require('redis')

//创建 redis 客户端
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) {//异步
    const promise = 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) {
                resolve(val)
            }
        })
    })
    return promise
}

module.exports = {
    set,
    get
}
  • session存入redis(将app.js原来涉及session的去掉)
 //解析session
 
 //serverHandle()方法中
   let needSetCookie = false
   let  userId = req.cookie.userid
   if (!userId) {
      /*userId 并不存在,产生一个随机数存入到 redis 并且在返回信息到客户端时将该 userId 存入到 cookie
       这一步操作的目的在于说:
      用户知道未登录后,重新在当前页面登录时,会携带 cookie 发送给服务器端,
      此时,服务端可以通过 userId 获取到 redis 中的值(可能有值,也可能为空对象)
      */
      needSetCookie = true
      userId = `${Date.now()}_${Math.random()}`
      //初始化 redis 中 session 值
      set(userId,{})
   }
   //获取session
   req.sessionId = userId
   get(req.sessionId).then(sessionData => {
      if (sessionData == null) {
          //初始化 redis 中 session 值
         set(req.sessionId,{})
         //设置 session
         req.session = {}
      }else {
         req.session = sessionData
      }
   })
  • 将从数据库mysql取出的信息同步到数据库Redis
 //同步到 redis  (router/user.js)
 set(req.sessionId,req.session)

此时,在app.js中的serverHandle()方法中有两个promise任务,获取session中的get和解析post请求参数
在这里插入图片描述
如果按照第二种方式(即原程序一样)会报错

  • 程序并不能先原先设想一样,先执行从redis中获取session,并将其存入到session中(从上往下执行)
  • 至于程序中promise任务执行的先后,我们并不知道

解决方法:第一种方法(通过链式来按照原先自己设想的逻辑

  • 测试
    在这里插入图片描述

下一篇,篇尾可以下载源码哦!!!!!!

欢迎访问我的个人博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海面有风

您的鼓励将是我前进的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值