再走NODE路: (篇四)原生node实现 cookie + session + redis 登录验证

关于登录这块应该是每一个系统中都必须的一个环节之一,虽然现在的第三方登录确实很流行,也很安全,但别人的始终是别人的。所以我们今天就来说一下如何使用原生的node实现客户端登录验证

  • 关于cookie(仅仅是我的理解): cookie是存储在浏览器的一段字符串,最长限制5kB,它的一些特性如下:
  1. 跨域不共享: 及在不同的请求域下的cookie的值是不同的
  2. 格式为键值的形式: 即 “k1=a1; k2=a2; k3=v3”
  3. 客户端在每次发送http请求时均会携带cookie到服务器
  4. 服务端可以获取cookie中的信息以及往cookie中写入信息
  5. 浏览器端js可以操作cookie, 但操作时会有一定的限制
  • 说完cookie之后,我们就来数一下nodejs 如何去操作cookie:
    在node中可以从 req.headers.cookie 中获取到cookie字符串,对该字符串进行分割解析后便可以得到相应的cookie信息: (这里分割解析就是根据cookie字符串的格式来进行), 如下,我们将cookie字符串解析成对象
// 解析cookie
req.cookie = {
     }
const cookieStr = req.headers.cookie
cookieStr.spilt(";").forEach(item => {
     
	if(!item){
     
		return
	}
	const arr = item.split("=")
	const key = arr[0]
	const value = arr[1]
	req.cookie[key] = value
})

经过如上的解析后我们便将cookie中的键值信息以对象的形式存入到了req.cookie中,后文中我们使用cookie信息便直接到req.cookie中去取得了

而在nodejs 中我们如何操作写入信息到cookie中,可以使用res.setHeader()方法去设置响应头中的cookie信息,如下所示:

res.setHeader('Set-Cookie', `userid=${
       userId}; path=/; httpOnly; expires=${
       getCookieExpires()};`)

其中Set-Cookie表示我们要往cookie中设置值, userid是我们之后写入到cookie中用于标识用户是否登陆的一段字符, path="/" 表示该cookie在当前的所有路径下均有效, httpOnly 表示的是限制客户端操作和修改cookie中的值, expires="" 用于设置cookie的过期时间,如果不进行设置,则该cookie将永久有效;(其中的getCookieExpires()是我定义的一个用于生成cookie过期时间的一个函数)

  • 由上我们介绍的cookie的一些东西,我们便可以这样理解一下登录验证的流程:
    客户端发送登录请求完成登录 -> 服务端在返回登录成功时往客户端的cookie中写入一段userid(sid)的用户登录信息,并限制客户端操作修改cookie -> 之后的每一次用户请求时, 服务端均可以根据客户端请求的cookie中是否有这么一段 userid(sid)来判断用户是否已经登录

于是我们可以这样做,在用户访问我们的登录接口时,我们根据用户传递的登录信息,校验正确后往客户端cookie中写入一段userid, 之后的所有接口访问时我们就解析一下cookie中是否有这样一个userid来返回不一样的值: 如下所示:

  1. 我们首先创建一个全局变量来存储userid所对应的用户信息: (为什么不直接将用户信息存储到cookie中的最大的一个原因是安全问题,避免将用户的信息直接存入到cookie中导致用户信息的泄露)
  • 这里我们就先引入一下session的概念(我个人的理解): session是指在服务端存储用户信息, 当客户端中携带userid(sid)请求时,根据这个session中的信息将userid 和对应的用户信息联系起来
// 定义一个全局的对象用于存储session
const SESSION_DATA = {
     }
  1. 根据每次请求的cookie中的信息判断是否登录以及对应用户的信息:
// 由于我们之前完成了对于cookie的解析,所以我们的req.cookie中便有了cookie的数据
let userId = req.cookie.userid
if(userId){
     
	if(!SESSION_DATA[userId]){
     
		SESSION_DATA[userId] = {
     }   // 判断SESSION_DATA中是否存放由userId对应的信息, 若没有则将其赋值为一个空对象
	}
} else {
     
	userId = `${
       Date.now()}_${
       Math.random()}` // 若userId不存在,则将其赋值为一个任意值: 一般的userId值都是需要经过加密处理的,这里我们为了方便直接就用一串时间戳加一个随机数来代替了
	SESSION_DATA[userId] = {
     }  // 在创建一个userid的同时我们也往SESSION_DATA中存入了该userId的信息
}
// 将SESSION_DATA中的userId对应的信息放到req.session中
req.session = SESSION_DATA[userId]
  1. 在用户登录成功后,我们便调用res.setHeader()将这个userId写入到客户端的cookie中,并限制客户端修改cookie, 与此同时,将登录后从mysql数据库中获取回来的用户信息记录到SESSION_DATA[userId]
// 这里我就简单仿制一份数据,就不到mysql中去拿了,同时为了避免post请求出现跨域问题,这里我就使用GET请求来模拟,将登录信息放到query中传递
   if(req.method === "GET" && req.path === '/api/login'){
     
        const {
      username, password } = req.query
        if(username === "zhangsan" && password === "123") {
     
        	// 这里由于我们上面已经将req.session 和 SESSION_DATA[userId]进行了绑定,所以可以直接往req.session中加入信息
            req.session.username = username
            res.setHeader('Set-Cookie', `userid=${
       userId}; path=/; httpOnly; expires=${
       getCookieExpires()};`)
            res.end("登录成功")
        }
        res.end("登录失败")
    }
  • 这样,在之后每次客户端发起请求时,cookie中均会带上我们写入的userid, 服务端根据这个userid到全局变量的SESSION_DATA中去取得该userid对应的用户信息,并且可以根据这个userid的存在与否判断用户是否已经完成了登录

这里我们引出了新的问题,就是我们不断的将session存入到全局变量SESSION_DATA中,这就存在一个很大的问题: 服务端运行时的变量信息都是存储在进程运行的内存中,所以当不断有用户登录时,SESSION_DATA中存储的信息就会越来越多,而我们知道,一个进行分配的内存空间是很有限的,当进程内存被占用完,进程运行也就崩溃了。 除此之外,还有一个问题是,多进程间的数据信息是无法共享的,所以当一个进程中存储了用户的userid后,当用户进入到另一个服务进程时是无法访问到其userid的

  • 那么解决的办法就是使用缓存数据库redis来处理:
    redis: (缓存数据库: 更快的访问速度,断电数据丢失, 存储的数据量小等特性)
    使用redis解决session储存问题的原因:
  1. redis和mysql做数据存储一样, 它跟web server服务都是不相关的两个服务,不会互相影响,也就不存在web server进程内存不够的情况
  2. redis作为缓存数据库,存取数据的速度更快,能够满足频繁操作读取session的需求
  3. 将session统一的存储到redis中,使得多个进程能够同时访问相同的session
  4. session中的数据信息不是很大,使用redis也满足存储量小的特性 (不考虑内存扩展集群)
  • 首先我们还是简单说一下怎么使用node连接使用redis:
  1. 电脑上下载并配置好redis, 并启动redis(windows系统下运行redis中的redis-server.exe 和 redis.windows.conf文件即可)
  2. 安装redis 模块: npm i redis --save
const redis = require('redis')

// 创建客户端连接: 参数1: redis端口, 参数2: 连接的域地址
const redisClient = redis.createClient(6379, '127.0.0.1')
// 监听错误事件
redisClient.on('error', (err) => {
     
    console.log(err)
})

// 测试连接
// 使用set()方法往redis 中存入数据:redis中存储的数据域格式为key-value的形式
/**
 * 参数1: 数据的键值(key)
 * 参数2: 数据的值(value)
 * 参数3: 传递一个redis.print表示数据存储成功后会打印出信息
 */
redisClient.set("myname", "chenshao", redis.print)

// 使用get()方法获取redis中指定键的值(key), 获取是数据是异步的
/**
 * 参数1: 需要获取数据的键值(key)
 * 参数2: 获取数据成功或者失败时的回调
 */
redisClient.get("myname", (err, val) => {
     
    if(err){
     
        throw err
    }
    console.log(val)
})
// 退出连接: 使用quit()函数关闭连接
redisClient.quit()
  • 到这里我们便可以在每次用户登录成功后,将写入客户端的userid作为redis存储信息的键,将用户信息作为值的形式将该用户的session信息存放到redis中,之后我们直接使用redis中的get()方法根据cookie中的userid到redis中查询用户信息即可:
    这里我为大家简单封装一个操作redis的get()和set() 方法
const redis = require('redis')
// 定义配置项,一般项目中均需要抽离出来的,这里我就直接写在这里了
const conf = {
     
    port: 6379,
    host: '127.0.0.1'
}
const redisClient = redis.createClient(conf.port, conf.host)
redisClient.on('error', (err) => {
     
    console.log(err)
})
/*
* @params key: 存储的session的key; val: 存储的session的value值
*/
function set(key, val){
     
    if(typeof val === 'object'){
     
        val = JSON.stringify(val)
    }
    redisClient.set(key, val, redis.print)
}
/**
* @params key: 获取指定session的key值
*/
function get(key){
     
// 因为获取redis中的数据异步的,所以我们这里作为一个Promise返回
    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) {
     
                resolve(val)
            }

        })
    })
}

module.exports = {
     
    set,
    get
}
  • 根据我们以上创建的get 和 set 方法,之后我们遍直解调用传入session去进行redis进行存储即可,如此,根据客户端请求cookie中是否有userid信息,redis中是否存储有userid对应的用户信息,根据这些判断我们便基本完成了用户登录验证的东西了

以上代码不完善的地方大家可以自行完成,基本的关键步骤以上是都说到了的,文章中的一些描述错误或者打字错误深感抱歉,也希望大家帮助指正,谢谢

你可能感兴趣的:(js,node)