jwt(Json Web token) 是一种用于不同组织之间交换数据的格式。既然名称中包含了Json,显而易见,其数据结构是以Json为基础进行搭建的。数据中包含了三个部分,header
、payload
以及token
。其中token可以使用私钥(HMAC算法),也可以基于公私钥的形式,通过对header及payload进行计算生成,从而达到校验header及payload数据合法性的作用。
使用场景:
- 跨域认证。单点登录系统是最常见的jwt使用场景。用户在登录后,系统返回jwt的信息。后续的所有请求都会带上jwt的信息,从而认证该用户的信息。由于数据的请求可以通过header、cookies、参数等方式,使得跨域访问变得十分简单。
- 跨域数据分享。通过使用公私钥的方法,客户端可以使用公钥对签发的数据进行校验,从而保证数据不被篡改。
结构
jwt包含了三个部分
- header
- payload
- signature
从结构上来看,三个部分都为base64-URL编码后,使用.
拼接后的数据:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJzdW1tZXJAdGVuY2VudCIsImFkbWluIjoiRmFsc2UiLCJleHAiOjE1MzkwNTMxODV9.
Tc8VyoGFihW_CBXnCIX6eHzbLo3WEQH-Sy65ohSWfqM
header
header包含了typ
及alg
两个部分:
{
"alg": "HS256", // 计算的方法
"typ": "JWT" // 都为JTW
}
payload
payload则包含了本次声明的数据。数据分为几个类型
- jwt协议中定义的声明
-
iss
签发人 -
sub
主题 -
aud
受众 -
exp
过期时间 -
nbf
生效时间 -
jti
jwt id
-
- 通用的数据
- 私有的数据: 用户自己定义的数据
一个payload
的样例:
{
"sub": "userinfo",
"name": "testuser",
"admin": false
}
signature
signature是对header
及payload
的数据进行签名。签名的算法可以使用HMAC RSA256
,也可以使用RSA256
.前者需要所有的使用者都拥有加密的secret
,后者的使用者则需要RSA的public key
用于数据的解密。
signature的算法通常如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
使用
从上面的定义来看,jwt本身是十分简单的。而且各种语言的JSON Parser
都很全,所以相应的库也十分全面,基本常见的语言都是支持的。而且一种语言都有非常多的轮子,具体可以参考。
这里就简单的以python作为样例,使用的是pyjwt
,文档请参考
HMAC
import jwt
key = 'secret'
encoded = jwt.encode({'sub': '123456', 'user': 'testuser'}, key, algorithm='HS256')
decoded = jwt.decode(encoded, key, algorithms='HS256')
RSA
RSA算法的用法和HMAC类似
import jwt
encoded = jwt.encode({'sub': 123456, 'user': 'testuser'}, pri, algorithm='RS256')
decoded = jwt.decode(encoded, pub)
交互
jwt适用的场景是跨域的数据认证。通常的使用流程如下:
- client需要访问服务资源,首先请求认证服务器,通过相应的参数,获取合法的
jwt token
- client在后续所有的访问中,都在请求包带上
jwt token
- 资源的服务器校验该
jwt
token是否合法。由于jwt
的特性,校验可以在服务器本地完成,而不需要去请求远端的认证服务器。
请求时,带参数的方式有多种:
-
cookies
如果认证服务器与资源服务器是同域的,则可以把jwt token放在cookies进行携带。
-
header
对于跨域的情况,则可以在请求的header中添加
Authorization: bearer <token>
字段的方式来传递。由于是在header
中添加数据,所以没有跨域的cookies无法携带的问题。 -
params
还有就是在请求的参数中增加token字段。比如
Get
方法中的url&token=xxx
的形式,或者是Post
中的请求参数
tips
使用claim校验
jwt
官方定义了几种常用的claim
,用于对数据合法性进行校验.常用的有:
-
exp: token的过期时间。
服务端或者客户端进行decode时,可以自动捕获超时错误。
jwt.encode({'exp': datetime.utcnow()}, 'secret') try: jwt.decode('JWT_STRING', 'secret', algorithms=['HS256']) except jwt.ExpiredSignatureError: # Signature has expired
nbf: token的启用时间
iss: token的签发者
aud: token的接受者
stateful & unstateful
stateful和unstateful是由jwt中数据存储的内容决定的. 如果jwt中的数据包含了所需要的全部数据, 每个client在使用数据时, 不需要再到某个服务中进行合法性校验, 则这个jwt是unstateful的, 反之, 则是stateful的.
举个例子:
{
"uid": 123456,
"exp": 1539148957,
"group": "admin",
"company": "1"
}
上面这个jwt包含了uid
, group
, company
字段. 这些数据已经足够处理后续的所有请求了. 因此, client在执行的时候, 不需要再请求一次服务器, 获取剩余的数据. 这种jwt, 可以称之为 unstateful的.
再看下面这个:
{
"uid": "sess-1234-412-x12",
"exp": 1539148957
}
这个jwt中仅仅包含了一个uid
信息, 所有的数据都没有. 在使用时, 还需要再请求一个服务获取相应的数据, 这种jwt就是stateful的.
当然, stateful的jwt, 也可以包含所有的数据.
{
"uid": 123456,
"exp": 1539148957,
"group": "admin",
"company": "1",
"tid": "123-xx-xx"
}
在使用该jwt之前, 需要首先校验该tid
是否还是合法. 一般会在一个服务器中维护一个tid的黑名单. 如果该tid属于黑名单中, 则强制废除该jwt.
安全问题
虽然token
看起来是一段加密后的数据, 而且还用了加密算法, 但实际上并不是加密的. 回顾下token
的样子,
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOjEyMzQ1NiwidXNlciI6InRlc3R1c2VyIn0.
NAHaQSXelvub7_y3XSXpofBQDuzBrGGxeJ4SAQPkYRE
token分成了三段:
- header
- payload
- token
其中, header\payload都是仅仅用base64-URL
encode后的结果. 也就是说,谁都可以做decode获取数据. 所以, jwt中一定不能包含涉密的数据,尤其是用户密码等敏感信息
性能问题
这里的性能问题, 指的是该结构带来的请求数据变多的问题. 在jwt的使用场景中, 所有的请求都需要完整带上jwt的数据. 由于payload这部分的数据仅仅是base64后的数据, 并没有做任何处理. 所以, 如果在payload中增加过多的数据, 就会导致jwt的结构变大, 从而导致请求的效率降低.
替代session
jwt
的竞争对手并不是传统的session, 而是其他的跨域认证方案, 例如SAML
以及SWT
. 但是, 也有很多人使用jwt作为session的替代品进行使用. 选择jwt的原因大多数如下:
-
平行扩容
使用session的服务在用了jwt后, 完全可以在本地对jwt发过来的数据进行校验, 从而不需要传统的session记录. 因此, 当服务快速平行扩容时, 也不会因为session记录找不到而引起问题.
-
免去session服务器
同样的, 因为jwt已经记录了用户相关的信息, 也就不需要再去session获取一次用户的数据. 从而避免session服务器成为卡点.
当然, 这两点优势也饱受质疑.
首先, 现在session大多都直接存放在redis
, memcache
等缓存中, 已经没有什么人使用本地的文件存储了. 此外, 也可以通过负载均衡来保证访问都单用户请求都落在单一服务器上.
而对于免去session服务器, 则要看如何定义jwt中的数据. 前面说过, jwt可以是unstateful或者是stateful的. 对于unstateful的jwt, 当登录服务器签发了之后, 该jwt只要在exp
之前都长期有效. 所以服务端是无法知道当前有多少用户在线. 这对于某些应用是不合适的. 而且, 废除jwt
也成为了件难事. 如果需要废除已经签发, 但仍在有效期的jwt
, 就需要添加一个黑名单
, 那就变成了stateful的jwt, 而且也需要一个集中化的服务.
结论
jwt
在2015年就已经成为RFC标准, 其token生成的设计, 很好的解决了不同组织之间相互数据信任的问题, 因而在单点认证方面得到了大规模的应用.
在单体服务中, jwt也被很多人用来作为session的替代品进行使用, 也引来了很多的讨论, 甚至是互喷. 整体上来说, 经过设计, 对于某些场景下, 由于签发的数据天然就带了可信任的token, 所以不需要再进行远端校验, 确实是能提高系统整体的吞吐率.