JWT的Node简单实现
What is JWT
JSON Web Token(JWT) 是一种基于json格式的创建 token 的开放标准 RFC 7519,常用于进行客户端与服务端的权限验证或是信息交换。
在我们熟悉的客户端权限验证方案中,通常有两种
用户登陆验证后服务端将权限凭证( seesionId 放在 cookie 中,每一次请求都会对请求自带的 cookie 进行校验
用户登陆验证后将权限凭证 token 返回给客户端,客户端将 token 存储在本地,之后的每一次请求需要将 token 携带(通常约定在 header 中),服务端在返回期望结果前先对 token 进行校验
JWT 是 token 方案中一种,对格式进行约定的规范。
该方案明显的优点是可扩展性强、跨域支持。
接下来会从以下几个问题来聊聊 JWT
验证流程是怎样的?
JWT 的基本格式与 Node 实现
cookie 方案与 token 方案的特点
验证流程
客户端在用户通过个人信息登陆成功后从服务端获取到一个登陆凭证 JWT token ,通常 token 需要包含过期时间,让登陆凭证能超时自动失效掉。
客户端存储在本地,如 local storage、session storage。
每次发送请求, 将 JWT 协议的 token 放入请求头中。如果验证通过则返回预期结果,如果失败则需要重新登陆获得新的有效 token。
用户登出账户后,客户端需要清除掉 token。
相当于每一次发送请求都要将登陆凭证 token 上传,通常放在 Authorization header 中,用 Bearer schema
1 | Authorization: Bearer <token> |
JWT 及 Node 实现
JWT 由三个部分组成,以.
的形式连接,分别是
Header
Payload
Signature
Header
Header 中常用字段 alg
、typ
,用来声明生成签名部分(Signature)的算法,以及 token 的类型,JWT 方案就设置为 JWT
。
算法选择上,可以选择对称加密算法,或是非对称加密算法 RS256 等,下面的简单实现以对称加密算法 HS256 为例。
一个常见的例子
1 | { |
完整的字段格式整理如下,具体说明见文档,这块我还无法全部理解
key | desc |
---|---|
alg | Algorithm |
enc | Encryption Algorithm |
zip | Compression Algorithm |
jku | JWK Set URL |
jwk | JSON Web Key |
kid | Key ID |
x5u | X.509 URL |
x5c | X.509 Certificate Chain |
x5t | X.509 Certificate SHA-1 Thumbprint |
x5t#S256 | X.509 Certificate SHA-256 Thumbprint |
typ | Type |
cty | Content Type |
crit | Critical |
Payload
Payload,即声明。通常是关于用户或其他数据的声明,需要注意不要将敏感信息明码传递了;另外规范约定了一组预定义的字段,虽然是非强制的,还是建议按照规范约定来。
一个简单的例子如下
1 | { |
完整的预定义字段如下
key | desc |
---|---|
iss | 发行者的 token |
sub | 主题的 token |
aud | 接受者(听众)的 token |
exp | 这可能是 Registered Claims 最常用的,定义数字格式的有效期限,重点是有效期限一定要大于现在的时间 |
nbf | 生效时间,定义一个时间在这个时间之前 JWT 不能进行处理 |
iat | 发行的时间,可以被用来判断 JWT 已经发出了多久 |
jti | JWT 唯一的识别值,可用来防止 JWT 被重复使用,尤其在一次性的 token 特別好用 |
我们可以通过这样一个方法生成 playload :
1 | function payloadWithExpirationTime (payload, minutesFromNow) { |
Signature
Signature 是对前两部分的加密签名,用于防止数组篡改。
Signature 由三部分组成,分别是
base64 编码后的 Header
base64 编码后的 Payload
仅服务端知道(定义)的 secret
一个生成签名的基本范式是:
1 | HMACSHA256( base64UrlEncode(header) + '.' + base64UrlEncode(payload), 'secret') |
实现(生成)
接下来我们用 Node 来实现,
首先是把 JSON 对象编码为 base64 的方法
1 |
|
然后需要一个加密生成签名的方法
1 |
|
调用方法,套用范式搞定
1 |
|
最终这个就是服务端生成的 JWT Token :${encodedHeaderInBase64}.${encodedPayloadInBase64}.${encodedSignatureInBase64}
实现(校验)
生成 token 的方法已经搞定了,接下来是对 token 的解析校验了
1 |
|
使用场景的比较
文章的开头提到过通常最常用的两种方案 cookie-session
和 Token
, 我觉得目前也不用太过于吹捧 token 方案,还是合理看待两者的特性和局限性吧。
存储
cookie-session 方案在是在用户验证通过后,在服务端创建存储一条用户信息,客户端仅存储一个 SessionId 随着认证用户增多,服务器的开销会增大,但这个问题在目前外部session存储方案非常多的情况下基本都可以应对
Token 方案是将凭证信息存储在客户端,服务端无需存储
由于 session 通常存储在内存中,这也带来一些扩展性的问题
跨域
cookie-session 多域名端资源跨域请求时会遭到跨域问题,需要前后端处理共同处理才能携带 cookie
Token 前端主动在header中携带 token,不会有跨域问题
这里其实前端 header 中的字段非常规的话,依然会遇到的跨域问题的,其实我觉得跨域这事不是什么大事,参考各种 CORS 解决方案就是了。
安全性
cookie-session 方案用户很容易受到CSRF攻击,Token 方案则避免了这个问题
如果 sessionId 泄漏,别人可以盗用身份;如果 secret 泄漏也是同样的。虽然 secret 在各种案例中是全局的一个固定值,是不是也可以设计成和用户相关的来提高安全性?
无论使用哪种方式切记用HTTPS来保证数据的安全性
JWT 生成的 token 看起来不可读,实际 base64 解码后数据信息一览无余,加密的签名只是用于服务端校验,所以不能明码存储敏感信息
状态更新
由于 Token 存在客户端,一旦签发后无法更改,比如用户登出了、更换密码、或是某个 JWT 被盗用,需要前期在设计时有特别的逻辑,否则真出现问题时将无能为力。真要用一定要考虑是否有这个场景,是否需要设计一个方式来避免这个问题 ,比如黑名单、服务器存储失效时间等等
同样,续签的处理对于 Token 方案来说也是需要设计的
总结
JWT 还是更适合做有效期极短的验证,比如限定时间的激活账号链接;或是一些简单的 restful api 认证。会话管理这套还是 cookie + session 有更完整的方案,JWT 如果要做会话管理还是需要参考结合 session + cookie 这套方案相似的处理。