Django+JWT實(shí)現(xiàn)Token認(rèn)證 原 薦

對外提供API不用django rest framework(DRF)就是旁門左道嗎?

基于Token的鑒權(quán)機(jī)制越來越多的用在了項(xiàng)目中,尤其是對于純后端只對外提供API沒有web頁面的項(xiàng)目,例如我們通常所講的前后端分離架構(gòu)中的純后端服務(wù),只提供API給前端,前端通過API提供的數(shù)據(jù)對頁面進(jìn)行渲染展示或增加修改等,我們知道HTTP是一種無狀態(tài)的協(xié)議,也就是說后端服務(wù)并不知道是誰發(fā)來的請求,那么如何校驗(yàn)請求的合法性呢?這就需要通過一些方式對請求進(jìn)行鑒權(quán)了

先來看看傳統(tǒng)的登錄鑒權(quán)跟基于Token的鑒權(quán)有什么區(qū)別

以Django的賬號密碼登錄為例來說明傳統(tǒng)的驗(yàn)證鑒權(quán)方式是怎么工作的,當(dāng)我們登錄頁面輸入賬號密碼提交表單后,會發(fā)送請求給服務(wù)器,服務(wù)器對發(fā)送過來的賬號密碼進(jìn)行驗(yàn)證鑒權(quán),驗(yàn)證鑒權(quán)通過后,把用戶信息記錄在服務(wù)器端(django_session表中),同時返回給瀏覽器一個sessionid用來唯一標(biāo)識這個用戶,瀏覽器將sessionid保存在cookie中,之后瀏覽器的每次請求都一并將sessionid發(fā)送給服務(wù)器,服務(wù)器根據(jù)sessionid與記錄的信息做對比以驗(yàn)證身份

Token的鑒權(quán)方式就清晰很多了,客戶端用自己的賬號密碼進(jìn)行登錄,服務(wù)端驗(yàn)證鑒權(quán),驗(yàn)證鑒權(quán)通過生成Token返回給客戶端,之后客戶端每次請求都將Token放在header里一并發(fā)送,服務(wù)端收到請求時校驗(yàn)Token以確定訪問者身份

session的主要目的是給無狀態(tài)的HTTP協(xié)議添加狀態(tài)保持,通常在瀏覽器作為客戶端的情況下比較通用。而Token的主要目的是為了鑒權(quán),同時又不需要考慮CSRF防護(hù)以及跨域的問題,所以更多的用在專門給第三方提供API的情況下,客戶端請求無論是瀏覽器發(fā)起還是其他的程序發(fā)起都能很好的支持。所以目前基于Token的鑒權(quán)機(jī)制幾乎已經(jīng)成了前后端分離架構(gòu)或者對外提供API訪問的鑒權(quán)標(biāo)準(zhǔn),得到廣泛使用

JSON Web Token(JWT)是目前Token鑒權(quán)機(jī)制下最流行的方案,網(wǎng)上關(guān)于JWT的介紹有很多,這里不細(xì)說,只講下Django如何利用JWT實(shí)現(xiàn)對API的認(rèn)證鑒權(quán),搜了幾乎所有的文章都是說JWT如何結(jié)合DRF使用的,如果你的項(xiàng)目沒有用到DRF框架,也不想僅僅為了鑒權(quán)API就引入龐大復(fù)雜的DRF框架,那么可以接著往下看

我的需求如下:

同一個view函數(shù)既給前端頁面提供數(shù)據(jù),又對外提供API服務(wù),要同時滿足基于賬號密碼的驗(yàn)證和JWT驗(yàn)證

項(xiàng)目用了Django默認(rèn)的權(quán)限系統(tǒng),既能對賬號密碼登錄的進(jìn)行權(quán)限校驗(yàn),又能對基于JWT的請求進(jìn)行權(quán)限校驗(yàn)

PyJWT介紹

要實(shí)現(xiàn)上邊的需求1,我們首先得引入JWT模塊,python下有現(xiàn)成的PyJWT模塊可以直接用,先看下JWT的簡單用法

安裝PyJWT

$ pip install pyjwt

利用PyJWT生成Token

>>> import jwt>>> encoded_jwt = jwt.encode({'username':'運(yùn)維咖啡吧','site':'https://ops-coffee.cn'},'secret_key',algorithm='HS256')

這里傳了三部分內(nèi)容給JWT,

第一部分是一個Json對象,稱為Payload,主要用來存放有效的信息,例如用戶名,過期時間等等所有你想要傳遞的信息

第二部分是一個秘鑰字串,這個秘鑰主要用在下文Signature簽名中,服務(wù)端用來校驗(yàn)Token合法性,這個秘鑰只有服務(wù)端知道,不能泄露

第三部分指定了Signature簽名的算法

查看生成的Token

>>>print(encoded_jwt)b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Ilx1OGZkMFx1N2VmNFx1NTQ5Nlx1NTU2MVx1NTQyNyIsInNpdGUiOiJodHRwczovL29wcy1jb2ZmZWUuY24ifQ.fIpSXy476r9F9i7GhdYFNkd-2Ndz8uKLgJPcd84BkJ4'

JWT生成的Token是一個用兩個點(diǎn)(.)分割的長字符串

點(diǎn)分割成的三部分分別是Header頭部,Payload負(fù)載,Signature簽名:Header.Payload.Signature

JWT是不加密的,任何人都可以讀的到其中的信息,其中第一部分Header和第二部分Payload只是對原始輸入的信息轉(zhuǎn)成了base64編碼,第三部分Signature是用header+payload+secret_key進(jìn)行加密的結(jié)果

可以直接用base64對Header和Payload進(jìn)行解碼得到相應(yīng)的信息

>>> import base64>>> base64.b64decode('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9')b'{"typ":"JWT","alg":"HS256"}'>>> base64.b64decode('eyJ1c2VybmFtZSI6Ilx1OGZkMFx1N2VmNFx1NTQ5Nlx1NTU2MVx1NTQyNyIsInNpdGUiOiJodHRwczovL29wcy1jb2ZmZWUuY24ifQ==')# 這里最后加=的原因是base64解碼對傳入的參數(shù)長度不是2的對象,需要再參數(shù)最后加上一個或兩個等號=

因?yàn)镴WT不會對結(jié)果進(jìn)行加密,所以不要保存敏感信息在Header或者Payload中,服務(wù)端也主要依靠最后的Signature來驗(yàn)證Token是否有效以及有無被篡改

解密Token

>>> jwt.decode(encoded_jwt,'secret_key',algorithms=['HS256']){'username':'運(yùn)維咖啡吧','site':'https://ops-coffee.cn'}

服務(wù)端在有秘鑰的情況下可以直接對JWT生成的Token進(jìn)行解密,解密成功說明Token正確,且數(shù)據(jù)沒有被篡改

當(dāng)然我們前文說了JWT并沒有對數(shù)據(jù)進(jìn)行加密,如果沒有secret_key也可以直接獲取到Payload里邊的數(shù)據(jù),只是缺少了簽名算法無法驗(yàn)證數(shù)據(jù)是否準(zhǔn)確,pyjwt也提供了直接獲取Payload數(shù)據(jù)的方法,如下

>>> jwt.decode(encoded_jwt, verify=False){'username':'運(yùn)維咖啡吧','site':'https://ops-coffee.cn'}

Django案例

Django要兼容session認(rèn)證的方式,還需要同時支持JWT,并且兩種驗(yàn)證需要共用同一套權(quán)限系統(tǒng),該如何處理呢?我們可以參考Django的解決方案:裝飾器,例如用來檢查用戶是否登錄的login_required和用來檢查用戶是否有權(quán)限的permission_required兩個裝飾器,我們可以自己實(shí)現(xiàn)一個裝飾器,檢查用戶的認(rèn)證模式,同時認(rèn)證完成后驗(yàn)證用戶是否有權(quán)限操作

于是一個auth_permission_required的裝飾器產(chǎn)生了:

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):# 格式化權(quán)限perms = (perm,)ifisinstance(perm, str)elsepermifrequest.user.is_authenticated:# 正常登錄用戶判斷是否有權(quán)限ifnotrequest.user.has_perms(perms):raisePermissionDeniedelse:try:? ? ? ? ? ? ? ? ? ? auth = request.META.get('HTTP_AUTHORIZATION').split()exceptAttributeError:returnJsonResponse({"code":401,"message":"No authenticate header"})# 用戶通過API獲取數(shù)據(jù)驗(yàn)證流程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登錄的用戶判斷是否有權(quán)限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的靜態(tài)方法來處理

classUser(AbstractBaseUser, PermissionsMixin):create_time = models.DateTimeField(auto_now_add=True, verbose_name='創(chuàng)建時間')? ? 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='激活狀態(tài)')? ? 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進(jìn)行鑒權(quán)了

>>> 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':'運(yùn)維咖啡吧','is_active': True}

這樣一個auth_permission_required方法就可以搞定上邊的全部需求了,簡單好用。

感興趣的可以自己來我的Java架構(gòu)群,可以獲取免費(fèi)的學(xué)習(xí)資料,群號:855801563對Java技術(shù),架構(gòu)技術(shù)感興趣的同學(xué),歡迎加群,一起學(xué)習(xí),相互討論。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內(nèi)容