django drf框架中的user验证以及JWT拓展的介绍


Posted in Python onAugust 12, 2019

登录注册是几乎所有网站都需要去做的接口,而说到登录,自然也就涉及到验证以及用户登录状态保存,最近用DRF在做的一个关于网上商城的项目中,引入了一个拓展DRF JWT,专门用于做验证和用户状态保存。这个拓展比传统的CSRF更加安全。先来介绍一下JWT认证机制吧!

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准( (RFC 7519 ).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

基于token的鉴权机制

基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

流程上是这样的:

  • 用户使用用户名密码来请求服务器
  • 服务器进行验证用户的信息
  • 服务器通过验证发送给用户一个token
  • 客户端存储token,并在每次请求时附送上这个token值
  • 服务端验证token值,并返回数据

这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持 CORS(跨来源资源共享) 策略,一般我们在服务端这么做就可以了 Access-Control-Allow-Origin: * 。

那么我们现在回到JWT的主题上。

JWT的构成

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).

jwt的头部承载两部分信息:1,声明类型,这里是jwt,2声明加密的算法 通常直接使用 HMAC SHA256。完整的头部就像下面这样的JSON:

{
 'typ': 'JWT',
 'alg': 'HS256'
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分。

如 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9。

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分1标准中注册的声明,2公共的声明,3私有的声明。标准中注册的声明 (建议但不强制使用) :1 iss: jwt签发者,2 sub: jwt所面向的用户,3 aud: 接收jwt的一方,4 exp: jwt的过期时间,这个过期时间必须要大于签发时间,5 nbf: 定义在什么时间之前,该jwt都是不可用的,6 iat: jwt的签发时间,7 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。定义一个payload:

{
 "sub": "1234567890",
 "name": "John Doe",
 "admin": true
}

然后将其进行base64加密,得到JWT的第二部分。

如 eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9。

JWT的第三部分是一个签证信息,这个签证信息由三部分组成:1 header (base64后的),2 payload (base64后的),3 secret。这个部分需要base64加密后的header和base64加密后的payload使用 . 连接组成的字符串,然后通过header中声明的加密方式进行加盐 secret 组合加密,然后就构成了jwt的第三部分。

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

首先需要安装拓展  pip install djangorestframework-jwt,然后在django进行配置, JWT_EXPIRATION_DELTA 指明token的有效期。

REST_FRAMEWORK = {
 'DEFAULT_AUTHENTICATION_CLASSES': (
  'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
  'rest_framework.authentication.SessionAuthentication',
  'rest_framework.authentication.BasicAuthentication',
 ),
}

JWT_AUTH = {
 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
}

Django REST framework JWT 扩展的说明文档中提供了手动签发JWT的方法

from rest_framework_jwt.settings import api_settings

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)

在注册时,引入上述代码,签发JWT即可。而对于登录,JWT拓展提供了内置的视图,在urls中添加对于路由即可。

from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
 url(r'^authorizations/$', obtain_jwt_token),
]

虽然写起来很简单,但是内部其实做了很多的事,今天就来详细研究一下,源代码内部做了哪些事情。

当用户登录,会以post形式发请求到后端,会访问 obtain_jwt_token中的post方法,在源代码中可以看到 obtain_jwt_token = ObtainJSONWebToken.as_view(),跟我们写的类视图十分类似,这是一个内部已经写好的类视图。

class ObtainJSONWebToken(JSONWebTokenAPIView):
 """
 API View that receives a POST with a user's username and password.

 Returns a JSON Web Token that can be used for authenticated requests.
 """
 serializer_class = JSONWebTokenSerializer

该类并未定义任何方法,所以对应的post方法应该写在父类,下面是父类中的post方法

def post(self, request, *args, **kwargs):
  serializer = self.get_serializer(data=request.data)

  if serializer.is_valid():
   user = serializer.object.get('user') or request.user
   token = serializer.object.get('token')
   response_data = jwt_response_payload_handler(token, user, request)
   response = Response(response_data)
   if api_settings.JWT_AUTH_COOKIE:
    expiration = (datetime.utcnow() +
        api_settings.JWT_EXPIRATION_DELTA)
    response.set_cookie(api_settings.JWT_AUTH_COOKIE,
         token,
         expires=expiration,
         httponly=True)
   return response

  return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

上述方法返回一个Response对象,经过一系列操作返回到前端,访问结束。

而在DRF框架中,在调用视图之前,就会进行相应的验证操作。想要了解整个过程,需要我们从源代码中一步步去探索。当前端发起一个请求到后端,会根据路由访问对象的视图类的as_view()方法,该方法会接着调用dispatch()方法,APIView是DRF中所有视图类的父类,可以看一下他的dispatch方法。

def dispatch(self, request, *args, **kwargs):
  """
  `.dispatch()` is pretty much the same as Django's regular dispatch,
  but with extra hooks for startup, finalize, and exception handling.
  """
  self.args = args
  self.kwargs = kwargs
  request = self.initialize_request(request, *args, **kwargs)
  self.request = request
  self.headers = self.default_response_headers # deprecate?

  try:
   self.initial(request, *args, **kwargs)

   # Get the appropriate handler method
   if request.method.lower() in self.http_method_names:
    handler = getattr(self, request.method.lower(),
         self.http_method_not_allowed)
   else:
    handler = self.http_method_not_allowed

   response = handler(request, *args, **kwargs)

  except Exception as exc:
   response = self.handle_exception(exc)

  self.response = self.finalize_response(request, response, *args, **kwargs)
  return self.response

可以看到,到请求进来,会调用self.initalize_request()方法对请求进行处理。

def initialize_request(self, request, *args, **kwargs):
  """
  Returns the initial request object.
  """
  parser_context = self.get_parser_context(request)

  return Request(
   request,
   parsers=self.get_parsers(),
   authenticators=self.get_authenticators(),
   negotiator=self.get_content_negotiator(),
   parser_context=parser_context
  )

我们需要关注的只有 authenticators=self.get_authenticators()这一句,

def get_authenticators(self):
  """
  Instantiates and returns the list of authenticators that this view can use.
  """
  return [auth() for auth in self.authentication_classes]

接着往上照,可以看到类属性 permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES,这就是要什么我们要在django配置中加入DEFAULT_PERMISSION_CLASSES配置,上述方法会遍历我们在配置中写到的列表,拿到里面的验证类,并进行实例化,并将生成的对象装在一个新的列表中,保存在新生成的Request对象中。然后我们接着看dispatch方法,在实际调用视图类中的对应方法前,还调用了self.initial()方法进行一些初始化操作。

def initial(self, request, *args, **kwargs):
  """
  Runs anything that needs to occur prior to calling the method handler.
  """
  self.format_kwarg = self.get_format_suffix(**kwargs)

  # Perform content negotiation and store the accepted info on the request
  neg = self.perform_content_negotiation(request)
  request.accepted_renderer, request.accepted_media_type = neg

  # Determine the API version, if versioning is in use.
  version, scheme = self.determine_version(request, *args, **kwargs)
  request.version, request.versioning_scheme = version, scheme

  # Ensure that the incoming request is permitted
  self.perform_authentication(request)
  self.check_permissions(request)
  self.check_throttles(request)

我们需要关注的是最后三排,分别调用了三个方法,进行身份验证,权限验证和分流操作,这里我们只关注 身份验证方法,self.perform_authentication(),该方法内只有一句代码request.user,看起来像是在调用request对象的user属性,其实不然,我们可以到DRF框架的Request对象中找到以下方法。

@property
 def user(self):
  """
  Returns the user associated with the current request, as authenticated
  by the authentication classes provided to the request.
  """
  if not hasattr(self, '_user'):
   with wrap_attributeerrors():
    self._authenticate()
  return self._user

user方法经过property装饰器装饰后,就可以像一个属性一样调用该方法,该方法在Request对象中存在对应的user时会直接返回,若用户登陆时,Request对象中没有对应的user,所以代码会走if判断里面,我们只需要关注方法self._authenticate()的调用即可。

def _authenticate(self):
  """
  Attempt to authenticate the request using each authentication instance
  in turn.
  """
  for authenticator in self.authenticators:
   try:
    user_auth_tuple = authenticator.authenticate(self)
   except exceptions.APIException:
    self._not_authenticated()
    raise

   if user_auth_tuple is not None:
    self._authenticator = authenticator
    self.user, self.auth = user_auth_tuple
    return

  self._not_authenticated()

可以看到,该方法会遍历我们在之前处理Request对象时传入的装着验证类对象的列表,并调用验证类的authenticate()方法,若验证成功生成对应的self.user和self.auth并直接return,往上则直接将生成的self.user进行返回,若验证内部出错,会调用 self._not_ authenticated(), 并抛出错误,往上看,在dispatch方法中,若初始化方法出错,则进行捕获,并调用self.handle_exception()方法生成一个Response对象进行返回,不会执行视图类中对应的方法,则调用对于的是self._not_authenticated()。

def _not_authenticated(self):
  """
  Set authenticator, user & authtoken representing an unauthenticated request.

  Defaults are None, AnonymousUser & None.
  """
  self._authenticator = None

  if api_settings.UNAUTHENTICATED_USER:
   self.user = api_settings.UNAUTHENTICATED_USER()
  else:
   self.user = None

  if api_settings.UNAUTHENTICATED_TOKEN:
   self.auth = api_settings.UNAUTHENTICATED_TOKEN()
  else:
   self.auth = None

UNAUTHENTICATED_USER在django默认配置中为一个匿名用户的类,UNAUTHENTICATED_TOKEN默认为None,若所有验证都为通过或者某一验证过程中出错,则生成一个匿名用户,并将self.auth设置为None。

综上所述,在请求执行之前,DRF框架会根据我们在配置文件中配置的验证类对用户进行身份验证,若未通过验证,则会生成一个匿名用户,验证通过,则生成对应的用户。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python Deque 模块使用详解
Jul 04 Python
使用python编写批量卸载手机中安装的android应用脚本
Jul 21 Python
Python实现拼接多张图片的方法
Dec 01 Python
Python多进程分块读取超大文件的方法
Apr 13 Python
Python3.遍历某文件夹提取特定文件名的实例
Apr 26 Python
mac下给python3安装requests库和scrapy库的实例
Jun 13 Python
python递归函数绘制分形树的方法
Jun 22 Python
浅谈Python traceback的优雅处理
Aug 31 Python
python读取TXT每行,并存到LIST中的方法
Oct 26 Python
在Python中实现函数重载的示例代码
Dec 12 Python
requests在python中发送请求的实例讲解
Feb 17 Python
详解python3 GUI刷屏器(附源码)
Feb 18 Python
python中eval与int的区别浅析
Aug 11 #Python
将Python文件打包成.EXE可执行文件的方法
Aug 11 #Python
python多线程同步实例教程
Aug 11 #Python
pandas的to_datetime时间转换使用及学习心得
Aug 11 #Python
python中时间转换datetime和pd.to_datetime详析
Aug 11 #Python
Python时间序列缺失值的处理方法(日期缺失填充)
Aug 11 #Python
python3实现带多张图片、附件的邮件发送
Aug 10 #Python
You might like
php实现基于微信公众平台开发SDK(demo)扩展的方法
2014/12/22 PHP
PHP中header函数的用法及其注意事项详解
2016/06/13 PHP
PHP实现图的邻接矩阵表示及几种简单遍历算法分析
2017/11/24 PHP
Yii2 queue的队列使用详解
2019/07/19 PHP
jQuery DOM操作小结与实例
2010/01/07 Javascript
DOM节点删除函数removeChild()用法实例
2015/01/12 Javascript
jquery获取checkbox的值并post提交
2015/01/14 Javascript
js随机生成字母数字组合的字符串 随机动画数字
2015/09/02 Javascript
jQuery版AJAX简易封装代码
2016/09/14 Javascript
Javascript 制作图形验证码实例详解
2016/12/22 Javascript
常用的几个JQuery代码片段
2017/03/13 Javascript
node中使用es5/6以及支持性与性能对比
2017/08/11 Javascript
NodeJS安装图文教程
2018/04/19 NodeJs
layui递归实现动态左侧菜单
2019/07/26 Javascript
解决Ant Design Modal内嵌Form表单initialValue值不动态更新问题
2020/10/29 Javascript
[52:26]完美世界DOTA2联赛决赛 FTD vs Phoenix 第一场 11.08
2020/11/11 DOTA
[52:52]完美世界DOTA2联赛PWL S3 LBZS vs access 第一场 12.10
2020/12/13 DOTA
非递归的输出1-N的全排列实例(推荐)
2017/04/11 Python
python去除扩展名的实例讲解
2018/04/23 Python
python使用tornado实现简单爬虫
2018/07/28 Python
使用python实现mqtt的发布和订阅
2019/05/05 Python
python中的协程深入理解
2019/06/10 Python
tensorflow 实现自定义layer并添加到计算图中
2020/02/04 Python
详解Python中如何将数据存储为json格式的文件
2020/11/18 Python
Perry Ellis官网:美国男士品味服装
2016/12/09 全球购物
德国化妆品和天然化妆品网上商店:kosmetikfuchs.de
2017/06/09 全球购物
群众路线批评与自我批评
2014/02/06 职场文书
出国留学担保书
2014/05/20 职场文书
生物科学专业毕业生求职信
2014/06/02 职场文书
爱国口号
2014/06/19 职场文书
查摆剖析材料范文
2014/09/30 职场文书
政协会议宣传标语
2014/10/09 职场文书
劳资员岗位职责
2015/02/13 职场文书
为什么RedisCluster设计成16384个槽
2021/09/25 Redis
mysql聚集索引、辅助索引、覆盖索引、联合索引的使用
2022/02/12 MySQL
Python 中面向接口编程
2022/05/20 Python