欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Django+JWT实现Token认证的实现方法

程序员文章站 2022-06-20 23:50:58
对外提供api不用django rest framework(drf)就是旁门左道吗? 基于token的鉴权机制越来越多的用在了项目中,尤其是对于纯后端只对外提供api没...

对外提供api不用django rest framework(drf)就是旁门左道吗?

基于token的鉴权机制越来越多的用在了项目中,尤其是对于纯后端只对外提供api没有web页面的项目,例如我们通常所讲的前后端分离架构中的纯后端服务,只提供api给前端,前端通过api提供的数据对页面进行渲染展示或增加修改等,我们知道http是一种无状态的协议,也就是说后端服务并不知道是谁发来的请求,那么如何校验请求的合法性呢?这就需要通过一些方式对请求进行鉴权了

先来看看传统的登录鉴权跟基于token的鉴权有什么区别

以django的账号密码登录为例来说明传统的验证鉴权方式是怎么工作的,当我们登录页面输入账号密码提交表单后,会发送请求给服务器,服务器对发送过来的账号密码进行验证鉴权,验证鉴权通过后,把用户信息记录在服务器端(django_session表中),同时返回给浏览器一个sessionid用来唯一标识这个用户,浏览器将sessionid保存在cookie中,之后浏览器的每次请求都一并将sessionid发送给服务器,服务器根据sessionid与记录的信息做对比以验证身份

token的鉴权方式就清晰很多了,客户端用自己的账号密码进行登录,服务端验证鉴权,验证鉴权通过生成token返回给客户端,之后客户端每次请求都将token放在header里一并发送,服务端收到请求时校验token以确定访问者身份

session的主要目的是给无状态的http协议添加状态保持,通常在浏览器作为客户端的情况下比较通用。而token的主要目的是为了鉴权,同时又不需要考虑csrf防护以及跨域的问题,所以更多的用在专门给第三方提供api的情况下,客户端请求无论是浏览器发起还是其他的程序发起都能很好的支持。所以目前基于token的鉴权机制几乎已经成了前后端分离架构或者对外提供api访问的鉴权标准,得到广泛使用

json web token(jwt)是目前token鉴权机制下最流行的方案,网上关于jwt的介绍有很多,这里不细说,只讲下django如何利用jwt实现对api的认证鉴权,搜了几乎所有的文章都是说jwt如何结合drf使用的,如果你的项目没有用到drf框架,也不想仅仅为了鉴权api就引入庞大复杂的drf框架,那么可以接着往下看

我的需求如下:

  1. 同一个view函数既给前端页面提供数据,又对外提供api服务,要同时满足基于账号密码的验证和jwt验证
  2. 项目用了django默认的权限系统,既能对账号密码登录的进行权限校验,又能对基于jwt的请求进行权限校验

pyjwt介绍

要实现上边的需求1,我们首先得引入jwt模块,python下有现成的pyjwt模块可以直接用,先看下jwt的简单用法

安装pyjwt

$ pip install pyjwt

利用pyjwt生成token

>>> import jwt
>>> encoded_jwt = jwt.encode({'username':'运维咖啡吧','site':'https://ops-coffee.cn'},'secret_key',algorithm='hs256')

这里传了三部分内容给jwt,

第一部分是一个json对象,称为payload,主要用来存放有效的信息,例如用户名,过期时间等等所有你想要传递的信息

第二部分是一个秘钥字串,这个秘钥主要用在下文signature签名中,服务端用来校验token合法性,这个秘钥只有服务端知道,不能泄露

第三部分指定了signature签名的算法

查看生成的token

>>> print(encoded_jwt)
b'eyj0exaioijkv1qilcjhbgcioijiuzi1nij9.eyj1c2vybmftzsi6ilx1ogzkmfx1n2vmnfx1ntq5nlx1ntu2mvx1ntqynyisinnpdguioijodhrwczovl29wcy1jb2zmzwuuy24ifq.fipsxy476r9f9i7ghdyfnkd-2ndz8uklgjpcd84bkj4'

jwt生成的token是一个用两个点(.)分割的长字符串

点分割成的三部分分别是header头部,payload负载,signature签名: header.payload.signature

jwt是不加密的,任何人都可以读的到其中的信息,其中第一部分header和第二部分payload只是对原始输入的信息转成了base64编码,第三部分signature是用header+payload+secret_key进行加密的结果

可以直接用base64对header和payload进行解码得到相应的信息

>>> import base64
>>> base64.b64decode('eyj0exaioijkv1qilcjhbgcioijiuzi1nij9')
b'{"typ":"jwt","alg":"hs256"}'

>>> base64.b64decode('eyj1c2vybmftzsi6ilx1ogzkmfx1n2vmnfx1ntq5nlx1ntu2mvx1ntqynyisinnpdguioijodhrwczovl29wcy1jb2zmzwuuy24ifq==')
# 这里最后加=的原因是base64解码对传入的参数长度不是2的对象,需要再参数最后加上一个或两个等号=

因为jwt不会对结果进行加密,所以不要保存敏感信息在header或者payload中,服务端也主要依靠最后的signature来验证token是否有效以及有无被篡改

解密token

>>> jwt.decode(encoded_jwt,'secret_key',algorithms=['hs256'])
{'username': '运维咖啡吧', 'site': 'https://ops-coffee.cn'}

服务端在有秘钥的情况下可以直接对jwt生成的token进行解密,解密成功说明token正确,且数据没有被篡改

当然我们前文说了jwt并没有对数据进行加密,如果没有secret_key也可以直接获取到payload里边的数据,只是缺少了签名算法无法验证数据是否准确,pyjwt也提供了直接获取payload数据的方法,如下

>>> jwt.decode(encoded_jwt, verify=false)
{'username': '运维咖啡吧', 'site': 'https://ops-coffee.cn'}

django案例

django要兼容session认证的方式,还需要同时支持jwt,并且两种验证需要共用同一套权限系统,该如何处理呢?我们可以参考django的解决方案:装饰器,例如用来检查用户是否登录的 login_required 和用来检查用户是否有权限的 permission_required 两个装饰器,我们可以自己实现一个装饰器,检查用户的认证模式,同时认证完成后验证用户是否有权限操作

于是一个 auth_permission_required 的装饰器产生了:

from django.conf import settings
from django.http import jsonresponse
from django.contrib.auth import get_user_model
from django.core.exceptions import permissiondenied

usermodel = get_user_model()


def auth_permission_required(perm):
  def decorator(view_func):
    def _wrapped_view(request, *args, **kwargs):
      # 格式化权限
      perms = (perm,) if isinstance(perm, str) else perm

      if request.user.is_authenticated:
        # 正常登录用户判断是否有权限
        if not request.user.has_perms(perms):
          raise permissiondenied
      else:
        try:
          auth = request.meta.get('http_authorization').split()
        except attributeerror:
          return jsonresponse({"code": 401, "message": "no authenticate header"})

        # 用户通过api获取数据验证流程
        if auth[0].lower() == 'token':
          try:
            dict = jwt.decode(auth[1], settings.secret_key, algorithms=['hs256'])
            username = dict.get('data').get('username')
          except jwt.expiredsignatureerror:
            return jsonresponse({"status_code": 401, "message": "token expired"})
          except jwt.invalidtokenerror:
            return jsonresponse({"status_code": 401, "message": "invalid token"})
          except exception as e:
            return jsonresponse({"status_code": 401, "message": "can not get user object"})

          try:
            user = usermodel.objects.get(username=username)
          except usermodel.doesnotexist:
            return jsonresponse({"status_code": 401, "message": "user does not exist"})

          if not user.is_active:
            return jsonresponse({"status_code": 401, "message": "user inactive or deleted"})

          # token登录的用户判断是否有权限
          if not user.has_perms(perms):
            return jsonresponse({"status_code": 403, "message": "permissiondenied"})
        else:
          return jsonresponse({"status_code": 401, "message": "not support auth type"})

      return view_func(request, *args, **kwargs)

    return _wrapped_view

  return decorator

在view使用时就可以用这个装饰器来代替原本的 login_required 和 permission_required 装饰器了

@auth_permission_required('account.select_user')
def user(request):
  if request.method == 'get':
    _jsondata = {
      "user": "ops-coffee",
      "site": "https://ops-coffee.cn"
    }
    
    return jsonresponse({"state": 1, "message": _jsondata})
  else:
    return jsonresponse({"state": 0, "message": "request method 'post' not supported"})

我们还需要一个生成用户token的方法,通过给user model添加一个token的静态方法来处理

class user(abstractbaseuser, permissionsmixin):
  create_time = models.datetimefield(auto_now_add=true, verbose_name='创建时间')
  update_time = models.datetimefield(auto_now=true, verbose_name='更新时间')
  username = models.emailfield(max_length=255, unique=true, verbose_name='用户名')
  fullname = models.charfield(max_length=64, null=true, verbose_name='中文名')
  phonenumber = models.charfield(max_length=16, null=true, unique=true, verbose_name='电话')
  is_active = models.booleanfield(default=true, verbose_name='激活状态')

  objects = usermanager()

  username_field = 'username'
  required_fields = []

  def __str__(self):
    return self.username

  @property
  def token(self):
    return self._generate_jwt_token()

  def _generate_jwt_token(self):
    token = jwt.encode({
      'exp': datetime.utcnow() + timedelta(days=1),
      'iat': datetime.utcnow(),
      'data': {
        'username': self.username
      }
    }, settings.secret_key, algorithm='hs256')

    return token.decode('utf-8')

  class meta:
    default_permissions = ()

    permissions = (
      ("select_user", "查看用户"),
      ("change_user", "修改用户"),
      ("delete_user", "删除用户"),
    )

可以直接通过用户对象来生成token:

>>> from accounts.models import user
>>> u = user.objects.get(username='admin@ops-coffee.cn')
>>> u.token
'eyj0exaioijkv1qilcjhbgcioijiuzi1nij9.eyjlehaioje1ndgymjg3nzksimlhdci6mtu0ode0mjm3oswizgf0ysi6eyj1c2vybmftzsi6imfkbwluqde2my5jb20ifx0.akznu7t_z2kwpxdjjmc-qxtndick0yhnwwmkxqqxklw'

生成的token给到客户端,客户端就可以拿这个token进行鉴权了

>>> import requests
>>> token = 'eyj0exaioijkv1qilcjhbgcioijiuzi1nij9.eyjlehaioje1ndgymjg4mzgsimlhdci6mtu0ode0mjqzocwizgf0ysi6eyj1c2vybmftzsi6imfkbwluqde2my5jb20ifx0.okc0safgksmt9zihtacupulz49q5ki4oja-b8-ghqla'
>>>
>>> r = requests.get('http://localhost/api/user', headers={'authorization': 'token '+token})
>>> r.json()
{'username': 'admin@ops-coffee.cn', 'fullname': '运维咖啡吧', 'is_active': true}

这样一个 auth_permission_required 方法就可以搞定上边的全部需求了,简单好用。

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