Django REST framework API認證(包含JWT認證)
一. 背景
在咱們學習Django Rest Framework(簡稱DRF)時,其很是友好地給咱們提供了一個可瀏覽API的界面。不少測試工做均可以在可瀏覽API界面完成測試。要使用可瀏覽API界面很簡單,只須要在urls.py文件中添加以下部分便可。html
1 |
from django.conf.urls import include |
其中,r'^api-auth/'
部分實際上能夠用任何你想使用URL替代,惟一的限制是所包含的URL必須使用'rest_framework'
命名空間。在Django 1.9+中,REST framework將自動設置,因此你也無須關心。
配置完成後,若是再次打開瀏覽器API界面並刷新頁面,你將在頁面右上角看到一個」Log in」連接。這就是DRF提供的登陸和登出入口,能夠用來完成認證。
而後進入到’rest_framework.urls’源碼,是能夠看到提供了’login’和’logout’兩個接口,分別用來登入和登陸的。代碼以下:前端
1 |
if django.VERSION < (1, 11): |
其中login接口調用LoginView視圖,logout接口調用LogoutView視圖。這兩個視圖都是django.contrib.auth應用提供的。在LogoutView視圖中,有這麼一個裝飾器@method_decorator(csrf_protect),是用來作CSRF code驗證的,就是作表單安全驗證的,防止跨站攻擊。而這個CSRF code是在返回HTML頁面的時候Django會自動註冊這麼一個CSRF code方法,而在template中會自動調用這個方法生成code值。在前端頁面元素form部分,能夠查看到name=」csrfmiddlewaretoken」標識,且在Django返回的 HTTP 響應的 cookie 裏,Django 會爲你添加一個csrftoken 字段,其值爲一個自動生成的token。這就是用來作表單安全驗證的,具體關於CSRF原理見Django章節。python
這裏要說明一個問題就是這個LoginView咱們是沒法直接拿來用的,由於它須要作CSRF驗證,而在先後端分離系統中不須要作CSRF驗證,這裏不存在站內站外的問題,自己就是跨站訪問的。那麼在咱們先後端分離項目中,如何作API接口的驗證呢?其實framework也已經提供了多種驗證方式。mysql
二. 身份驗證
REST framework提供了許多開箱即用的身份驗證方案,同時也容許你實施自定義方案。這裏須要明確一下用戶認證(Authentication)和用戶受權(Authorization)是兩個不一樣的概念,認證解決的是「有沒有」的問題,而受權解決的是「能不能」的問題。git
BasicAuthentication
該認證方案使用 HTTP Basic Authentication,並根據用戶的用戶名和密碼進行簽名。Basic Authentication 一般只適用於測試。github
SessionAuthentication
此認證方案使用 Django 的默認 session 後端進行認證。Session 身份驗證適用於與您的網站在同一會話環境中運行的 AJAX 客戶端。算法
TokenAuthentication
此認證方案使用簡單的基於令牌的 HTTP 認證方案。令牌身份驗證適用於 client-server 架構,例如本機桌面和移動客戶端。sql
RemoteUserAuthentication
這種身份驗證方案容許您將身份驗證委託給您的 Web 服務器,該服務器設置 REMOTE_USER 環境變量。數據庫
默認的認證方案可使用DEFAULT_AUTHENTICATION_CLASSES全局設置,在settings.py文件配置。在默認狀況下,DRF開啓了 BasicAuthentication 與 SessionAuthentication 的認證。django
1 |
REST_FRAMEWORK = { |
關於DRF,幾乎全部的配置都定義在MREST_FRAMEWORK變量中。另外,關於認證方式DRF默認會檢測配置在DEFAULT_AUTHENTICATION_CLASSES變量中的全部認證方式,只要有一個認證方式經過便可登陸成功。這裏的DEFAULT_AUTHENTICATION_CLASSES與Django中的MIDDLEWARE相似,在將request經過url映射到views以前,Django和DRF都會調用定義在MREST_FRAMEWORK變量中的類的一些方法。
另外,你還可使用基於APIView類的視圖,在每一個視圖或每一個視圖集的基礎上設置身份驗證方案。
1 |
from rest_framework.authentication import SessionAuthentication, BasicAuthentication |
須要明白的一點是,DRF的認證是在定義有權限類(permission_classes)的視圖下才有做用,且權限類(permission_classes)必需要求認證用戶才能訪問此視圖。若是沒有定義權限類(permission_classes),那麼也就意味着容許匿名用戶的訪問,天然牽涉不到認證相關的限制了。因此,通常在項目中的使用方式是在全局配置DEFAULT_AUTHENTICATION_CLASSES認證,而後會定義多個base views,根據不一樣的訪問需求來繼承不一樣的base views便可。
1 |
from rest_framework.permissions import ( |
另外,在先後端分離項目中通常不會使用 BasicAuthentication 與 SessionAuthentication 的認證方式。因此,咱們只須要關心 TokenAuthentication 認證方式便可。
三.TokenAuthentication
要使用TokenAuthentication
方案,你須要將認證類配置爲包含TokenAuthentication
。
1 |
REST_FRAMEWORK = { |
並在INSTALLED_APPS設置中另外包含 rest_framework.authtoken:
1 |
INSTALLED_APPS = ( |
注意: rest_framework.authtoken應用必定要放到INSTALLED_APPS,而且確保在更改設置後運行
python manage.py migrate
。 rest_framework.authtoken應用須要建立一張表用來存儲用戶與Token的對應關係。
數據庫遷移完成後,能夠看到多了一個authtoken_token表,表結構以下:
1
2
3
4
5
6
7
8
9
10
11
12 mysql> show create table authtoken_token\G
*************************** 1. row ***************************
Table: authtoken_token
Create Table: CREATE TABLE `authtoken_token` (
`key` varchar(40) NOT NULL,
`created` datetime(6) NOT NULL,
`user_id` int(11) NOT NULL,
PRIMARY KEY (`key`),
UNIQUE KEY `user_id` (`user_id`),
CONSTRAINT `authtoken_token_user_id_35299eff_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.01 sec)
其中「user_id」字段關聯到了用戶表。
- 配置URLconf
使用TokenAuthentication
時,你可能但願爲客戶提供一種機制,以獲取給定用戶名和密碼的令牌。 REST framework 提供了一個內置的視圖來支持這種行爲。要使用它,請將obtain_auth_token
視圖添加到您的 URLconf 中:1
2
3
4from rest_framework.authtoken import views
urlpatterns += [
url(r'^api-token-auth/', views.obtain_auth_token)
]
其中,r'^api-token-auth/'
部分實際上能夠用任何你想使用URL替代。
- 建立Token
你還須要爲用戶建立令牌,用戶令牌與用戶是一一對應的。若是你已經建立了一些用戶,則能夠爲全部現有用戶生成令牌,例如1
2
3
4
5from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token
for user in User.objects.all():
Token.objects.get_or_create(user=user)
你也能夠爲某個已經存在的用戶建立Token:
1 |
for user in User.objects.filter(username='admin'): |
建立成功後,會在Token表中生成對應的Token信息。
若是你但願每一個用戶都擁有一個自動生成的令牌,則只需捕捉用戶的post_save
信號便可。
1 |
from django.conf import settings |
請注意,你須要確保將此代碼片斷放置在已安裝的models.py模塊或 Django 啓動時將導入的其餘某個位置。
- 獲取Token
上面雖然介紹了多種建立Token的方式,其實咱們最簡單的就是隻須要配置一下urls.py,而後就能夠經過暴露的API來獲取Token了。當使用表單數據或 JSON 將有效的username和password字段發佈到視圖時,obtain_auth_token視圖將返回 JSON 響應:1
2$ curl -d "username=admin&password=admin123456" http://127.0.0.1:8000/api-token-auth/
{"token":"684b41712e8e38549504776613bd5612ba997616"}
請注意,缺省的obtain_auth_token
視圖顯式使用 JSON 請求和響應,而不是使用你設置的默認的渲染器和解析器類。
當咱們正常獲取到Token後,obtain_auth_token
視圖會自動幫咱們在Token表中建立對應的Token。源碼以下:
1 |
class ObtainAuthToken(APIView): |
默認狀況下,沒有權限或限制應用於obtain_auth_token
視圖。 若是您但願應用throttling
,則須要重寫視圖類,並使用throttle_classes
屬性包含它們。
若是你須要自定義obtain_auth_token
視圖,你能夠經過繼承ObtainAuthToken
視圖類來實現,並在你的urls.py中使用它。例如,你可能會返回超出token值的其餘用戶信息:
1 |
from rest_framework.authtoken.views import ObtainAuthToken |
還有urls.py:
1 |
urlpatterns += [ |
- 認證Token
當咱們獲取到Token後,就能夠拿着這個Token來認證其餘API了。對於客戶端進行身份驗證,令牌密鑰應包含在Authorization
HTTP header 中。關鍵字應以字符串文字 「Token」 爲前綴,用空格分隔兩個字符串。例如:1
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
注意: 若是你想在 header 中使用不一樣的關鍵字(例如Bearer),只需子類化TokenAuthentication
並設置keyword類變量。
若是成功經過身份驗證,TokenAuthentication
將提供如下憑據。
request.user是一個User實例,包含了用戶名及相關信息。
request.auth是一個rest_framework.authtoken.models.Token實例。
未經身份驗證的響應被拒絕將致使HTTP 401 Unauthorized的響應和相應的 WWW-Authenticate header。例如:
1 |
WWW-Authenticate: Token |
測試令牌認證的API,例如:
1 |
$ curl -X GET -H 'Authorization: Token 684b41712e8e38549504776613bd5612ba997616' http://127.0.0.1:8000/virtual/ |
注意: 若是您在生產中使用TokenAuthentication
,則必須確保您的 API 只能經過https訪問。
四. 認證源碼
使用 TokenAuthentication 認證方式,當認證成功後,在 request 中將提供了 request.user 和 request.auth 實例。其中 request.user 實例中有用戶信息,好比用戶名及用戶ID,而 request.auth 實例中有Token信息。那麼DRF是如何把 Token 轉換爲用戶信息呢?經過下面的源碼部分就能夠看到它們是如何轉換的。
基於 DRF 的請求處理,與常規的 url 配置不一樣,一般一個 Django 的 url 請求對應一個視圖函數,在使用 DRF 時,咱們要基於視圖對象,而後調用視圖對象的 as_view 函數,as_view 函數中會調用 rest_framework/views.py 中的 dispatch 函數,這個函數會根據 request 請求方法,去調用咱們在 view 對象中定義的對應的方法,就像這樣:
1 |
from rest_framework.authtoken import views |
這裏雖然直接調用 views.obtain_auth_token 方法,但進入到 views.obtain_auth_token 方法後仍是 DRF 模式,源碼以下:
1 |
obtain_auth_token = ObtainAuthToken.as_view() |
ObtainAuthToken 方法是繼承 DRF 中的 APIView 的 View 類:
1 |
class ObtainAuthToken(APIView): |
若是你是用 POST 方法請求 ObtainAuthToken,那麼 as_view() 函數會調用 dispatch 函數,dispatch 根據 request.METHOD,這裏是 POST,去調用 ObtainAuthToken 類的 POST 方法,這就跟一般的 url->view 的流程同樣了。
這裏須要注意的一點就是,DRF 中的 APIVIEW 是繼承 Django View 的,重寫了部分 as_view 方法,而調用 dispatch 函數是在 Django View 的 as_view 方法中作的事情,源碼部分以下:
1 |
class APIView(View): |
可是用戶認證是在執行請求 View 以前作的,因此其實就是在 dispatch 函數之中作的,具體見源碼 rest-framework/views.py 中 APIView 類中的 dispatch 函數:
1 |
class APIView(View): |
這裏的 self.initialize_request 也能夠關注一下,由於這裏的 request 對象,後面也會有調用的地方。
1 |
class APIView(View): |
其中 self.get_authenticators() 方法就是用來取 self.authentication_classes 變量。
1 |
class APIView(View): |
關於 authentication_classes 變量,上面已經給出了,就在 APIView 裏面 authentication_classes 字段。
而後就到了認證,重點在於 self.initial(request, *args, **kwargs) 函數,對於這個函數:
1 |
class APIView(View): |
這裏關注 self.perform_authentication(request) 驗證某個用戶,其實能夠看到權限檢查及限流也是在這裏作的。
1 |
class APIView(View): |
這裏 request.user 實際上是一個 @property 的函數,加 @property 表示調用 user 方法的時候不須要加括號「user()」,能夠直接調用 request.user 。而這裏的 request 對象就是上面 initialize_request 方法返回的,其中還返回了 DRF 定義的 request 對象,在 request 對象中有被 @property 裝飾的 user 方法。
1 |
class Request(object): |
重點來了,到了真正認證的方法了,關注 self._authenticate()
函數便可。此方法會循環嘗試每一個 DRF 認證方式。
1 |
class Request(object): |
那麼 self.authenticators 從哪兒來的呢?就是上面展現的,在 APIVIEW 類中的 authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES 獲得的。咱們上面在介紹 DRF 身份驗證時也說了,能夠把認證類定義在全局 settings 文件中,你還可使用基於 APIView 類的視圖,在每一個視圖或每一個視圖集的基礎上設置身份驗證方案。以下方式:
1 |
class ExampleView(APIView): |
當基於 APIView 類的視圖定義驗證或權限類時,至關於覆蓋了原生 APIVIEW 中的相關變量,天然就使用覆蓋後的變量了。authentication_classes 裏面放的就是能夠用來驗證一個用戶的類,他是一個元組,驗證用戶時,按照這個元組順序,直到驗證經過或者遍歷整個元組尚未經過。同理 self.check_permissions(request) 是驗證該用戶是否具備API的使用權限。關於對view控制的其餘類都在rest-framework/views.py的APIView類中定義了。
因爲咱們這裏只是拿 TokenAuthentication 認證說明,因此忽略 BasicAuthentication 和 SessionAuthentication 這兩種認證,其原理與TokenAuthentication 同樣。這樣,就進入到了 TokenAuthentication 認證,其源碼部分以下:
1 |
// 獲取header部分 Authorization 標識的信息 |
PS:DRF自帶的TokenAuthentication認證方式也很是簡單,同時弊端也很大,真正項目中用的較少。因爲須要存儲在數據庫表中,它在分佈式系統中用起來較爲麻煩,而且每次都須要查詢數據庫,增長數據庫壓力;同時它不支持Token的過時設置,這是一個很大的問題。在實際先後端分離項目中使用JWT(Json Web Token)標準的認證方式較多,每一個語言都有各自實現JWT的方式,Python也不例外。
五. JWT認證
瞭解完DRF自帶的TokenAuthentication認證方式的弊端以後,再來看JWT(Json Web Token)認證方式。它們兩個的原理是同樣的,就是認證用戶Token,而後取出對應的用戶。但JWT解決了兩個較大的問題。
第一,是不須要把Token存儲到數據庫表中了,而是根據必定的算法來算出用戶Token,而後每次用戶來驗證時再以一樣的方式生成對應的Token進行校驗。固然,實際JWT生成Token的方式仍是較爲複雜的,具體能夠看JWT協議相關文章。
第二,JWT對於生成的Token能夠設置過時時間,從而在必定程度提升了Token的安全性。
JWT的原理仍是稍稍有點麻煩的,裏面涉及了一些對稱加密和非對稱加密的算法。可是JWT使用起來確是很是簡單,Python中有PyJWT庫,而在DRF中也有對應的開源項目django-rest-framework-jwt
-
安裝
直接使用pip安裝便可,目前支持Python、Django、DRF主流版本:1
$ pip install djangorestframework-jwt
-
使用
在settings.py文件中,將JSONWebTokenAuthentication 添加到REST framework框架的DEFAULT_AUTHENTICATION_CLASSES1
2
3
4
5
6
7
8
9
10REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
一樣,你還可使用基於APIView類的視圖,在每一個視圖或每一個視圖集的基礎上設置身份驗證方案。與上面演示的 Token 認證同樣,這裏就不貼代碼了,儘量使用基於APIView類的視圖認證方式。
但使用基於APIView類的視圖認證方式時,不要忘記導入類。
1 |
from rest_framework_jwt.authentication import JSONWebTokenAuthentication |
在你的urls.py文件中添加如下URL路由,以便經過POST包含用戶名和密碼的令牌獲取。
1 |
from rest_framework_jwt.views import obtain_jwt_token |
若是你使用用戶名admin和密碼admin123456建立了用戶,則能夠經過在終端中執行如下操做來測試JWT是否正常工做。
1 |
$ curl -X POST -d "username=admin&password=admin123456" http://127.0.0.1:8000/api-token-auth/ |
或者,你可使用Django REST framework支持的全部內容類型來獲取身份驗證令牌。例如:
1 |
$ curl -X POST -H "Content-Type: application/json" -d '{"username":"admin","password":"admin123456"}' http://127.0.0.1:8000/api-token-auth/ |
如今訪問須要認證的API時,就必需要包含Authorization: JWT <your_token>
頭信息了:
1 |
$ curl -H "Authorization: JWT <your_token>" http://127.0.0.1:8000/virtual/ |
- 刷新Token
若是JWT_ALLOW_REFRESH爲True,能夠「刷新」未過時的令牌以得到具備更新到期時間的全新令牌。像以下這樣添加一個URL模式:1
2
3
4from rest_framework_jwt.views import refresh_jwt_token
urlpatterns += [
url(r'^api-token-refresh/', refresh_jwt_token)
]
使用方式就是將現有令牌傳遞到刷新API,以下所示: {"token": EXISTING_TOKEN}
。請注意,只有非過時的令牌纔有效。另外,響應JSON看起來與正常獲取令牌端點{"token": NEW_TOKEN}
相同。
1 |
$ curl -X POST -H "Content-Type: application/json" -d '{"token":"<EXISTING_TOKEN>"}' http://localhost:8000/api-token-refresh/ |
能夠重複使用令牌刷新(token1 -> token2 -> token3),但此令牌鏈存儲原始令牌(使用用戶名/密碼憑據獲取)的時間。做爲orig_iat,你只能將刷新令牌保留至JWT_REFRESH_EXPIRATION_DELTA。
刷新token以得到新的token的做用在於,持續保持活躍用戶登陸狀態。好比經過用戶密碼得到的token有效時間爲1小時,那麼也就意味着1小時後此token失效,用戶必須得從新登陸,這對於活躍用戶來講實際上是多餘的。若是這個用戶在這1小時內都在瀏覽網站,咱們不該該讓用戶從新登陸,就是在token沒有失效以前調用刷新接口爲用戶得到新的token。
- 認證Token
在一些微服務架構中,身份驗證由單個服務處理。此服務負責其餘服務委派確認用戶已登陸此身份驗證服務的責任。這一般意味着其餘服務將從用戶接收JWT傳遞給身份驗證服務,並在將受保護資源返回給用戶以前等待JWT有效的確認。添加如下URL模式:1
2
3
4from rest_framework_jwt.views import verify_jwt_token
urlpatterns += [
url(r'^api-token-verify/', verify_jwt_token)
]
將Token傳遞給驗證API,若是令牌有效,則返回令牌,返回狀態碼爲200。不然,它將返回400 Bad Request以及識別令牌無效的錯誤。
1 |
$ curl -X POST -H "Content-Type: application/json" -d '{"token":"<EXISTING_TOKEN>"}' http://localhost:8000/api-token-verify/ |
-
手動建立Token
有時候你可能但願手動生成令牌,例如在建立賬戶後當即將令牌返回給用戶。或者,你須要返回的信息不止是Token,可能還有用戶權限相關值。你能夠這樣作:1
2
3
4
5
6
7from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload) -
其餘設置
你能夠覆蓋一些其餘設置,好比變動Token過時時間,如下是全部可用設置的默認值。在settings.py文件中設置。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39JWT_AUTH = {
'JWT_ENCODE_HANDLER':
'rest_framework_jwt.utils.jwt_encode_handler',
'JWT_DECODE_HANDLER':
'rest_framework_jwt.utils.jwt_decode_handler',
'JWT_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_payload_handler',
'JWT_PAYLOAD_GET_USER_ID_HANDLER':
'rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler',
'JWT_RESPONSE_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_response_payload_handler',
// 這是用於簽署JWT的密鑰,確保這是安全的,不共享不公開的
'JWT_SECRET_KEY': settings.SECRET_KEY,
'JWT_GET_USER_SECRET_KEY': None,
'JWT_PUBLIC_KEY': None,
'JWT_PRIVATE_KEY': None,
'JWT_ALGORITHM': 'HS256',
// 若是祕鑰是錯誤的,它會引起一個jwt.DecodeError
'JWT_VERIFY': True,
'JWT_VERIFY_EXPIRATION': True,
'JWT_LEEWAY': 0,
// Token過時時間設置
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
'JWT_AUDIENCE': None,
'JWT_ISSUER': None,
// 是否開啓容許Token刷新服務,及限制Token刷新間隔時間,從原始Token獲取開始計算
'JWT_ALLOW_REFRESH': False,
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
// 定義與令牌一塊兒發送的Authorization標頭值前綴
'JWT_AUTH_HEADER_PREFIX': 'JWT',
'JWT_AUTH_COOKIE': None,
}