• Django+JWT实现Token认证


    对外提供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的静态方法来处理

    文章未完,全部内容请关注公众号【运维咖啡吧】或个人网站https://ops-coffee.cn查看,运维咖啡吧专注于原创精品内容分享,感谢您的支持

    扫码关注公众号查看更多实用文章

  • 相关阅读:
    关于HTML(七)--------HTML废弃的标签
    关于HTML(六)--------canvas
    抓包神器之Charles,常用功能都在这里了(转自https://blog.csdn.net/mxw2552261/article/details/78645118)
    混合开发的坑(7) ---输入文本时,键盘遮挡
    关于HTML(五)---------meta标签
    关于HTML(四)---------使用data-的好处
    关于HTML(三)---------xhtml和html的区别
    关于HTML(二)---------浏览器的标准模式和怪异模式
    关于HTML(一)---------HTML5新特性2
    关于HTML(一)---------HTML5新特性1
  • 原文地址:https://www.cnblogs.com/37Y37/p/10305437.html
Copyright © 2020-2023  润新知