RESTful API 認證
和 Web 應用不同,RESTful APIs 通常是無狀態的, 也就意味着不應使用 sessions 或 cookies, 因此每個請求應附帶某種授權憑證,因為用戶授權狀態可能沒通過 sessions 或 cookies 維護, 常用的做法是每個請求都發送一個秘密的 access token 來認證用戶, 由於 access token 可以唯一識別和認證用戶, API 請求應通過 HTTPS 來防止 man-in-the-middle(MitM)中間人攻擊。
通常有下面幾種方式來發送 access token:
- HTTP 基本認證:access token 當作用戶名發送,應用在 access token 可安全存在 API 使用端的場景, 例如,API 使用端是運行在一台服務器上的程序。
- 請求參數:access token 當作 API URL 請求參數發送,例如
https://example.com/users?access-token=xxxxxxxx
, 由於大多數服務器都會保存請求參數到日志, 這種方式應主要用於JSONP
請求,因為它不能使用HTTP頭來發送access token - OAuth 2:使用者從認證服務器上獲取基於 OAuth2 協議的 access token, 然后通過 HTTP Bearer Tokens 發送到 API 服務器。
Django REST framework 認證
一、身份驗證
REST framework 提供了一些開箱即用的身份驗證方案,並且還允許你實現自定義方案。這里需要明確一下用戶認證(Authentication)和用戶授權(Authorization)是兩個不同的概念,認證解決的是“有沒有”的問題,而授權解決的是“能不能”的問題。
- BasicAuthentication
該認證方案使用 HTTP Basic Authentication,並根據用戶的用戶名和密碼進行簽名。Basic Authentication 通常只適用於測試。
- SessionAuthentication
此認證方案使用 Django 的默認 session 后端進行認證。Session 身份驗證適用於與您的網站在同一會話環境中運行的 AJAX 客戶端。
- TokenAuthentication
此認證方案使用簡單的基於令牌的 HTTP 認證方案。令牌身份驗證適用於 client-server 架構,例如本機桌面和移動客戶端。
- RemoteUserAuthentication
這種身份驗證方案允許您將身份驗證委托給您的 Web 服務器,該服務器設置 REMOTE_USER 環境變量。
默認的認證方案可以使用DEFAULT_AUTHENTICATION_CLASSES
全局設置,在settings.py
文件配置。在默認情況下,DRF開啟了 BasicAuthentication 與 SessionAuthentication 的認證。
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
類的視圖,在每個視圖或每個視圖集的基礎上設置身份驗證方案。
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即可。
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
。
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', ) }
並在INSTALLED_APPS
設置中另外包含 rest_framework.authtoken
:
INSTALLED_APPS = ( ... 'rest_framework.authtoken' )
注意: rest_framework.authtoken應用一定要放到INSTALLED_APPS,並且確保在更改設置后運行python manage.py migrate
。 rest_framework.authtoken應用需要創建一張表用來存儲用戶與Token的對應關系。
數據庫遷移完成后,可以看到多了一個authtoken_token表,表結構如下
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
其中“user_id”字段關聯到了用戶表。
-
配置URLconf
使用TokenAuthentication
時,你可能希望為客戶提供一種機制,以獲取給定用戶名和密碼的令牌。 REST framework 提供了一個內置的視圖來支持這種行為。要使用它,請將obtain_auth_token
視圖添加到您的 URLconf 中:
from rest_framework.authtoken import views urlpatterns += [ url(r'^api-token-auth/', views.obtain_auth_token) ]
其中,r'^api-token-auth/'
部分實際上可以用任何你想使用URL替代。
-
創建Token
你還需要為用戶創建令牌,用戶令牌與用戶是一一對應的。如果你已經創建了一些用戶,則可以為所有現有用戶生成令牌,例如:
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:
for user in User.objects.filter(username='admin'): Token.objects.get_or_create(user=user)
創建成功后,會在Token表中生成對應的Token信息。
如果你希望每個用戶都擁有一個自動生成的令牌,則只需捕捉用戶的post_save
信號即可
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 響應:
$ 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。源碼如下:
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
值的其他用戶信息:
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
:
urlpatterns += [ url(r'^api-token-auth/', CustomAuthToken.as_view()) ]
- 認證Token
當我們獲取到Token后,就可以拿着這個Token來認證其他API了。對於客戶端進行身份驗證,令牌密鑰應包含在 Authorization
HTTP header 中。關鍵字應以字符串文字 “Token” 為前綴,用空格分隔兩個字符串。例如:
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
注意: 如果你想在 header 中使用不同的關鍵字(例如Bearer
),只需子類化TokenAuthentication
並設置keyword
類變量。
如果成功通過身份驗證,TokenAuthentication
將提供以下憑據。
request.user
是一個User
實例,包含了用戶名及相關信息。request.auth
是一個rest_framework.authtoken.models.Token
實例。
未經身份驗證的響應被拒絕將導致HTTP 401 Unauthorized
的響應和相應的 WWW-Authenticate header。例如:
WWW-Authenticate: Token
測試令牌認證的API,例如:
$ curl -X GET -H 'Authorization: Token 684b41712e8e38549504776613bd5612ba997616' http://127.0.0.1:8000/virtual/
注意: 如果您在生產中使用TokenAuthentication,則必須確保您的 API 只能通過https訪問。
PS:DRF自帶的TokenAuthentication認證方式也非常簡單,同時弊端也很大,真正項目中用的較少。由於需要存儲在數據庫表中,它在分布式系統中用起來較為麻煩,並且每次都需要查詢數據庫,增加數據庫壓力;同時它不支持Token的過期設置,這是一個很大的問題。在實際前后端分離項目中使用JWT(Json Web Token)標准的認證方式較多,每個語言都有各自實現JWT的方式,Python也不例外。
參考:http://www.ywnds.com/?p=14967