文档章节

Django+JWT实现Token认证

运维咖啡吧
 运维咖啡吧
发布于 01/22 18:59
字数 2233
阅读 506
收藏 13

对外提供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_requiredpermission_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方法就可以搞定上边的全部需求了,简单好用。


长按关注公众号查看更多原创文章

如果你觉得文章对你有帮助,请点右下角【好看】。如果你觉得读的不尽兴,推荐阅读以下文章:

© 著作权归作者所有

共有 人打赏支持
运维咖啡吧
粉丝 23
博文 31
码字总数 53887
作品 1
浦东
架构师
私信 提问
加载中

评论(2)

运维咖啡吧
运维咖啡吧

引用来自“银杏卡卡”的评论

要用好jwt也不简单的,用jwt登录和退出登录,在退出后,服务器还得处理这个失效的token,不然客户端还是可以通过旧的token通过服务端审核的,再就是修改密码等在单点登录的情况下使用jwt也还是不简单的。
👍确实如你所说,所以呀专业的工具方法做专业的事,只拿JWT给API签Token,页面的登录校验等就还是session cookies来吧
银杏卡卡
银杏卡卡
要用好jwt也不简单的,用jwt登录和退出登录,在退出后,服务器还得处理这个失效的token,不然客户端还是可以通过旧的token通过服务端审核的,再就是修改密码等在单点登录的情况下使用jwt也还是不简单的。
ASP.NET WebApi 基于分布式Session方式实现Token签名认证(发布版)

一、课程介绍 明人不说暗话,跟着阿笨一起学玩WebApi!开发提供数据的WebApi服务,最重要的是数据的安全性。那么对于我们来说,如何确保数据的安全将会是需要思考的问题。在ASP.NETWebService...

跟着阿笨一起玩NET
2018/09/05
0
0
HAS-插件式Kerberos认证框架

HAS解决方案要点 Hadoop服务以及服务之间继续使用原先的Kerberos的认证机制; 在集群部署的时候可以把节点使用的Keytab放到可靠的节点上, 集群运行时,集群内的节点只有通过认证后才能正常使...

寒沙牧
2017/12/25
0
0
Nancy基于JwtBearer认证的使用与实现

简单使用 第一步 , 用VS创建一个空的ASP.NET Core Web Application 第二步 , 安装相关的NuGet包 通过命令在Package Manager Console执行安装下面的包,也可以用图形界面来完成这一步操作。...

sshpp
2017/07/24
0
0
Spring Cloud中如何保证各个微服务之间调用的安全性

一.背景 微服务架构下,我们的系统根据业务被拆分成了多个职责单一的微服务。 每个服务都有自己的一套API提供给别的服务调用,那么如何保证安全性呢? 不是说你想调用就可以调用,一定要有认...

尹吉欢
2017/11/25
0
0
Spring Security 登录框架源码逻辑剖析

若无特殊配置登录认证会通过来处理 继承了抽象类 已经定义了方法,对请求的拦截是通过其父类方法实现的。 中核心便是那个方法了,这个的抽象方法被继承实现了 在这个方法中基于用户的用户名和...

谢随安
2018/12/07
0
0

没有更多内容

加载失败,请刷新页面

加载更多

学习设计模式之路

https://java-design-patterns.com/patterns/ https://www.oodesign.com/ https://www.programering.com/a/MTNxAzMwATY.html https://design-patterns.readthedocs.io/zh_CN/latest/ https:/......

晨猫
今天
1
0
JDK1.8 jar包说明

JDK 1.8 lib:access-bridge-64.jarcharsets.jarcldrdata.jardeploy.jardnsns.jarjaccess.jarjavawa.jarjce.jarjfr.jarjfxrt.jarjfxswt.jarjsse.jarlocaledata.jar......

冷基
今天
1
0
判断用户的icloud是否开启【Swift4.2】

使用icloudkit存储用户私人数据时,必须判断用户的icloud是否开启【Swift4.2】 func isICloudContainerAvailable()-> Bool { if FileManager.default.ubiquityIdentityToken != ni......

叶落花开
今天
2
0
今天的学习

1、执行git add *命令就把改动提交到了暂存区,执行git pull命令将远程仓库的数据拉到当前分支并合并,执行git checkout [branch-name]切换分支 2、执行git commit -m '说明' 命令就把改动提...

墨冥
昨天
0
0
Android4.4 及以下TextView,Button等控件使用矢量图报错

1 问题描述 最近项目开发中,图标资源我尽量使用了矢量图,然而配置了基本的兼容设置,程序在低版本中运行还是出现了问题。 xml布局文件中,在TextView中使用矢量图,比如android:drawableS...

newtrek
昨天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部