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