23.7.4. 区块链时代与Token登录

2. 为什么是Token

Cookie/Session机制的局限性,对于Token机制而言并不存在。因为Token信息除了可以存储在Cookie中,也可以储存在Local Storage中。

2.1 什么是Token

从各个终端与服务端进行数据交互的身份验证的字符串,就是Token。Token被翻译为“令牌”,顾名思义,其作用就是给每一次需要身份验证的从客户端向服务端发送的数据请求一张代表了权限的“令牌”。

../../../_images/django_token202101.png

2.2 基于区块链技术发展中Token的技术展望

关于Token的技术展望有三点:

(1)消灭假货。基于区块链技术,每一个品牌的每一件商品,都可以有全世界唯一的商品标识,而且还可以非常低的成本验证这些商品的信息。目前市面上的二维码认证很容易伪造,一些不良的商家,只要复制一件正品的二维码,即可造出无数贴有正品二维码标识的假货。

(2)消灭注册。当网络实名制彻底普及以后,完全可以通过区块链技术,让每个人都可以使用同一个账号登录任何一个网站,不需要像现在这样,每下载一个新的应用程序,都要通过手机号注册一个账号,如果长时间不使用,还容易将账号和密码忘记。人脸识别技术和区块链技术的配合,说不定可以使“登录密码”这种验证方式成为历史。

(3)消灭盗版。目前所有打击盗版的成本,都由支持正版的人在承担,这显然并不合理。区块链技术可以帮助那些支持正版的人分享利润,同时区块链技术也有利于打击盗版。

2.3 Django实现Token登录的业务模式

2.3.1 Django REST framework的Token生成

通过安装和配置Django REST framework及其依赖包,改造项目demo5,实现将demo5的登录机制换成Token模式。生成Token的步骤如下:

(1)在demo5中安装Django REST framework及其依赖包markdown和django-filter。

pip install Djangorestframework markdown Django-filter -i "https://pypi.doubanio.com/simple/"

(2)在settings.py中添加注册代码:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app1.apps.App1Config',
    'rest_framework',
    'rest_framework.authtoken'
]

(3)打开终端,执行数据更新命令:

python manage.py makemigrations
python manage.py migrate

执行数据更新命令,数据库中会自动生成一张authtoken_token表.

(4)打开终端运行创建超级用户命令:

python manage.py createsuperuser

然后输入用户名root,邮箱1@1.com,密码aaaa1111。在demo5的用户表auth_user中,生成了一条记录,password被自动加密了

用户表才是Django项目在建立时自动生成的用户表,这张表包含很多字段,而且对密码字段也有加密处理,可以说是一张功能相对比较强大的表。

(5)在urls.py中配置Token登录的路由:

from django.contrib import admin
from django.urls import path
from rest_framework.authtoken import views

urlpatterns = [
    path('api-token-auth/',views.obtain_auth_token),
]

(6)运行项目,然后使用Postman模拟网络请求,采用post的方式,向http://127.0.0.1:8000/api-token-auth/提交用户名和密码,将会返回Token信息:

../../../_images/django_token202102.png
{
    "token": "9246c96b4ac9abb127dcc4bd2a3ab954ed33e9dc"
}

当我们刷新Database中的authtoken_token表,可以看到生成的Token记录已经存在。

2.3.2 Django REST framework的Token认证

我们已经通过对demo5的改造,成功生成并且获取到了Token,接下来开发Token认证的功能,步骤如下:

(1)在settings.py中添加配置代码:

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated', #必须有
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.TokenAuthentication',
    )
}

注意:上述代码中,在settings中不但要加入认证的配置代码,还要加入权限的配置代码,如果不加入权限的配置代码,那么认证代码将无法阻止未认证用户获取到本应该只有已认证的用户才可以获取到的数据信息,这一点与Django RESTframework的官方文档存在差异,有可能是因为版本问题而产生的Bug。

(2)将app01/views.py中的代码重写为:

from .models import Administrator
from django.shortcuts import render, redirect, HttpResponse
from rest_framework.views import APIView


# Create your views here.
class IndexView(APIView):
    """
    首页
    """

    # authentication_classes = []
    # permission_classes = []
    def get(self, request):
        # print(request)
        return HttpResponse('首页')

(3)将urls.py中的代码重写为:

from django.contrib import admin
from django.urls import path
from rest_framework.authtoken import views
from app1.views import IndexView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api-token-auth/', views.obtain_auth_token),
    path('index/', IndexView.as_view(), name='index'),
]

(4)运行项目demo5,然后使用Postman在协议头中加入键值对:

{"key":"Authorization","value":"Token 9246c96b4ac9abb127dcc4bd2a3ab954ed33e9dc"}

注意:Token与字符串之间有一个空格。

(5)如果Token信息不正确,则会返回以下内容

{
    "detail": "Authentication credentials were not provided."
}

Token正确返回信息如下:

首页

(6)取消认证限制。

综上可知,由Django REST framework所完成的Token认证流程,是作用于整个项目全局的,也就是说,任何一个数据请求,都会被要求携带Token。

但是获取Token需要进行数据请求,在没有登录之前,用户根本无法获得Token,所以我们至少要让已登录的数据请求不受Token的认证限制

要完成这个需求非常简单,在views.py中编写代码如下:

from django.shortcuts import render, redirect, HttpResponse
from rest_framework.views import APIView


# Create your views here.
class IndexView(APIView):
    """
    首页
    """
    authentication_classes = []
    permission_classes = []

    def get(self, request):
        # print(request)
        return HttpResponse('首页')

这时,再启动项目,即使没有Token,也可以获取首页内容了。

2.3.3 Django REST framework的Token的局限性

我们再来看一下Django REST framework所自建的Token表,可以发现这个表格只有三个字段(不算ID字段):记录Token内容的key字段,记录生成Token时间的created字段,以及外键user_id字段。

很显然,缺少了一个Token的有效期时间字段。从原理上来说,有效期时间字段并没有存在的必要,但是从网络安全的角度上来看,这个字段却是必不可少的。试想,如果一个Token字符串没有有效期限制,只要网络请求被抓包,被黑客获取了一条Token,那么与获取到用户的账号和密码是没有区别的。所以,Django RESTframework的Token,第一个局限性就是其自建的Token表缺少记录有效期时间的字段。

第二个局限性表现在不利于分布式部署或多个系统使用一套验证,Token表只能放在一台服务器上,如果每一次数据请求都要查询一次数据库的整个用户表,那么对于服务器来说将是很大的消耗。试想一下,假如一个平台有四五亿用户,用户任何一次点赞的操作,都要在四五亿数量级的数据表中完成一次查询,那将是一件多么麻烦的事情啊!

使用Json Web Token机制,便可以解决这些问题。

2.3.4 Json Web Token的原理

Json Web Token,简称JWT,在如今的技术圈内,算是鼎鼎大名了。可以说所有的前后端分离项目中,不论是使用Python、Java、PHP还是C#开发的网站,大部分都是使用JWT进行登录验证的。

JWT的生命周期如下

../../../_images/django_token_jwt.png

(1)用户在前端通过账号和密码进行登录操作,将身份信息发送到后端服务器进行身份验证。

(2)如果后端服务器通过了身份验证,则会将一部分身份信息通过非对称加密生成JWT,返回给前端。

(3)前端获取到JWT之后,将JWT保存在本地。

(4)从前端向后端发送数据请求,都携带JWT。

(5)后端验证JWT,如果通过验证,就返回请求的数据;如果没通过,则返回错误提示。

JWT的数据结构是很长的一段字符串,使用.将其分为3个部分,依次如下:

Header(头部)
Payload(负载)
Signature(签名)

虽然JWT会因为字符串很长而导致自动折行,但是JWT本身就是一行。

2.3.5 JWT在Django中的应用

(1)新建Django项目,命名为demo5_jwt,新建App命名为app01。

(2)安装Django REST framework及其依赖包markdown和Django-filter:

pip install Djangorestframework markdown Django-filter -i "https://pypi.doubanio.com/simple/"

(3)在settings.py中加入注册代码:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app1.apps.App1Config',
    'rest_framework'
]

(4)安装JWT依赖包:

pip install djangorestframework-jwt -i "https://pypi.doubanio.com/simple/"

(5)在settings.py中追加配置相关代码:

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated', #必须有
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    )
}

import datetime
JWT_AUTH = {
    # 指明token的有效期
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
}

(6)在urls.py中配置JWT的路由代码:

from django.contrib import admin
from django.urls import path
from app1.views import IndexView
from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
    path('admin/', admin.site.urls),
    # jwt的认证接口
    path('jwt-token-auth/', obtain_jwt_token),
    path('index/', IndexView.as_view(), name='index')
]

(7)执行数据更新命令:

python manage.py makemigrations
python manage.py migrate

(8)打开终端运行创建超级用户命令:

python manage.py createsuperuser

生成超级用户,输入用户名root,密码root2222。

(9)运行项目,使用Postman以post的方式,向 http://127.0.0.1:8000/jwt-token-auth/提交

{
    "username":"root"
    "password":"root2222"
}

返回JWT:

{
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6InJvb3QiLCJleHAiOjE2MTg2NjUxMjMsImVtYWlsIjoiMUAxLmNvbSJ9.XPQuGlw6tRHGW0Oa-jb--Y4SSaYsUUBGRKQtLgZfoCc"
}

结果如图

../../../_images/django_token_jwt002.png

(10)JWT的身份验证。在app01/views.py中编写身份认证视图类:

from django.shortcuts import render, redirect, HttpResponse
from rest_framework.views import APIView


# Create your views here.
class IndexView(APIView):
    """
    首页
    """

    # authentication_classes = []
    # permission_classes = []
    def get(self, request):
        print(request)
        return HttpResponse('首页')

在urls.py中增加路由代码:

from django.contrib import admin
from django.urls import path
from app1.views import IndexView
from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
    path('admin/', admin.site.urls),
    # jwt的认证接口
    path('jwt-token-auth/', obtain_jwt_token),
    path('index/', IndexView.as_view(), name='index')
]

然后运行项目,如图所示,使用Postman以get的方式,在头文件内添加键值对:

{
"Authorization":"JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6InJvb3QiLCJleHAiOjE2MTg2NjU1NjYsImVtYWlsIjoiMUAxLmNvbSJ9.vj5XW1D9MJYBF76eZsB2HLEbKnnkgiZvsIdmUyxtDmE"
}

(5)如果Token信息不正确,则会返回以下内容

{
    "detail": "Error decoding signature."
}

Token信息正确返回

首页

身份验证已通过,获取到了“首页”数据。至此,完成了JWT在Django项目里的应用。