JWT 介绍
我们都知道,在某个网站或者app我们要保存个人信息,一般都是需要注册登录的,而登录之后,如何让服务器在之后的访问中知道,我是登录了的,而不是匿名用户呢?以前,应该通用的解决方式都是使用 cookie(浏览器) + session(服务器) 的机制来保存登录状态。也就是说,在我登录的时候的时候,服务器在 session
中保存我的登录信息,然后将我的 session标识
设置在 cookie
中返回给浏览器保存。
但是呢,随着网页功能的不断丰富以及需求的增加,cookie + session 的功能已经不能满足我们的需要了,因为这个机制存在很多弊端:
- 跨域: cookies 在跨域场景表现并不好
- 状态: 需要维护 session,如果是分布式的话,还需要多机共享 session 机制
- 移动支持: 在移动端使用
cookie
不是一个简单方便的方案 - CSRF: 基于 cookie 的机制很容易被 CSRF
- 性能: 查询 session 信息可能会有数据库查询操作
这里不能说完全的列举出 cookie + session
的所有不好,也必须强调的是不要一味否决这个方案,毕竟还是有很多优点的,例如浏览器支持良好,原生支持,不需要额外的编写代码。
为了解决不一样的需求,所以基于 token
的验证方式产生了。所谓基于 token
的验证方式就是说每个请求中都携带被签名过的 token
给服务器,这里携带可以放在请求头,也可以放在请求体,反正能传给服务器就好了。我用下图来进行对比基于 cookie
的和基于 token
的验证方式的区别。
使用基于 Token 的验证方式可以解决大部分 Cookie 的问题。而 JWT 就是基于 Token 的验证方式的一种实现,并且是自包含( Self-Contained )的,也就是说 Token 中包含必要的信息。
JWT 格式
一个简单的 JWT 的例子为:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ.ec7IVPU-ePtbdkb85IRnK4t4nUVvF2bBf8fGhJmEwSs
如果你细致得去看的话会发现其实这是一个分为 3 段的字符串,段与段之间用 点号 隔开,在 JWT 的概念中,每一段的名称分别为:
Header.Payload.Signature
在字符串中每一段都是被 base64url
编码后的 JSON,其中 Payload
段可能被加密。
Header
JWT 的 Header 通常包含两个字段,分别是:typ
(type) 和 alg
(algorithm)。
- typ:token的类型,这里固定为 JWT
- alg:使用的 hash 算法,例如:HMAC SHA256 或者 RSA
一个简单的例子:
{
"alg": "HS256",
"typ": "JWT"
}
我们对他进行编码后是:
>>> base64.b64encode(json.dumps({"alg":"HS256","typ":"JWT"}))
'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9'
Payload
JWT 中的 Payload 其实就是真实存储我们需要传递的信息的部分,例如正常我们会存储些用户 ID、用户名之类的。此外,还包含一些例如发布人、过期日期等的元数据。
但是,这部分和 Header 部分不一样的地方在于这个地方可以加密,而不是简单得直接进行 BASE64 编码。但是这里我为了解释方便就直接使用 BASE64 编码,需要注意的是,这里的 BASE64 编码稍微有点不一样,切确得说应该是 Base64UrlEncoder,和 Base64 编码的区别在于会忽略最后的 padding(=号),然后 ‘-‘ 会被替换成’_’。
举个例子,例如我们的 Payload 是:
{"user_id":"zhangsan"}
那么直接 Base64 的话应该是:
>>> base64.urlsafe_b64encode('{"user_id":"zhangsan"}')
'eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ=='
然后去掉 = 号,最后应该是:
'eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ'
Signature
签名的验证方式依赖于算法,可以是密钥加密(加解密用同一个密钥),也可以是非对称加解密(加密用私钥,解密用公钥),这里就介绍一下密钥加密的方式。
Signature 部分其实就是对我们前面的 Header 和 Payload 部分进行签名,保证 Token 在传输的过程中没有被篡改或者损坏,签名的算法也很简单,但是,为了加密,所以除了 Header 和 Payload 之外,还多了一个密钥字段,完整算法为:
Signature = HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
还是以前面的例子为例,
base64UrlEncode(header) =》 eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9
base64UrlEncode(payload) =》 eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ
secret 就设为:”secret”, 那最后出来的签名应该是:
>>> import hmac
>>> import hashlib
>>> import base64
>>> header = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9"
>>> payload = "eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ"
>>> msg = "{}.{}".format(header, payload)
>>> base64.urlsafe_b64encode(hmac.new('secret', msg=msg.strip('='), digestmod=hashlib.sha256).digest())
'ec7IVPU-ePtbdkb85IRnK4t4nUVvF2bBf8fGhJmEwSs='
将上面三个部分组装起来就组成了我们的 JWT token 了,所以我们的 {'user_id': 'zhangsan'}
的 token 就是:
eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ.eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ.ec7IVPU-ePtbdkb85IRnK4t4nUVvF2bBf8fGhJmEwSs
JWT 应用
JWT 的应用场景有很多,最简单的应用场景就是 REST API 的鉴权了。在实战篇,我们就以 Flask 为例尝试一个 JWT Token 鉴权的例子。
JWT 实战
这个实战是以 Python 为例,依赖于 Flask 和 JWT 的第三方库。过程就不多说了,如果对 Flask 不熟悉的话,可以简单得看一下官方的介绍熟悉一下,应该会很简单入门的。
安装依赖:
[root@liqiang.io]# pip install flask
[root@liqiang.io]# pip install jwt
创建项目
[root@liqiang.io]# vim app.py
然后在里面添加一下内容:
import jwt
import logging
from jwt.exceptions import ExpiredSignatureError, DecodeError
from flask import Flask, request, make_response, g, jsonify
from datetime import datetime, timedelta
app = Flask(__name__)
@app.before_request
def process_token():
token = request.cookies.get(
'jwt',
request.headers.get('Authorization', 'a.b.c'))
try:
if request.path == "/api/auth":
return None
user_info = jwt.decode(token, 'secret')
g.user_info = user_info
except ExpiredSignatureError as e:
logging.warning(e)
response = make_response('Your JWT has expired')
response.status_code = 401
return response
except DecodeError as e:
logging.warning(e)
response = make_response('Your JWT is invalid')
response.status_code = 401
return response
@app.route('/api/jwt', methods=['GET'])
def get_jwt():
# print g.user_info
return jsonify(**g.user_info)
@app.route('/api/auth', methods=['GET'])
def login():
token = jwt.encode({'username': 'Renaissance Dev',
'role': 'super_admin',
'exp': datetime.utcnow() + timedelta(minutes=app.config.get('HAPYAK_JWT_LIFETIME', 60)),
'iat': datetime.utcnow()},
app.config.get('JWT_KEY', 'secret'))
response = make_response(token)
response.set_cookie('jwt', token)
return response
app.run(debug=True)
然后保存为 app.py, 使用以下命令请服务器
[root@liqiang.io]# python app.py
接下来编写一下客户端:
首先是授权:
>>> url = "http://localhost:5000"
>>> resp = requests.get("{}/api/auth".format(url))
>>> print resp.text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlJlbmFpc3NhbmNlIERldiIsImlhdCI6MTQ1OTc2ODU1MSwicm9sZSI6InN1cGVyX2FkbWluIiwiZXhwIjoxNDU5NzcyMTUxfQ.mOJwfxhADCfC89uUytXCS7AJSkkmPPSU0z9lcdj5ZAE
然后使用这个 Token 去访问api,
>>> resp = requests.get("{}/api/jwt".format(url),
headers={'Authorization': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlJlbmFpc3NhbmNlIERldiIsImlhdCI6MTQ1OTc2ODQ0Mywicm9sZSI6InN1cGVyX2FkbWluIiwiZXhwIjoxNDU5NzcyMDQzfQ.rR4uqNe77F4RkvbSGSVWCa5WBPfcz-tav9r_AAIRMhk'})
>>> print resp.text
{
"exp": 1459772043,
"iat": 1459768443,
"role": "super_admin",
"username": "Renaissance Dev"
}
这就是一个简单的实例了,希望对大家有所帮助。
扩展知识
exp
在 JWT 的字段中,其实没有对顶 exp (Expiration) 字段,但是约定俗成经常有人使用这个字段用于表示 Token 的过期时间。
kid
在 Header 中有一个可选的字段叫做 KID
,名字是 key identifier
的简写,他的作用是当你可能有多个公私钥加密 JWT Token 时,可以用这个字段标识这个 JWT Token 是哪对公私钥加解密的,这在某些场景下很有用。例如我曾经在公司的某个项目中就使用了这个功能,场景是我们有多个 IDC,每个 IDC 之间的网络条件限制是严格的,所以 JWT 很适合,因为只需要公钥就可以验证 Token,无需进行网络鉴定访问;同时,出于安全考虑,我们每个 IDC 都是放置独立的公私钥,这样可以防止不同 IDC 之间的公私钥泄露导致全局公私钥泄露。
所以在这种场景下,多个公私钥就是一个实际的场景了。
JWKS
JSON(JavaScript 对象符号)是一种基于文本的、独立于语言的格式,容易被人类和机器理解。
JOSE(Javascript Object Signing and Encryption)是一个框架,用于促进任何两方之间的安全传输要求。它的规范提供了一种对任何内容进行加密的一般方法,不一定是在 JSON 中。然而,它是建立在 JSON 上的,便于在网络应用中使用。
JWT 通过 JWS 或者 JWE 实现。
- JWS:表示借助 JSON 数据结构用数字签名或基于哈希的消息验证码(HMAC)来保护的内容。它用 JWS 签名对 JWS 头和 JWS 有效载荷进行加密保护。这三者的编码字符串使用类似于 JWT 的点进行连接。使用的标识符和算法在 JSON 网络算法规范中规定。
- 所以
alg
参数必须存在
- 所以
- JWE:能够对令牌进行加密,以便只有预期的接收者能够阅读它。它规范了在 JSON 数据结构中表示编码数据的方式。加密的有效载荷的表示方法可以是 JWE 紧凑序列化或 JWE JSON 序列化。
- JWK(JWKS):JWK 是一个 JSON 结构,用椭圆曲线或 RSA 算法将一组公钥表示为 JSON 对象。公钥表示法可以帮助用相应的私钥验证签名。
kid
就用于在 JWK 中找到正确的哪个公钥
- JWA:JWA 规范主要侧重于列举 JWS、JWK 和 JWE 所需的算法。它还描述了针对这些算法和密钥类型的操作。