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框架,那么可以接着往下看

我的需求如下:

同一个view函数既给前端页面提供数据,又对外提供API服务,要同时满足基于账号密码的验证和JWT验证

项目用了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的装饰器产生了:

fromdjango.confimportsettingsfromdjango.httpimportJsonResponsefromdjango.contrib.authimportget_user_modelfromdjango.core.exceptionsimportPermissionDeniedUserModel = get_user_model()defauth_permission_required(perm):defdecorator(view_func):def_wrapped_view(request, *args, **kwargs):# 格式化权限perms = (perm,)ifisinstance(perm, str)elsepermifrequest.user.is_authenticated:# 正常登录用户判断是否有权限ifnotrequest.user.has_perms(perms):raisePermissionDeniedelse:try:                    auth = request.META.get('HTTP_AUTHORIZATION').split()exceptAttributeError:returnJsonResponse({"code":401,"message":"No authenticate header"})# 用户通过API获取数据验证流程ifauth[0].lower() =='token':try:                        dict = jwt.decode(auth[1], settings.SECRET_KEY, algorithms=['HS256'])                        username = dict.get('data').get('username')exceptjwt.ExpiredSignatureError:returnJsonResponse({"status_code":401,"message":"Token expired"})exceptjwt.InvalidTokenError:returnJsonResponse({"status_code":401,"message":"Invalid token"})exceptExceptionase:returnJsonResponse({"status_code":401,"message":"Can not get user object"})try:                        user = UserModel.objects.get(username=username)exceptUserModel.DoesNotExist:returnJsonResponse({"status_code":401,"message":"User Does not exist"})ifnotuser.is_active:returnJsonResponse({"status_code":401,"message":"User inactive or deleted"})# Token登录的用户判断是否有权限ifnotuser.has_perms(perms):returnJsonResponse({"status_code":403,"message":"PermissionDenied"})else:returnJsonResponse({"status_code":401,"message":"Not support auth type"})returnview_func(request, *args, **kwargs)return_wrapped_viewreturndecorator

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

@auth_permission_required('account.select_user')defuser(request):ifrequest.method =='GET':        _jsondata = {"user":"ops-coffee","site":"https://ops-coffee.cn"}returnJsonResponse({"state":1,"message": _jsondata})else:returnJsonResponse({"state":0,"message":"Request method 'POST' not supported"})

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

classUser(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):returnself.username    @propertydeftoken(self):returnself._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')returntoken.decode('utf-8')classMeta: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方法就可以搞定上边的全部需求了,简单好用。

感兴趣的可以自己来我的Java架构群,可以获取免费的学习资料,群号:855801563对Java技术,架构技术感兴趣的同学,欢迎加群,一起学习,相互讨论。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,820评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,648评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,324评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,714评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,724评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,328评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,897评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,804评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,345评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,431评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,561评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,238评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,928评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,417评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,528评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,983评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,573评论 2 359

推荐阅读更多精彩内容