一、实现登录验证
1.创建User和Token表
User表用作用户名密码认证,Token表用于存放用户每次成功登陆后的随机Token。
在models.py中添加以下两张表:
#用户表
classUser(models.Model):
username= models.CharField(max_length=32)
password= models.CharField(max_length=32)#token表
classToken(models.Model):
user= models.OneToOneField("User", on_delete=models.CASCADE)
token= models.CharField(max_length=128)
执行命令,生成数据库表:
python manage.py makemigrations
python manage.py migrate
2.实现登录验证操作
添加路由条目:
urlpatterns =[
path('admin/', admin.site.urls),
re_path('^publishes/$', views.PublishView.as_view(), name="publish"),
re_path('^publishes/(?P\d+)/$', views.PublishDetailView.as_view(), name="publishdetail"),
re_path('^books/$', views.BookView.as_view(), name="book"),
re_path('^books/(?P\d+)/$', views.BookDetailView.as_view(), name="bookdetail"),
re_path('^authors/$', views.AuthorViewSet.as_view({"get": "list", "post": "create"}), name="author"),
re_path('^authors/(?P\d+)/$', views.AuthorViewSet.as_view(
{"get": "retrieve", "put": "update", "patch": "partial_update", "delete": "destroy"}), name="authordetail"),
re_path('^login/$', views.LoginView.as_view(), name="login"),
]
实现视图类LoginView:
#导入User和Token的model类
from .models importUserfrom .models importToken#生成一个随机token,username和ctime的MD5加密值
defget_random_str(user):importhashlibimporttime#获取当前时间
ctime =str(time.time())#username的md5
md5 = hashlib.md5(bytes(user, encoding='utf-8'))#加上ctime
md5.update(bytes(ctime, encoding='utf-8'))returnmd5.hexdigest()classLoginView(APIView):defpost(self, request):
res= {'code': 1000, "msg": None}try:#从post请求中获取用户提交的用户名和密码
username = request.data.get("username")
password= request.data.get("password")#判断数据库中的数据是否匹配
user_obj = User.objects.filter(username=username, password=password).first()#如果不匹配,返回登录失败
if notuser_obj:
res['code'] = 1001res['msg'] = "用户名或密码错误"
else:#如果匹配,则生成一个随机token
token =get_random_str(username)#如果token已经存在,则更新,如果不存在,则创建
Token.objects.update_or_create(user=user_obj, defaults={'token': token})
res["token"] =tokenexceptException as e:
res['code'] = 1002res['msg'] =ereturn HttpResponse(json.dumps(res))
二、实现token认证
1.实现token认证
要实现认证,只需要在需要认证的视图类中添加 authentication_classes 列表。restframe认证组件会自动去该列表中寻找认证使用的类(类由我们来定义)。例如BookView视图类中:
classBookView(APIView):
authentication_classes=[TokenAuth,]pass
查看restframework调用authentication_classes中类的源码,可以看到TokenAuth中必须实现 authenticate方法,以及 authenticate_header 方法:
classTokenAuth(object):#认证token过程
defauthenticate(self, request):
token= request.GET.get("token")
token_obj= Token.objects.filter(token=token).first()if nottoken_obj:raise exceptions.AuthenticationFailed("验证失败")return(token_obj.user, token_obj)defauthenticate_header(self, request):return None
或者,继承 BaseAuthentication也可以:
from rest_framework.authentication importBaseAuthenticationclassTokenAuth(BaseAuthentication):#认证token过程
defauthenticate(self, request):
token= request.GET.get("token")
token_obj= Token.objects.filter(token=token).first()if nottoken_obj:raise exceptions.AuthenticationFailed("验证失败")return (token_obj.user, token_obj)
然后应用于BookView:
classBookView(APIView):
authentication_classes=[TokenAuth]defget(self, request):
book_list=Book.objects.all()
bs= BookModelSerializers(book_list, many=True, context={'request': request})returnResponse(bs.data)defpost(self, request):
bs= BookModelSerializers(data=request.data)ifbs.is_valid():
bs.save()returnResponse(bs.data)else:return Response(bs.errors)
这样,我们想要通过GET请求获取book数据的时候,就需要先访问login页面,获取token,然后在GET请求中附带token,才能正确获取book数据:
2.测试
POST请求访问http://127.0.0.1:8000/login/,附带用户名和密码,进行登录验证:
获得返回值:
{"code": 1000, "msg": null, "token": "91dc33a308cd4e8b04e14bb3d23d492b"}
然后GET请求访问http://127.0.0.1:8000/books/?token=91dc33a308cd4e8b04e14bb3d23d492b:
获得返回结果:
[{"id":8,"publish":"http://127.0.0.1:8000/publishes/1/","title":"Python2标准库3","price":99,"pub_date":"2012-11-20T13:03:33Z","authors":[1,2]},{"id":9,"publish":"http://127.0.0.1:8000/publishes/1/","title":"Python2标准库4","price":99,"pub_date":"2012-11-20T13:03:33Z","authors":[1,2]},{"id":10,"publish":"http://127.0.0.1:8000/publishes/1/","title":"Python2标准库5","price":99,"pub_date":"2012-11-20T13:03:33Z","authors":[1,2]},{"id":11,"publish":"http://127.0.0.1:8000/publishes/1/","title":"Python2标准库6","price":99,"pub_date":"2012-11-20T13:03:33Z","authors":[1,2]},{"id":12,"publish":"http://127.0.0.1:8000/publishes/1/","title":"Python2标准库7","price":99,"pub_date":"2012-11-20T13:03:33Z","authors":[1,2]},{"id":13,"publish":"http://127.0.0.1:8000/publishes/1/","title":"Python2标准库","price":99,"pub_date":"2012-11-20T13:03:33Z","authors":[1,2]},{"id":14,"publish":"http://127.0.0.1:8000/publishes/1/","title":"Python3","price":99,"pub_date":"2020-01-20T13:03:04Z","authors":[3]},{"id":15,"publish":"http://127.0.0.1:8000/publishes/1/","title":"JAVA","price":99,"pub_date":"2020-01-20T13:03:04Z","authors":[3]},{"id":16,"publish":"http://127.0.0.1:8000/publishes/1/","title":"JAVA","price":99,"pub_date":"2020-01-20T13:03:04Z","authors":[3]},{"id":17,"publish":"http://127.0.0.1:8000/publishes/1/","title":"JAVA","price":99,"pub_date":"2020-01-20T13:03:04Z","authors":[3]},{"id":18,"publish":"http://127.0.0.1:8000/publishes/1/","title":"hello","price":99,"pub_date":"2020-01-20T13:03:04Z","authors":[3]}]
如果未携带token,或携带的token错误:
返回结果:
{"detail":"验证失败"}
三、restframework配置
1.引子
在第二节中,我们实现了token的生成和认证,在认证时,我们使用自定义的TokenAuth类来进行认证,但是如果在每个视图类中都加上 authentication_classes 列表,比较冗余。
我们观察restframe的源码,可以看到,当我们不添加 authentication_classes 列表变量时,APIView中 authentication_classes 变量会读取一个默认值:
classAPIView(View):#The following policies may be set at either globally, or per-view.
renderer_classes =api_settings.DEFAULT_RENDERER_CLASSES
parser_classes=api_settings.DEFAULT_PARSER_CLASSES
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES
throttle_classes=api_settings.DEFAULT_THROTTLE_CLASSES
...
...
继续查看api_settings所属类的源码:
classAPISettings:def __init__(self, user_settings=None, defaults=None, import_strings=None):ifuser_settings:
self._user_settings= self.__check_user_settings(user_settings)
self.defaults= defaults orDEFAULTS
self.import_strings= import_strings orIMPORT_STRINGS
self._cached_attrs=set()
...
...
这里的DEFAULTS就是restframework的默认配置:
DEFAULTS ={#Base API policies
'DEFAULT_RENDERER_CLASSES': ['rest_framework.renderers.JSONRenderer','rest_framework.renderers.BrowsableAPIRenderer',
],'DEFAULT_PARSER_CLASSES': ['rest_framework.parsers.JSONParser','rest_framework.parsers.FormParser','rest_framework.parsers.MultiPartParser'],'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework.authentication.SessionAuthentication','rest_framework.authentication.BasicAuthentication'],'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.AllowAny',
],'DEFAULT_THROTTLE_CLASSES': [],'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation','DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata','DEFAULT_VERSIONING_CLASS': None,#Generic view behavior
'DEFAULT_PAGINATION_CLASS': None,'DEFAULT_FILTER_BACKENDS': [],#Schema
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.openapi.AutoSchema',#Throttling
'DEFAULT_THROTTLE_RATES': {'user': None,'anon': None,
},'NUM_PROXIES': None,#Pagination
'PAGE_SIZE': None,#Filtering
'SEARCH_PARAM': 'search','ORDERING_PARAM': 'ordering',#Versioning
'DEFAULT_VERSION': None,'ALLOWED_VERSIONS': None,'VERSION_PARAM': 'version',#Authentication
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser','UNAUTHENTICATED_TOKEN': None,#View configuration
'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name','VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description',#Exception handling
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler','NON_FIELD_ERRORS_KEY': 'non_field_errors',#Testing
'TEST_REQUEST_RENDERER_CLASSES': ['rest_framework.renderers.MultiPartRenderer','rest_framework.renderers.JSONRenderer'],'TEST_REQUEST_DEFAULT_FORMAT': 'multipart',#Hyperlink settings
'URL_FORMAT_OVERRIDE': 'format','FORMAT_SUFFIX_KWARG': 'format','URL_FIELD_NAME': 'url',#Input and output formats
'DATE_FORMAT': ISO_8601,'DATE_INPUT_FORMATS': [ISO_8601],'DATETIME_FORMAT': ISO_8601,'DATETIME_INPUT_FORMATS': [ISO_8601],'TIME_FORMAT': ISO_8601,'TIME_INPUT_FORMATS': [ISO_8601],#Encoding
'UNICODE_JSON': True,'COMPACT_JSON': True,'STRICT_JSON': True,'COERCE_DECIMAL_TO_STRING': True,'UPLOADED_FILES_USE_URL': True,#Browseable API
'HTML_SELECT_CUTOFF': 1000,'HTML_SELECT_CUTOFF_TEXT': "More than {count} items...",#Schemas
'SCHEMA_COERCE_PATH_PK': True,'SCHEMA_COERCE_METHOD_NAMES': {'retrieve': 'read','destroy': 'delete'},
}
View Code
前面代码中,使用 api_settings.DEFAULT_AUTHENTICATION_CLASSES ,api_settings没有这个属性,所以会自动调用 APISettings 的__getattr__()方法:
def __getattr__(self, attr):if attr not inself.defaults:raise AttributeError("Invalid API setting: '%s'" %attr)try:#Check if present in user settings
val =self.user_settings[attr]exceptKeyError:#Fall back to defaults
val =self.defaults[attr]#Coerce import strings into classes
if attr inself.import_strings:
val=perform_import(val, attr)#Cache the result
self._cached_attrs.add(attr)
setattr(self, attr, val)return val
__getattr__()方法先判断DEFAULTS中是否存在 DEFAULT_AUTHENTICATION_CLASSES ,如果不存在则报错。然后去user_settings中获取 DEFAULT_AUTHENTICATION_CLASSES 的值,user_settings是一个属性方法:
@propertydefuser_settings(self):if not hasattr(self, '_user_settings'):
self._user_settings= getattr(settings, 'REST_FRAMEWORK', {})return self._user_settings
这段代码会先去django的settings中查看是否存在名为"REST_FRAMEWORK"的配置项。所以我们要使用自定义的认证类,可以在django的settings中配置REST_FRAMEWORK来指定。
首先,将TokenAuth类从views.py移到单独的一个模块,例如utils.py:
#utils.py
from rest_framework importexceptionsfrom .models importTokenfrom rest_framework.authentication importBaseAuthenticationclassTokenAuth(BaseAuthentication):#认证token过程
defauthenticate(self, request):
token= request.GET.get("token")
token_obj= Token.objects.filter(token=token).first()if nottoken_obj:raise exceptions.AuthenticationFailed("验证失败")return (token_obj.user, token_obj)
然后在django的settings中添加配置:
REST_FRAMEWORK ={"DEFAULT_AUTHENTICATION_CLASSES": ["demo.utils.TokenAuth"]
}
这样,我们的所有视图类在被访问时都会使用TokenAuth类来对token进行验证,但是在访问/login/页面时,由于还没有登录认证,所以不能进行token验证。
可以在LoginView视图类中,加上一个空的 authentication_classes 列表来处理:
classLoginView(APIView):
authentication_classes=[]
...
...
这样,访问/login/的时候不会验证token,而访问其他资源的时候会验证token。
ღ♋