以前尝试过使用Midway.js 结合egg-jwt
插件来做过jwt的token。这种插件式的方式虽然便利,但是还是基于别人封装的库在使用,对于一些基本的实现还是有些模糊的,这次正好结合express
和JsonWebToken
,来手动实现这个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 | const token = jwt.sign({ |
那么我们尝试下使用pem文件作为密钥。
那么pem文件是啥?
Privacy Enhanced Mail,一般为文本格式,以 —–BEGIN… 开头,以 —–END… 结尾。中间的内容是 BASE64 编码。这种格式可以保存证书和私钥,有时我们也把PEM 格式的私钥的后缀改为 .key 以区别证书与私钥。
首先我们生成下pem文件:
1 | openssl genrsa -des3 -out key.pem 2048 |
这个命令会生成一个2048位、des3方法加密的密钥,在生成的时候如下所示,会需要一个单独的短语密码(如果嫌麻烦可以将-des3
加密去掉):
1 | Generating RSA private key, 2048 bit long modulus |
然后我们可以看到生成了一个pem文件:key.pem
。
我们放入到对应的项目文件夹:src/config/key.pem
,然后在程序中使用:
1 | const fs = require('fs') |
跑一下接口测试,是可以正常生成的token。
options
可选参数,用来配置jwt的加密算法、过期时间以及其他的一些东西,来看下常用参数:
- algorithm 加密的算法,默认为
HS256
。 - expiresIn 过期时间,以秒或表示时间跨度的
zeit/ms
来表示。例如:60
,2days
、10h
、7h
等。如果值时数值的话会被解释为秒计算,如果使用字符串的话需要确保提供时间单位(天、小时等),如果没有单位的数字字符串,会被解释为毫秒("120"
会被解释为120ms
)。 - noBefore 延迟有效,时间的解释和
expiresIn
相同,但表示的是在这个时间之前是无效的,时间之后才有效。比如设置了7h
,那么在生成token的7小时只能是无效的,在七小时之后才有效。
options的其他参数暂时也没用到,可以直接参考文档
callback 回调函数
如果在签名函数中提供了callback回调函数,那么生成token的方式将变成为异步的方式,在回调函数中可以接受错误消息err
或token
。例如:
1 | const token = jwt.sign({ |
修改成带回调函数后,只可以在回调函数中接收到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 | app.use(function (req, res, next) { |
也可以不提供回调函数来使用同步的写法:
1 | app.use(function (req, res, next) { |
异常以及错误码
使用jwt.verify()
函数进行解token的时候,有可能会抛出错误,一般包含下面几种:
TokenExpiredError token已过期
验证token过期的话会抛出这个错误:
1 | err = { |
JsonWebTokenError
当token本身有问题时就抛出这个错误:
1 | err = { |
错误消息可能是:
- ‘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 | err = { |
综合代码
Ok,了解了上面相关的方法和参数,我们来整合下我们的代码。
首先,签名函数和解码函数都需要密钥文件,我们可以将token的生成和解码都封装到lib文件夹下面的jwt.js
文件中:
1 | const fs = require('fs') |
使用:
1 | const jwt = require('./lib/jwt') |
完成了使用JsonWebToken
来做token的签发和解码,结合express,可以完成用户登录功能。其他功能后续探索。