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 的例子为:

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ.ec7IVPU-ePtbdkb85IRnK4t4nUVvF2bBf8fGhJmEwSs

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

    Header.Payload.Signature

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

Header

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

一个简单的例子:

    {
      "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
    >>> dig = hmac.new('secret',     >>> msg="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ", 
               digestmod=
    >>> base64.b64encode(dig.digest())
    'ec7IVPU-ePtbdkb85IRnK4t4nUVvF2bBf8fGhJmEwSs='

将上面三个部分组装起来就组成了我们的 JWT token 了,所以我们的

    {'user_id': 'zhangsan'}

的 token 就是:

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ.ec7IVPU-ePtbdkb85IRnK4t4nUVvF2bBf8fGhJmEwSs

JWT 应用

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

JWT 实战

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

安装依赖:

    pip install flask
    pip install jwt

创建项目

    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, 使用以下命令请服务器

    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"
    }

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

参考内容

  1. JWT Introduction
  2. 基于Token的认证和基于声明的标识
  3. Flask
  4. PyJWT