DDR爱好者之家 Design By 杰米

登录注册是几乎所有网站都需要去做的接口,而说到登录,自然也就涉及到验证以及用户登录状态保存,最近用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"htmlcode">
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框架会根据我们在配置文件中配置的验证类对用户进行身份验证,若未通过验证,则会生成一个匿名用户,验证通过,则生成对应的用户。

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

DDR爱好者之家 Design By 杰米
广告合作:本站广告合作请联系QQ:858582 申请时备注:广告合作(否则不回)
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
DDR爱好者之家 Design By 杰米