一、DRF的token基本使用
1.DRF的token登录原理
基于DRF的前后端分离登录与单独使用Django登录的原理不同,不再需要CSRF验证,DRF提供了许多开箱即用的身份验证方案,并且还允许实现自定义方案。身份验证始终在视图的最开始处,在进行权限和限制检查之前以及在允许任何其他代码进行之前运行。
身份验证方案始终定义为类列表,DRF框架尝试对列表中的每个类进行身份验证,并使用成功进行身份验证的第一个类的返回值设置request.user和request.auth。
在使用前,需要在settings.py中进行配置:
# DRF配置 REST_FRAMEWORK = { 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', ] }
DRF提供4种认证类,包括BasicAuthentication、TokenAuthentication、SessionAuthentication和RemoteUserAuthentication:
BasicAuthentication机制使用HTTP基本身份验证,该身份针对用户的用户名和密码进行了签名,在实际开发中一般仅适用于测试;
TokenAuthentication身份验证方案使用基于令牌的简单HTTP身份验证方案,适用于客户端-服务器设置,例如本地台式机和移动客户端,适用于前后端分离项目,也是本项目中身份验证的重点;
SessionAuthentication机制常见于浏览器,因为浏览器可以自动设置cookie,并将session和cookie传到浏览器,在后端分离项目中较少见;
对于RemoteUserAuthentication,通过此身份验证方案,可以将身份验证委派给Web服务器,要求服务器设置REMOTE_USER环境变量。
综上,选择TokenAuthentication,即选择Token的认证方式,需要在settings.py中添加到INSTALLED_APPS:
INSTALLED_APPS = [ 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'apps.users.apps.UsersConfig', 'goods.apps.GoodsConfig', 'trade.apps.TradeConfig', 'user_operation.apps.UserOperationConfig', 'DjangoUeditor', 'xadmin', 'crispy_forms', 'django.contrib.admin', 'rest_framework', 'django_filters', 'corsheaders', 'rest_framework.authtoken' ]
加入之后,执行makemigrations
和migrate
命令进行数据映射,查看数据库可以看到生成新表authtoken_token,其表结构如下:
+---------+-------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +---------+-------------+------+-----+---------+-------+ | key | varchar(40) | NO | PRI | NULL | | | created | datetime(6) | NO | | NULL | | | user_id | int(11) | NO | UNI | NULL | | +---------+-------------+------+-----+---------+-------+ 3 rows in set (0.01 sec)
其中,user_id是一个外键,指向users_userprofile表,表中的key(即token)和user之间具有一对一的关系。
但是在创建用户后并不会自动创建token,而是需要自己创建,可以使用HTTP请求模拟发送工具进行发送参数创建,使用Postman演示如下:
显然,通过携带数据访问http://127.0.0.1:8000/api-token-auth/,生成了当前用户的token并获取到,在生成token的同时,自动将生成的token和当前用户存入表authtoken_token中,如下:
+------------------------------------------+----------------------------+---------+ | key | created | user_id | +------------------------------------------+----------------------------+---------+ | 236de0331b3e5a89665771f9aaff9be720cbba04 | 2020-07-27 08:29:47.306382 | 1 | +------------------------------------------+----------------------------+---------+ 1 row in set (0.01 sec)
现在已经获取到了token,就可以使用了。为了使客户端进行身份验证,令牌密钥应包含在Authorization HTTP标头中。密钥应以字符串文字Token作为前缀,并用空格分隔两个字符串。
此时再使用获取到的Token请求商品数据如下:
显然,获取到了商品数据,可以体会到token比session的应用更方便,但是使用token验证也存在一些问题:
请求服务器生成的token只存在于一台被请求的服务器中,如果是分布式系统,为了数据一致,则需要将该服务器的数据同步到其他服务器,增加了操作和维护难度;
token没有过期时间,显然这对于验证来说并不完善。
2.viewsets设置认证类
在使用token认证时,如果token不正确,则会抛出异常,并且如果对于本来不需要认证即可访问的公开数据要是再需要正确的token才能访问的话,就会降低项目的友好性,此时可以对token不采用全局设置,而在View中单独设置,settings.py如下:
# DRF配置 REST_FRAMEWORK = { 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', ] }
为了测试,在apps/goods/views.py中进行配置如下:
class GoodsListViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): '''商品列表页,并实现分页、搜索、过滤、排序''' queryset = Goods.objects.filter(is_delete=False) serializer_class = GoodsSerializer pagination_class = GoodsPagination authentication_classes = [TokenAuthentication] filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filter_class = GoodsFilter search_fields = ['name', 'goods_brief', 'goods_desc'] ordering_fields = ['sold_num', 'shop_price']
此时再请求http://127.0.0.1:8000/goods/,request.user即为当前用户admin。
当然在实际项目中由于goods是公开数据,因此不需要设置authentication_classes配置验证,还是为:
class GoodsListViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): '''商品列表页,并实现分页、搜索、过滤、排序''' queryset = Goods.objects.filter(is_delete=False) serializer_class = GoodsSerializer pagination_class = GoodsPagination filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filter_class = GoodsFilter search_fields = ['name', 'goods_brief', 'goods_desc'] ordering_fields = ['sold_num', 'shop_price']
二、JSON Web Token登录
1.JWT原理
JSON Web Token (简称JWT),是目前最流行的跨域身份验证解决方案,使用基于Token的身份验证方法,在服务端不需要存储用户的登录记录。
在之前已经测试过,传统的前后端分离项目中,前端登录,后端生成对应的token信息并保存到session或数据库中。但是如果存在XSS漏洞,就可能存在cookie泄漏、信息不安全的问题。如果将验证信息保存到数据库中,会增加数据库的操作和存储开销;如果存到session中,又会增大服务器存储压力;如果采用加密算法来对用户信息加密得到token,则很容易被解密而泄漏用户信息。
JWT是一种开放的、行业标准的RFC7519方法,用于在双方之间安全地表示声明,JWT是凭据,使用加密算法加密,可以授予对资源的访问权限,具有简洁、自包含的特点。
JWT消息组成包含三部分:
Header头部
包含token类型和加密算法,并使用base64编码。
Payload负载
存放信息,包含用户id、签发者、面向的用户、接收方、签发时间和过期时间等,也通过base编码。
Signature签名
因为Header和Payload信息可以通过解码获取到具体信息并伪造信息进行请求,因此需要通过签名来进行识别,其使用Header中指定的算法对Header和Payload信息以及提供的密钥进行签名,来保证安全性。
相比于session,JWT将登录信息保存到本地,减轻了服务器的存储压力,并且可应用于单点登录。
2.使用JWT完成用户认证
在DRF中使用JWT需要先安装依赖库,直接在虚拟环境中使用命令pip install djangorestframework-jwt
安装即可。
安装后,需要在settings.py中进行配置:
# DRF配置 REST_FRAMEWORK = { 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', ] }
JSONWebTokenAuthentication可以对用户发送来的数据Token进行验证,并取出其中的user。
还需要在users.py中配置路由:
from django.conf.urls import url, include from django.views.static import serve from rest_framework.documentation import include_docs_urls from rest_framework.routers import DefaultRouter from rest_framework.authtoken import views from rest_framework_jwt.views import obtain_jwt_token import xadmin from .settings import MEDIA_ROOT from goods.views import GoodsListViewSet, CategoryViewSet # Create a router and register our viewsets with it. router = DefaultRouter() # 配置goods的路由 router.register(r'goods', GoodsListViewSet, basename='goods') # 配置categories的路由 router.register(r'categorys', CategoryViewSet, basename='categorys') urlpatterns = [ url(r'^xadmin/', xadmin.site.urls), url(r'^media/(?P<path>.*)$', serve, {'document_root':MEDIA_ROOT}), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), # 商品列表页 url(r'^', include(router.urls)), # 文档路由 url(r'docs/', include_docs_urls(title='生鲜电商')), # DRF自带认证路由 url(r'^api-token-auth/', views.obtain_auth_token, name='api_token_auth'), # JWT认证路由 url(r'^jwt-auth/', obtain_jwt_token), ]
现对JWT进行获取和验证测试如下: