0%

Express.js中的jwt应用

以前尝试过使用Midway.js 结合egg-jwt插件来做过jwt的token。这种插件式的方式虽然便利,但是还是基于别人封装的库在使用,对于一些基本的实现还是有些模糊的,这次正好结合expressJsonWebToken,来手动实现这个token的签发和解码过程。

关于一些jwt的基本概念可以参考以前的这篇文章:midway jwt token 探索

生成token

来看下签名语法:

1
jwt.sign(payload, secretOrPrivateKey, [options, callback])

payload 有效载荷

有效载荷,通常为对象或字符串或者是缓冲区。在解token的时候可以获得对应的数据。如果有效载荷不是缓冲区或字符串,那么将会使用JSON.stringify将其强制转换为字符串。

一般操作是在登录接口中,通过账户密码和数据库信息进行匹配后,将脱敏的信息作为载荷放入到签名函数中,然后生成token再发送给客户端,下次请求的时候客户端将带着这个token,然后我们可以解出对应的数据,做登录后的操作。

secretOrPrivateKey 密钥

签名的密钥,我们可以简单的设置为一个字符串,也可以设置为其中包含HMAC算法的密钥或者是基于RSA和ECDSAPEM编码的私钥。

我们可以直接使用了字符串xxx作为短语密钥:

1
2
3
4
5
6
const token = jwt.sign({
name: user.name,
email: user.email,
}, 'xxx', {
expiresIn: '1d',
})

那么我们尝试下使用pem文件作为密钥。

那么pem文件是啥?

Privacy Enhanced Mail,一般为文本格式,以 —–BEGIN… 开头,以 —–END… 结尾。中间的内容是 BASE64 编码。这种格式可以保存证书和私钥,有时我们也把PEM 格式的私钥的后缀改为 .key 以区别证书与私钥。

首先我们生成下pem文件:

1
$ openssl genrsa -des3 -out key.pem 2048

这个命令会生成一个2048位、des3方法加密的密钥,在生成的时候如下所示,会需要一个单独的短语密码(如果嫌麻烦可以将-des3加密去掉):

1
2
3
4
5
6
Generating RSA private key, 2048 bit long modulus
...........+++
......+++
e is 65537 (0x10001)
Enter pass phrase for key.pem:
Verifying - Enter pass phrase for key.pem:

然后我们可以看到生成了一个pem文件:key.pem

我们放入到对应的项目文件夹:src/config/key.pem,然后在程序中使用:

1
2
3
4
5
6
7
8
9
const fs = require('fs')
const Path = require('path')
const privateKey = fs.readFileSync(Path.resolve(__dirname, '../../config/key.pem'))
const token = jwt.sign({
name: user.name,
email: user.email,
}, privateKey, {
expiresIn: '1d',
})

跑一下接口测试,是可以正常生成的token。

options

可选参数,用来配置jwt的加密算法、过期时间以及其他的一些东西,来看下常用参数:

  • algorithm 加密的算法,默认为HS256
  • expiresIn 过期时间,以秒或表示时间跨度的zeit/ms来表示。例如:602days10h7h等。如果值时数值的话会被解释为秒计算,如果使用字符串的话需要确保提供时间单位(天、小时等),如果没有单位的数字字符串,会被解释为毫秒("120"会被解释为120ms)。
  • noBefore 延迟有效,时间的解释和expiresIn相同,但表示的是在这个时间之前是无效的,时间之后才有效。比如设置了7h,那么在生成token的7小时只能是无效的,在七小时之后才有效。

options的其他参数暂时也没用到,可以直接参考文档

callback 回调函数

如果在签名函数中提供了callback回调函数,那么生成token的方式将变成为异步的方式,在回调函数中可以接受错误消息errtoken。例如:

1
2
3
4
5
6
7
8
const token = jwt.sign({
name: user.name,
email: user.email,
}, privateKey, {
expiresIn: '1d',
}, function (err, token) {
console.log(err, token)
})

修改成带回调函数后,只可以在回调函数中接收到token,变成了异步的形式,我们直接赋值的token反而是没有值了。

解token

前面已经可以正常生成token了,我们希望在请求头中携带token,并且在添加中间件处理token获得携带的有效载荷。

看下验证的函数:

1
jwt.verify(token, secretOrPublicKey, [options, callback])

token

通过签名函数生成的token,需要客户端在请求头中携带。

secretOrPublicKey

密钥,尝试了下,签名函数和验证函数需要的密钥必须一样,而不是公钥、私钥密钥对。一样才可以解析出来,否则就没法解出载荷。

options

常用的参数有:

  • algorithms 允许的算法名称的列表,例如['HS256', 'HS384']
  • complete 返回解码的对象的所有属性(有效负载、表头、签名),而不是仅仅有效载荷的常规内容。
  • ignoreExpiration 如果为true,则忽略校验过期时间。
  • ignoreNotBefore 如果为true,则忽略校验延迟时间

options的其他参数暂时也没用到,可以直接参考文档

callback 回调函数

如果提供回调函数,则函数变为异步的处理方式,在回调中接受错误err或是解码的载荷。

如果没有提供回调函数,则函数为同步执行的方式,解码成功的话将返回有效载荷。如果没有,则抛出错误。

解码的方法可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.use(function (req, res, next) {
if (req.headers.hasOwnProperty('token')) {
jwt.verify(req.headers.token, publicKey, {}, function (err, decode) {
if (err) {
return res.json({
message: 'token不存在或已过期',
code: '20005'
}).status(200);
}
console.log(decode)
next();
})
} else {
next();
}
})

也可以不提供回调函数来使用同步的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.use(function (req, res, next) {
if (req.headers.hasOwnProperty('token')) {
try {
const decoded = jwt.verify(req.headers.token, publicKey);
// todo 将解码出的内容放置在全局变量中
next();
} catch (e) {
return res.json({
message: 'token不存在或已过期',
code: '20005'
}).status(200);

}
} else {
next();
}
})

异常以及错误码

使用jwt.verify()函数进行解token的时候,有可能会抛出错误,一般包含下面几种:

TokenExpiredError token已过期

验证token过期的话会抛出这个错误:

1
2
3
4
5
err = {
name: 'TokenExpiredError`,
message: 'jwt expired',
expiredAt: 1408621000,
}

JsonWebTokenError

当token本身有问题时就抛出这个错误:

1
2
3
4
5
err = {
name: 'JsonWebTokenError`,
message: 'jwt malformed',
}

错误消息可能是:

  • ‘jwt malformed’
  • ‘jwt signature is required’
  • ‘invalid signature’
  • ‘jwt audience invalid. expected: [OPTIONS AUDIENCE]’
  • ‘jwt issuer invalid. expected: [OPTIONS ISSUER]’
  • ‘jwt id invalid. expected: [OPTIONS JWT ID]’
  • ‘jwt subject invalid. expected: [OPTIONS SUBJECT]’

NotBeforeError

延迟时间还没到就抛出这个错误:

1
2
3
4
5
err = {
name: 'NotBeforeError',
message: 'jwt not active',
date: '2021-03-18T06:18:52.482Z',
}

综合代码

Ok,了解了上面相关的方法和参数,我们来整合下我们的代码。

首先,签名函数和解码函数都需要密钥文件,我们可以将token的生成和解码都封装到lib文件夹下面的jwt.js文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const fs = require('fs')
const Path = require('path')
const jwt = require('jsonwebtoken')
const privateKey = fs.readFileSync(Path.resolve(__dirname, '../config/key.pem'))

class Jwt {
static sign(payload, options = {}) {
let token;
try {
token = jwt.sign(payload, privateKey, options);
} catch (e) {
throw new Error(e)
}
return token;
}

static verify(token, options = {}) {
let decoded;
try {
decoded = jwt.verify(token, privateKey, options);
} catch (e) {
throw new Error(e)
}
return decoded;
}
}
module.exports = Jwt

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const jwt = require('./lib/jwt')
app.use(function (req, res, next) {
if (req.headers.hasOwnProperty('token')) {
try {
const decoded = jwt.verify(req.headers.token);
res.locals.user = {
id: decoded.id,
email: decoded.email,
}
next();
} catch (e) {
return res.json({
message: 'token不存在或已过期',
code: '20005'
}).status(200);

}
} else {
next();
}
})

完成了使用JsonWebToken来做token的签发和解码,结合express,可以完成用户登录功能。其他功能后续探索。

码字辛苦,打赏个咖啡☕️可好?💘