前言
上一节咱们实现了一个登录接口,但仅仅是“登录”而已。登录完成之后,服务器应该保存好登录态,以认识下一个访问的“人”。那么本节就以主流的保存登录态方案为例,给咱们的登录接口加上登录态。同时,中间件是 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
参数中,这里就不再做多赘述。