/ Node  

JWT的Node简单实现


        

What is JWT

JSON Web Token(JWT) 是一种基于json格式的创建 token 的开放标准 RFC 7519,常用于进行客户端与服务端的权限验证或是信息交换

在我们熟悉的客户端权限验证方案中,通常有两种

  1. 用户登陆验证后服务端将权限凭证( seesionId 放在 cookie 中,每一次请求都会对请求自带的 cookie 进行校验

  2. 用户登陆验证后将权限凭证 token 返回给客户端,客户端将 token 存储在本地,之后的每一次请求需要将 token 携带(通常约定在 header 中),服务端在返回期望结果前先对 token 进行校验

JWT 是 token 方案中一种,对格式进行约定的规范。

该方案明显的优点是可扩展性强、跨域支持。

 

接下来会从以下几个问题来聊聊 JWT

  • 验证流程是怎样的?

  • JWT 的基本格式与 Node 实现

  • cookie 方案与 token 方案的特点

 

验证流程

  1. 客户端在用户通过个人信息登陆成功后从服务端获取到一个登陆凭证 JWT token ,通常 token 需要包含过期时间,让登陆凭证能超时自动失效掉。

  2. 客户端存储在本地,如 local storage、session storage。

  3. 每次发送请求, 将 JWT 协议的 token 放入请求头中。如果验证通过则返回预期结果,如果失败则需要重新登陆获得新的有效 token。

  4. 用户登出账户后,客户端需要清除掉 token。

相当于每一次发送请求都要将登陆凭证 token 上传,通常放在 Authorization header 中,用 Bearer schema

1
Authorization: Bearer  <token>

 

JWT 及 Node 实现

JWT 由三个部分组成,以. 的形式连接,分别是

  • Header

  • Payload

  • Signature

Header 中常用字段 algtyp ,用来声明生成签名部分(Signature)的算法,以及 token 的类型,JWT 方案就设置为 JWT

算法选择上,可以选择对称加密算法,或是非对称加密算法 RS256 等,下面的简单实现以对称加密算法 HS256 为例。

一个常见的例子

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

完整的字段格式整理如下,具体说明见文档,这块我还无法全部理解

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
2
3
4
{ 
"email": "liluhuizj@qq.com",
"exp": 1557673606348
}

完整的预定义字段如下

key desc
iss 发行者的 token
sub 主题的 token
aud 接受者(听众)的 token
exp 这可能是 Registered Claims 最常用的,定义数字格式的有效期限,重点是有效期限一定要大于现在的时间
nbf 生效时间,定义一个时间在这个时间之前 JWT 不能进行处理
iat 发行的时间,可以被用来判断 JWT 已经发出了多久
jti JWT 唯一的识别值,可用来防止 JWT 被重复使用,尤其在一次性的 token 特別好用

 

我们可以通过这样一个方法生成 playload :

1
2
3
4
5
6
function payloadWithExpirationTime (payload, minutesFromNow) { 
let date = new Date()
date.setMinutes(date.getMinutes() + minutesFromNow)
payload.exp = date.getTime()
return payload
}

Signature

Signature 是对前两部分的加密签名,用于防止数组篡改。

Signature 由三部分组成,分别是

  • base64 编码后的 Header

  • base64 编码后的 Payload

  • 仅服务端知道(定义)的 secret

一个生成签名的基本范式是:

1
HMACSHA256( base64UrlEncode(header) + '.' + base64UrlEncode(payload), 'secret')

实现(生成)

接下来我们用 Node 来实现,

首先是把 JSON 对象编码为 base64 的方法

1
2
3
4
5
6
7
8

function base64UrlEncodeJSON (json) {
return Buffer.from(
JSON.stringify(json)
).toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
}

然后需要一个加密生成签名的方法

1
2
3
4
5
6
7
8
9
10
11

const crypto = require('crypto')

function generateSignature (str, secret) {
return crypto
.createHmac('sha256', secret)
.update(str)
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
}

调用方法,套用范式搞定

1
2
3
4
5

const encodedHeaderInBase64 = base64UrlEncodeJSON(header)
const encodedPayloadInBase64 = base64UrlEncodeJSON(payload)
const encodedSignatureInBase64 = generateSignature(`${encodedHeaderInBase64}.${encodedPayloadInBase64}`, 'some-secret')
const token = `${encodedHeaderInBase64}.${encodedPayloadInBase64}.${encodedSignatureInBase64}`

最终这个就是服务端生成的 JWT Token :
${encodedHeaderInBase64}.${encodedPayloadInBase64}.${encodedSignatureInBase64}

实现(校验)

生成 token 的方法已经搞定了,接下来是对 token 的解析校验了

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

// Returns true if token is valid, otherwise returns false
function isValid (token, secret) {
const parts = token.split('.')
const header = base64UrlDecodeToJSON(parts[0])
const payload = base64UrlDecodeToJSON(parts[1])
if (header.alg !== 'HS256' || header.typ !== 'JWT') {
return false
}
const signature = parts[2]
const exp = payload.exp
if (exp) {
if (exp < new Date().getTime()) {
return false
}
}
return generateSignature(`${parts[0]}.${parts[1]}`, secret) === signature
}
function base64UrlDecodeToJSON (str) {
return JSON.parse(
Buffer.from(
str.replace(/-/g, '+').replace(/_/g, '/'), 'base64'
).toString('utf8')
)
}

使用场景的比较

文章的开头提到过通常最常用的两种方案 cookie-sessionToken, 我觉得目前也不用太过于吹捧 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 这套方案相似的处理。

 

参考资料