JWT 介绍

我们都知道,在某个网站或者app我们要保存个人信息,一般都是需要注册登录的,而登录之后,如何让服务器在之后的访问中知道,我是登录了的,而不是匿名用户呢?以前,应该通用的解决方式都是使用 cookie(浏览器) + session(服务器) 的机制来保存登录状态。也就是说,在我登录的时候的时候,服务器在 session 中保存我的登录信息,然后将我的 session标识 设置在 cookie 中返回给浏览器保存。

但是呢,随着网页功能的不断丰富以及需求的增加,cookie + session 的功能已经不能满足我们的需要了,因为这个机制存在很多弊端:

这里不能说完全的列举出 cookie + session 的所有不好,也必须强调的是不要一味否决这个方案,毕竟还是有很多优点的,例如浏览器支持良好,原生支持,不需要额外的编写代码。

为了解决不一样的需求,所以基于 token 的验证方式产生了。所谓基于 token 的验证方式就是说每个请求中都携带被签名过的 token 给服务器,这里携带可以放在请求头,也可以放在请求体,反正能传给服务器就好了。我用下图来进行对比基于 cookie 的和基于 token 的验证方式的区别。

Image-2016-04-03-183956001.png

Image-2016-04-03-184925001.png

使用基于 Token 的验证方式可以解决大部分 Cookie 的问题。而 JWT 就是基于 Token 的验证方式的一种实现,并且是自包含( Self-Contained )的,也就是说 Token 中包含必要的信息。

JWT 格式

一个简单的 JWT 的例子为:

  1. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ.ec7IVPU-ePtbdkb85IRnK4t4nUVvF2bBf8fGhJmEwSs

如果你细致得去看的话会发现其实这是一个分为 3 段的字符串,段与段之间用 点号 隔开,在 JWT 的概念中,每一段的名称分别为:

  1. Header.Payload.Signature

在字符串中每一段都是被 base64url 编码后的 JSON,其中 Payload 段可能被加密。

Header

JWT 的 Header 通常包含两个字段,分别是:typ(type) 和 alg(algorithm)。

一个简单的例子:

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

我们对他进行编码后是:

  1. >>> base64.b64encode(json.dumps({"alg":"HS256","typ":"JWT"}))
  2. 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9'

Payload

JWT 中的 Payload 其实就是真实存储我们需要传递的信息的部分,例如正常我们会存储些用户 ID、用户名之类的。此外,还包含一些例如发布人、过期日期等的元数据。

但是,这部分和 Header 部分不一样的地方在于这个地方可以加密,而不是简单得直接进行 BASE64 编码。但是这里我为了解释方便就直接使用 BASE64 编码,需要注意的是,这里的 BASE64 编码稍微有点不一样,切确得说应该是 Base64UrlEncoder,和 Base64 编码的区别在于会忽略最后的 padding(=号),然后 ‘-‘ 会被替换成’_’。

举个例子,例如我们的 Payload 是:

  1. {"user_id":"zhangsan"}

那么直接 Base64 的话应该是:

  1. >>> base64.urlsafe_b64encode('{"user_id":"zhangsan"}')
  2. 'eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ=='

然后去掉 = 号,最后应该是:

  1. 'eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ'

Signature

签名的验证方式依赖于算法,可以是密钥加密(加解密用同一个密钥),也可以是非对称加解密(加密用私钥,解密用公钥),这里就介绍一下密钥加密的方式。

Signature 部分其实就是对我们前面的 Header 和 Payload 部分进行签名,保证 Token 在传输的过程中没有被篡改或者损坏,签名的算法也很简单,但是,为了加密,所以除了 Header 和 Payload 之外,还多了一个密钥字段,完整算法为:

  1. Signature = HMACSHA256(
  2. base64UrlEncode(header) + "." +
  3. base64UrlEncode(payload),
  4. secret)

还是以前面的例子为例,

  1. base64UrlEncode(header) =》 eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9
  2. base64UrlEncode(payload) =》 eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ

secret 就设为:”secret”, 那最后出来的签名应该是:

  1. >>> import hmac
  2. >>> import hashlib
  3. >>> import base64
  4. >>> header = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9"
  5. >>> payload = "eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ"
  6. >>> msg = "{}.{}".format(header, payload)
  7. >>> base64.urlsafe_b64encode(hmac.new('secret', msg=msg.strip('='), digestmod=hashlib.sha256).digest())
  8. 'ec7IVPU-ePtbdkb85IRnK4t4nUVvF2bBf8fGhJmEwSs='

将上面三个部分组装起来就组成了我们的 JWT token 了,所以我们的 {'user_id': 'zhangsan'} 的 token 就是:

  1. eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ.eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ.ec7IVPU-ePtbdkb85IRnK4t4nUVvF2bBf8fGhJmEwSs

JWT 应用

JWT 的应用场景有很多,最简单的应用场景就是 REST API 的鉴权了。在实战篇,我们就以 Flask 为例尝试一个 JWT Token 鉴权的例子。

JWT 实战

这个实战是以 Python 为例,依赖于 Flask 和 JWT 的第三方库。过程就不多说了,如果对 Flask 不熟悉的话,可以简单得看一下官方的介绍熟悉一下,应该会很简单入门的。

安装依赖:

  1. [root@liqiang.io]# pip install flask
  2. [root@liqiang.io]# pip install jwt

创建项目

  1. [root@liqiang.io]# vim app.py

然后在里面添加一下内容:

  1. import jwt
  2. import logging
  3. from jwt.exceptions import ExpiredSignatureError, DecodeError
  4. from flask import Flask, request, make_response, g, jsonify
  5. from datetime import datetime, timedelta
  6. app = Flask(__name__)
  7. @app.before_request
  8. def process_token():
  9. token = request.cookies.get(
  10. 'jwt',
  11. request.headers.get('Authorization', 'a.b.c'))
  12. try:
  13. if request.path == "/api/auth":
  14. return None
  15. user_info = jwt.decode(token, 'secret')
  16. g.user_info = user_info
  17. except ExpiredSignatureError as e:
  18. logging.warning(e)
  19. response = make_response('Your JWT has expired')
  20. response.status_code = 401
  21. return response
  22. except DecodeError as e:
  23. logging.warning(e)
  24. response = make_response('Your JWT is invalid')
  25. response.status_code = 401
  26. return response
  27. @app.route('/api/jwt', methods=['GET'])
  28. def get_jwt():
  29. # print g.user_info
  30. return jsonify(**g.user_info)
  31. @app.route('/api/auth', methods=['GET'])
  32. def login():
  33. token = jwt.encode({'username': 'Renaissance Dev',
  34. 'role': 'super_admin',
  35. 'exp': datetime.utcnow() + timedelta(minutes=app.config.get('HAPYAK_JWT_LIFETIME', 60)),
  36. 'iat': datetime.utcnow()},
  37. app.config.get('JWT_KEY', 'secret'))
  38. response = make_response(token)
  39. response.set_cookie('jwt', token)
  40. return response
  41. app.run(debug=True)

然后保存为 app.py, 使用以下命令请服务器

  1. [root@liqiang.io]# python app.py

接下来编写一下客户端:
首先是授权:

  1. >>> url = "http://localhost:5000"
  2. >>> resp = requests.get("{}/api/auth".format(url))
  3. >>> print resp.text
  4. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlJlbmFpc3NhbmNlIERldiIsImlhdCI6MTQ1OTc2ODU1MSwicm9sZSI6InN1cGVyX2FkbWluIiwiZXhwIjoxNDU5NzcyMTUxfQ.mOJwfxhADCfC89uUytXCS7AJSkkmPPSU0z9lcdj5ZAE

然后使用这个 Token 去访问api,

  1. >>> resp = requests.get("{}/api/jwt".format(url),
  2. headers={'Authorization': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlJlbmFpc3NhbmNlIERldiIsImlhdCI6MTQ1OTc2ODQ0Mywicm9sZSI6InN1cGVyX2FkbWluIiwiZXhwIjoxNDU5NzcyMDQzfQ.rR4uqNe77F4RkvbSGSVWCa5WBPfcz-tav9r_AAIRMhk'})
  3. >>> print resp.text
  4. {
  5. "exp": 1459772043,
  6. "iat": 1459768443,
  7. "role": "super_admin",
  8. "username": "Renaissance Dev"
  9. }

这就是一个简单的实例了,希望对大家有所帮助。

扩展知识

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 实现。

参考内容

  1. JWT Introduction
  2. 基于Token的认证和基于声明的标识
  3. Flask
  4. PyJWT
  5. What are JWT, JWS, JWE, JWK, and JWA?