Django REST framework API認證(包括JWT認證)

 

Django REST framework API認證(包含JWT認證) + 權限

Django REST framework API認證(包含JWT認證)

一. 背景

在咱們學習Django Rest Framework(簡稱DRF)時,其很是友好地給咱們提供了一個可瀏覽API的界面。不少測試工做均可以在可瀏覽API界面完成測試。要使用可瀏覽API界面很簡單,只須要在urls.py文件中添加以下部分便可。html

1
2
3
4
from django.conf.urls import include
urlpatterns += [
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]

 

其中,r'^api-auth/'部分實際上能夠用任何你想使用URL替代,惟一的限制是所包含的URL必須使用'rest_framework'命名空間。在Django 1.9+中,REST framework將自動設置,因此你也無須關心。
配置完成後,若是再次打開瀏覽器API界面並刷新頁面,你將在頁面右上角看到一個」Log in」連接。這就是DRF提供的登陸和登出入口,能夠用來完成認證。
而後進入到’rest_framework.urls’源碼,是能夠看到提供了’login’和’logout’兩個接口,分別用來登入和登陸的。代碼以下:前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if django.VERSION < (1, 11):
login = views.login
login_kwargs = {'template_name': 'rest_framework/login.html'}
logout = views.logout
else:
login = views.LoginView.as_view(template_name='rest_framework/login.html')
login_kwargs = {}
logout = views.LogoutView.as_view()


app_name = 'rest_framework'
urlpatterns = [
url(r'^login/$', login, login_kwargs, name='login'),
url(r'^logout/$', logout, name='logout'),
]

 

其中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
2
3
4
5
6
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
)
}

 

關於DRF,幾乎全部的配置都定義在MREST_FRAMEWORK變量中。另外,關於認證方式DRF默認會檢測配置在DEFAULT_AUTHENTICATION_CLASSES變量中的全部認證方式,只要有一個認證方式經過便可登陸成功。這裏的DEFAULT_AUTHENTICATION_CLASSES與Django中的MIDDLEWARE相似,在將request經過url映射到views以前,Django和DRF都會調用定義在MREST_FRAMEWORK變量中的類的一些方法。
另外,你還可使用基於APIView類的視圖,在每一個視圖或每一個視圖集的基礎上設置身份驗證方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

class ExampleView(APIView):
authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticated,)

def get(self, request, format=None):
content = {
'user': unicode(request.user), # `django.contrib.auth.User` instance.
'auth': unicode(request.auth), # None
}
return Response(content)

 

須要明白的一點是,DRF的認證是在定義有權限類(permission_classes)的視圖下才有做用,且權限類(permission_classes)必需要求認證用戶才能訪問此視圖。若是沒有定義權限類(permission_classes),那麼也就意味着容許匿名用戶的訪問,天然牽涉不到認證相關的限制了。因此,通常在項目中的使用方式是在全局配置DEFAULT_AUTHENTICATION_CLASSES認證,而後會定義多個base views,根據不一樣的訪問需求來繼承不一樣的base views便可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from rest_framework.permissions import (
IsAuthenticated,
IsAdminUser,
IsAuthenticatedOrReadOnly
)

class BaseView(APIView):
'''普通用戶'''
permission_classes = (
IsOwnerOrReadOnly,
IsAuthenticated
)


class SuperUserpermissions(APIView):
'''超級用戶'''
permission_classes = (IsAdminUser,)


class NotLogin(APIView):
'''匿名用戶'''
pass

 

另外,在先後端分離項目中通常不會使用 BasicAuthentication 與 SessionAuthentication 的認證方式。因此,咱們只須要關心 TokenAuthentication 認證方式便可。

三.TokenAuthentication

要使用TokenAuthentication方案,你須要將認證類配置爲包含TokenAuthentication

1
2
3
4
5
6
7
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
)
}

並在INSTALLED_APPS設置中另外包含 rest_framework.authtoken:

1
2
3
4
INSTALLED_APPS = (
...
'rest_framework.authtoken'
)

 

注意: 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
    4
    from 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
    5
    from 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
2
for user in User.objects.filter(username='admin'):
Token.objects.get_or_create(user=user)

 

建立成功後,會在Token表中生成對應的Token信息。

若是你但願每一個用戶都擁有一個自動生成的令牌,則只需捕捉用戶的post_save信號便可。

1
2
3
4
5
6
7
8
9
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token

@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)

 

請注意,你須要確保將此代碼片斷放置在已安裝的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
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
39
40
41
class ObtainAuthToken(APIView):
throttle_classes = ()
permission_classes = ()
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
renderer_classes = (renderers.JSONRenderer,)
serializer_class = AuthTokenSerializer
if coreapi is not None and coreschema is not None:
schema = ManualSchema(
fields=[
coreapi.Field(
name="username",
required=True,
location='form',
schema=coreschema.String(
title="Username",
description="Valid username for authentication",
),
),
coreapi.Field(
name="password",
required=True,
location='form',
schema=coreschema.String(
title="Password",
description="Valid password for authentication",
),
),
],
encoding="application/json",
)

def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data,
context={'request': request})
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key})


obtain_auth_token = ObtainAuthToken.as_view()

 

默認狀況下,沒有權限或限制應用於obtain_auth_token視圖。 若是您但願應用throttling,則須要重寫視圖類,並使用throttle_classes屬性包含它們。

若是你須要自定義obtain_auth_token視圖,你能夠經過繼承ObtainAuthToken視圖類來實現,並在你的urls.py中使用它。例如,你可能會返回超出token值的其餘用戶信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from rest_framework.response import Response

class CustomAuthToken(ObtainAuthToken):

def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data,
context={'request': request})
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user)
return Response({
'token': token.key,
'user_id': user.pk,
'email': user.email
})

 

還有urls.py:

1
2
3
urlpatterns += [
url(r'^api-token-auth/', CustomAuthToken.as_view())
]

 

  • 認證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
2
3
4
from rest_framework.authtoken import views
urlpatterns += [
url(r'^api-token-auth/', views.obtain_auth_token)
]

 

這裏雖然直接調用 views.obtain_auth_token 方法,但進入到 views.obtain_auth_token 方法後仍是 DRF 模式,源碼以下:

1
obtain_auth_token = ObtainAuthToken.as_view()

 

ObtainAuthToken 方法是繼承 DRF 中的 APIView 的 View 類:

1
2
3
4
5
6
7
class ObtainAuthToken(APIView):
throttle_classes = ()
permission_classes = ()
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
renderer_classes = (renderers.JSONRenderer,)
serializer_class = AuthTokenSerializer
....

 

若是你是用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class APIView(View):
....
@classmethod
def as_view(cls, **initkwargs):
"""
Store the original class on the view function.

This allows us to discover information about the view when we do URL
reverse lookups. Used for breadcrumb generation.
"""
if isinstance(getattr(cls, 'queryset', None), models.query.QuerySet):
def force_evaluation():
raise RuntimeError(
'Do not evaluate the `.queryset` attribute directly, '
'as the result will be cached and reused between requests. '
'Use `.all()` or call `.get_queryset()` instead.'
)
cls.queryset._fetch_all = force_evaluation

view = super(APIView, cls).as_view(**initkwargs)
view.cls = cls
view.initkwargs = initkwargs

 

可是用戶認證是在執行請求 View 以前作的,因此其實就是在 dispatch 函數之中作的,具體見源碼 rest-framework/views.py 中 APIView 類中的 dispatch 函數:

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
class APIView(View):
// 從settings文件中獲取認證類、限流類、權限類
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
....

def dispatch(self, request, *args, **kwargs):
"""
`.dispatch()` is pretty much the same as Django's regular dispatch,
but with extra hooks for startup, finalize, and exception handling.
"""
self.args = args
self.kwargs = kwargs

// 封裝request,對原始request對象增長一些功能,好比認證類,都是在initialize_request方法中完成
request = self.initialize_request(request, *args, **kwargs)
self.request = request
self.headers = self.default_response_headers # deprecate?

try:
// 調用self.initial進行用戶認證
self.initial(request, *args, **kwargs)

# Get the appropriate handler method
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(),
self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed

response = handler(request, *args, **kwargs)

except Exception as exc:
response = self.handle_exception(exc)

self.response = self.finalize_response(request, response, *args, **kwargs)
return self.response

 

這裏的 self.initialize_request 也能夠關注一下,由於這裏的 request 對象,後面也會有調用的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class APIView(View):
....
def initialize_request(self, request, *args, **kwargs):
"""
Returns the initial request object.
"""
parser_context = self.get_parser_context(request)

return Request(
request,
parsers=self.get_parsers(),
authenticators=self.get_authenticators(), // 這裏把認證類封裝進行了request裏面
negotiator=self.get_content_negotiator(),
parser_context=parser_context
)

 

其中 self.get_authenticators() 方法就是用來取 self.authentication_classes 變量。

1
2
3
4
5
6
7
class APIView(View):
....
def get_authenticators(self):
"""
Instantiates and returns the list of authenticators that this view can use.
"""
return [auth() for auth in self.authentication_classes]

 

關於 authentication_classes 變量,上面已經給出了,就在 APIView 裏面 authentication_classes 字段。

而後就到了認證,重點在於 self.initial(request, *args, **kwargs) 函數,對於這個函數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class APIView(View):
....
def initial(self, request, *args, **kwargs):
"""
Runs anything that needs to occur prior to calling the method handler.
"""
self.format_kwarg = self.get_format_suffix(**kwargs)

# Perform content negotiation and store the accepted info on the request
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg

# Determine the API version, if versioning is in use.
version, scheme = self.determine_version(request, *args, **kwargs)
request.version, request.versioning_scheme = version, scheme

# Ensure that the incoming request is permitted
self.perform_authentication(request) // 用戶認證
self.check_permissions(request) // 權限檢查
self.check_throttles(request) // 限流檢查

 

這裏關注 self.perform_authentication(request) 驗證某個用戶,其實能夠看到權限檢查及限流也是在這裏作的。

1
2
3
4
5
6
7
8
9
10
11
class APIView(View):
....
def perform_authentication(self, request):
"""
Perform authentication on the incoming request.

Note that if you override this and simply 'pass', then authentication
will instead be performed lazily, the first time either
`request.user` or `request.auth` is accessed.
"""
request.user

 

這裏 request.user 實際上是一個 @property 的函數,加 @property 表示調用 user 方法的時候不須要加括號「user()」,能夠直接調用 request.user 。而這裏的 request 對象就是上面 initialize_request 方法返回的,其中還返回了 DRF 定義的 request 對象,在 request 對象中有被 @property 裝飾的 user 方法。

1
2
3
4
5
6
7
8
9
10
11
12
class Request(object):
....
@property
def user(self):
"""
Returns the user associated with the current request, as authenticated
by the authentication classes provided to the request.
"""
if not hasattr(self, '_user'):
with wrap_attributeerrors():
self._authenticate()
return self._user

 

重點來了,到了真正認證的方法了,關注 self._authenticate()函數便可。此方法會循環嘗試每一個 DRF 認證方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Request(object):
....
def _authenticate(self):
"""
嘗試使用每一個身份驗證明例驗證請求
self.authenticators = [BasicAuthentication, SessionAuthentication, TokenAuthentication]
"""
for authenticator in self.authenticators:
try:
user_auth_tuple = authenticator.authenticate(self)
except exceptions.APIException:
// 若是authenticate方法拋出異常,則執行self._not_authenticated方法,至關於匿名用戶,沒有經過認證
self._not_authenticated()
raise

if user_auth_tuple is not None:
self._authenticator = authenticator
self.user, self.auth = user_auth_tuple
return

// 若是沒有設置認證類的話,也至關於匿名用戶,沒有經過認證
self._not_authenticated()

 

那麼 self.authenticators 從哪兒來的呢?就是上面展現的,在 APIVIEW 類中的 authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES 獲得的。咱們上面在介紹 DRF 身份驗證時也說了,能夠把認證類定義在全局 settings 文件中,你還可使用基於 APIView 類的視圖,在每一個視圖或每一個視圖集的基礎上設置身份驗證方案。以下方式:

1
2
3
class ExampleView(APIView):
authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticated,)

 

當基於 APIView 類的視圖定義驗證或權限類時,至關於覆蓋了原生 APIVIEW 中的相關變量,天然就使用覆蓋後的變量了。authentication_classes 裏面放的就是能夠用來驗證一個用戶的類,他是一個元組,驗證用戶時,按照這個元組順序,直到驗證經過或者遍歷整個元組尚未經過。同理 self.check_permissions(request) 是驗證該用戶是否具備API的使用權限。關於對view控制的其餘類都在rest-framework/views.py的APIView類中定義了。

因爲咱們這裏只是拿 TokenAuthentication 認證說明,因此忽略 BasicAuthentication 和 SessionAuthentication 這兩種認證,其原理與TokenAuthentication 同樣。這樣,就進入到了 TokenAuthentication 認證,其源碼部分以下:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// 獲取header部分 Authorization 標識的信息
def get_authorization_header(request):
"""
Return request's 'Authorization:' header, as a bytestring.

Hide some test client ickyness where the header can be unicode.
"""
auth = request.META.get('HTTP_AUTHORIZATION', b'')
if isinstance(auth, text_type):
# Work around django test client oddness
auth = auth.encode(HTTP_HEADER_ENCODING)
return auth


// 解析並認證 Token
class TokenAuthentication(BaseAuthentication):
"""
Simple token based authentication.

Clients should authenticate by passing the token key in the "Authorization"
HTTP header, prepended with the string "Token ". For example:

Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a
"""

keyword = 'Token'
model = None

def get_model(self):
if self.model is not None:
return self.model
from rest_framework.authtoken.models import Token
return Token

"""
A custom token model may be used, but must have the following properties.

* key -- The string identifying the token
* user -- The user to which the token belongs
"""

def authenticate(self, request):
// 經過上面的get_authorization_header方法獲得Token信息
// auth = [b'Token', b'684b41712e8e38549504776613bd5612ba997616']
auth = get_authorization_header(request).split()

// 經過獲取Token關鍵字,並與keyword變量比對,來判斷是不是Token方式認證
if not auth or auth[0].lower() != self.keyword.lower().encode():
return None

// auth長度等於2時纔是合法值,繼續往下進行
if len(auth) == 1:
msg = _('Invalid token header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid token header. Token string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)

// 進行token解碼,從bytes編碼格式轉爲字符串
try:
token = auth[1].decode()
except UnicodeError:
msg = _('Invalid token header. Token string should not contain invalid characters.')
raise exceptions.AuthenticationFailed(msg)

// 把轉換過的token傳給認證憑證方法進行驗證
return self.authenticate_credentials(token)

// 驗證憑證方法進行接收token並進行驗證
def authenticate_credentials(self, key):
// 獲取Token模型實例
model = self.get_model()
try:
// 使用select_related方法獲取相應外鍵對應的對象(就是兩表Join),而後進行Token過濾查詢
token = model.objects.select_related('user').get(key=key)
except model.DoesNotExist:
raise exceptions.AuthenticationFailed(_('Invalid token.'))

// 判斷用戶是不是登陸成功
if not token.user.is_active:
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

// 返回user實例,及token實例
return (token.user, token)

 

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_CLASSES

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    REST_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
2
3
4
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns += [
url(r'^api-token-auth/', 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
    4
    from 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
    4
    from 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
    7
    from 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
    39
    JWT_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,
    }
相關文章
相關標籤/搜索