玩转Express(二)登录态&中间件

1,528 阅读5分钟

前言

上一节咱们实现了一个登录接口,但仅仅是“登录”而已。登录完成之后,服务器应该保存好登录态,以认识下一个访问的“人”。那么本节就以主流的保存登录态方案为例,给咱们的登录接口加上登录态。同时,中间件是 Express 中一个十分重要的概念,咱们今天也来一起盘一盘它。

登录态

首先 http 是无状态的,它无法识别用户。但就现在的网站而言,大部分接口都需要知道当前操作的用户是谁,每次发送接口都需要区分出每个人。所以登录态就是用来区分用户的,让服务器知道这次请求会话是谁发送的,谁需要处理的。

本文将讲述两种保存登录态的方式一是服务端有状态,一是服务端无状态。

服务端有状态

当登录成功后,服务器会根据当前登录的用户生成一个 id ,并把它分别保留在客户端的 cookie 以及服务端中。则客户端每次请求的时候都会带上这个 id ,其实服务端就可以拿这个 id 来做对比,从而知道当前请求的用户是谁。服务端的状态存储可以利用 session ,也可以利用别的方式,我们本着折腾的心态,用一个单例的缓存来存储状态。

Cache类实现

我们现在自己简单实现一个缓存的类,这个类暂定有以下功能:

  • 键值对形式存储
  • 支持过期时间

好的,现在我们已经列出来了这些功能。接下来慢慢实现即可。现在根目录下新建一个 cache.js 文件。具体代码如下:

class Cache {
    constructor(maxAge = 1000 * 60 * 30) {
        this.maxAge = maxAge
        //保存缓存的object
        this.cache = {}
        //过期时间object
        this.ttl = {}
    }
    getCache(key) {
        let ttl = this.ttl[key]
        if ((+new Date()) - ttl >= this.maxAge) {
            //过期了则删除
            this.deleteCache(key)
            return undefined
        } else {
            return this.cache[key]
        }
    }
    setCache(key, value) {
        this.cache[key] = value
        this.ttl[key] = +new Date()
    }
    deleteCache(key) {
        delete this.cache[key]
        delete this.ttl[key]
    }
}

module.exports = Cache

app.js中引入使用如下:

const Cache = require('./cache')
global.cache = new Cache()

登录态保存

那么我们就可以愉快的将用户信息存储在服务端了,改造登录接口如下

res.cookie('n_session_id', md5(account), {
    httpOnly: true,//客户端不可修改cookie
    maxAge: 1000 * 60 * 30
})
cache.setCache(md5(account), account)

接下来咱们加上一个接口,验证一下登录态:

router.get('/getList', (req, res) => {
  let account = req.cookies.n_session_id
  let serverAccount = cache.getCache(account)
  if (!account || !serverAccount) {
    res.json('请先登录')
  } else {
    let mock = [{
      id: 1,
      name: 'abc'
    }, {
      id: 2,
      name: 'efg'
    }]
    res.json(mock)
  }
})

服务端无状态

上述服务器有状态的方法有一个明显的缺点,当系统变大服务器数量变多时,有可能用户当前访问的服务器并不是用户之前保存登录态的服务器,当然也可以用 Redis 或其他方案来解决这个问题。下面我们用一种名为 JWT 的方案,让登录态完全保存在客户端。具体 JWT 的讲解可参考文章什么是 JWT -- JSON WEB TOKEN

接入JWT

首先先安装 jsonwebtoken

npm install jsonwebtoken

再简单封装一个加密算法,如下:

const jwt = require('jsonwebtoken');
const Token = {
  //data为加密数据
  encrypt:function(data){
    return jwt.sign(data, 'token')
  },
  decrypt:function(token){
    try {
      let data = jwt.verify(token, 'token');
      return {
        token:true,
        id:data.id
      };
    } catch (e) {
      return {
        token:false,
        data:e
      }
    }
  }
}

接下来就可以在路由中愉快的使用了。 token 的客户端存储一般有两种,一种存放在 cookie 中,另一种 json 返回 token 后,客户端每一次请求都把 token 放在请求头里。这里采用第一种,改造登录请求如下:

const token = Token.encrypt({
    user: account
})
res.cookie('token', token, {
    httpOnly: true,
    maxAge: 1000 * 60 * 30
})

验证 token 时如下操作即可:

//解密
let data = Token.decrypt(req.cookies.token);
if (data.token) {
    //有效token
}else{
    //无效token
}

中间件

从本质上说,一个 Express 应用是在调用各种中间件。中间件( middleware )是一个函数,他可以访问请求对象( request object(req) ),响应对象( response object(res) )和 web 应用中处于请求-响应循环

Express中有如下几种中间件

  • 应用级中间件
  • 路由级中间件
  • 错误处理中间件
  • 内置中间件
  • 第三方中间件

一个请求发送到服务器后,它的生命周期是先收到 request(请求),然后服务端处理,处理完了以后发送 response(响应)回去而这个服务端处理的过护,需要把处理的事情分一下,分配成几个部分来做。简单实例可以看笔者的一篇博文Express中间件,这里就不再赘述。

中间件鉴权

我们之前开发了登录的接口以及保存了登录态,也开发了一个测试的接口需要验证等登录态。而在一个系统中,需要验证登录态的接口肯定比不需要验证的多,而每一个接口都写一份验证登录态的逻辑未免太过冗余。所以用中间件来统一处理。

根目录新建一个 middleware 文件夹,新建一个 login.js 文件。

  • 新建一个可以绕过登录态的数组,匹配到里面的元素则直接跳过
  • 获取登录态,若无则跳登录,若有则把 account 加入 req 对象中,下面的路由就可以直接 req.account 拿到用户的信息 -中间件的 use 顺序十分重要
const whileList = ['/users/login'];

function login(req, res, next) {
    let account = req.cookies.n_session_id
    let serverAccount = cache.getCache(account)
    let url = req.url
    if (whileList.includes(url)) {
        next()
    } else {
        if (!account || !serverAccount) {
            res.json('请先登录')
        } else {
            req.account = serverAccount
            next()
        }
    }
}

module.exports = login

然后在 app.js 中引入使用:

const loginMiddleware = require('./middleware/login')
app.use(loginMiddleware)

app.use('/', indexRouter)
app.use('/users', usersRouter)

像我们之前封装的 ORM 类和 Cache 类也可以同样利用中间件的方式放入 req 参数中,这里就不再做多赘述。