當(dāng)我們通過django框架創(chuàng)建RESTful API對外提供后,我們希望這些API只有相關(guān)權(quán)限的人才可以調(diào)用奕筐,這個(gè)怎么做呢?可以采用在django框架之上rest-framework去做变骡,當(dāng)然必須安裝rest-framework,然后在django的setting中的INSTALLED_APPS加上rest_framework离赫。
基于rest-framework的請求處理,與常規(guī)的url配置不同塌碌,通常一個(gè)django的url請求對應(yīng)一個(gè)視圖函數(shù)渊胸,在使用rest-framework時(shí),我們要基于視圖對象台妆,然后調(diào)用視圖對象的as_view函數(shù)翎猛,as_view函數(shù)中會(huì)調(diào)用rest_framework/views.py中的dispatch函數(shù)胖翰,這個(gè)函數(shù)會(huì)根據(jù)request請求方法,去調(diào)用我們在view對象中定義的對應(yīng)的方法切厘,就像這樣:
urlpatterns = [
url(
r"^test/?", testView.as_view(),
)]
testView是繼承rest-framework中的APIView的View類
from rest_framework import status
from rest_framework.authentication import BasicAuthentication
from rest_framework.permissions import IsAuthenticated
class testView(APIView):
authentication_classes = (
BasicAuthentication,
# SessionAuthentication,
# TokenAuthentication,
)
permission_classes = (
IsAuthenticated,
)
def get(self, request):
pass
如果你是用get方法請求test,那么as_view()函數(shù)會(huì)調(diào)用dispatch函數(shù)萨咳,dispatch根據(jù)request.METHOD,這里是get疫稿,去調(diào)用testView類的get方法培他,這就跟通常的url->視圖函數(shù)的流程一樣了。
但是權(quán)限驗(yàn)證是在執(zhí)行請求之前做的遗座,所以其實(shí)就是在dispatch函數(shù)之中做的舀凛,具體見源碼rest-framework/views.py中APIView類中的dispatch函數(shù):
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 = self.initialize_request(request, *args, **kwargs)
self.request = request
self.headers = self.default_response_headers # deprecate?
try:
self.initial(request, *args, **kwargs) #重點(diǎn)關(guān)注
# 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
其實(shí)重點(diǎn)在于 self.initial(request, *args, **kwargs)函數(shù),對于這個(gè)函數(shù)
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) #重點(diǎn)關(guān)注
self.check_permissions(request) #重點(diǎn)關(guān)注
self.check_throttles(request) #重點(diǎn)關(guān)注
self.perform_authentication(request) 驗(yàn)證某個(gè)用戶
"""
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其實(shí)是一個(gè)@property的函數(shù)
@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'):
self._authenticate()
return self._user
所以關(guān)注self._authenticate()函數(shù)就好了
def _authenticate(self):
"""
Attempt to authenticate the request using each authentication instance
in turn.
Returns a three-tuple of (authenticator, user, authtoken).
"""
for authenticator in self.authenticators:
try:
user_auth_tuple = authenticator.authenticate(self) #重點(diǎn)
except exceptions.APIException:
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()
驗(yàn)證用戶就是authenticator.authenticate途蒋,那么self.authenticators從哪兒來的呢猛遍?
關(guān)注文章開頭給出的testView類中的
authentication_classes = (
BasicAuthentication,
)
permission_classes = (
IsAuthenticated,
)
authentication_classes 里面放的就是可以用來驗(yàn)證一個(gè)用戶的類,他是一個(gè)元組碎绎,驗(yàn)證用戶時(shí)螃壤,按照這個(gè)元組順序,直到驗(yàn)證通過或者遍歷整個(gè)元組還沒有通過筋帖。
同理self.check_permissions(request)是驗(yàn)證該用戶是否具有API的使用權(quán)限奸晴。關(guān)于對view控制的其他類都在rest-framework/views.py的APIView類中定義了。
class APIView(View):
# The following policies may be set at either globally, or per-view.
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
metadata_class = api_settings.DEFAULT_METADATA_CLASS
versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
具體可參見http://www.django-rest-framework.org/api-guide/views/
所以日麸,這里剩下的就是實(shí)現(xiàn)校驗(yàn)用戶的BasicAuthentication類了寄啼。對于像BasicAuthentication這樣的類,必須實(shí)現(xiàn)authenticate方法代箭,并且返回一個(gè)用戶墩划,賦值給request.user,這個(gè)request.user就是系統(tǒng)中進(jìn)行用戶認(rèn)證的user對象嗡综,后續(xù)的權(quán)限驗(yàn)證一般都是通過判斷request.user的user對象是否擁有某個(gè)權(quán)限乙帮。rest-framework默認(rèn)的就是BasicAuthentication,也就是跟admin登陸用的一樣的認(rèn)證极景。其源碼如下:
class BasicAuthentication(BaseAuthentication):
"""
HTTP Basic authentication against username/password.
"""
www_authenticate_realm = 'api'
def authenticate(self, request):
"""
Returns a `User` if a correct username and password have been supplied
using HTTP Basic authentication. Otherwise returns `None`.
"""
auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != b'basic':
return None
if len(auth) == 1:
msg = _('Invalid basic header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid basic header. Credentials string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
try:
auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')
except (TypeError, UnicodeDecodeError, binascii.Error):
msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
raise exceptions.AuthenticationFailed(msg)
userid, password = auth_parts[0], auth_parts[2]
# userid就是用戶名 password 就是密碼
return self.authenticate_credentials(userid, password)
def authenticate_credentials(self, userid, password):
"""
Authenticate the userid and password against username and password.
"""
credentials = {
get_user_model().USERNAME_FIELD: userid,
'password': password
}
user = authenticate(**credentials) #重點(diǎn)關(guān)注
if user is None:
raise exceptions.AuthenticationFailed(_('Invalid username/password.'))
if not user.is_active:
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
return (user, None)
def authenticate_header(self, request):
return 'Basic realm="%s"' % self.www_authenticate_realm
對于上述函數(shù)調(diào)用流程察净,重點(diǎn)關(guān)注user = authenticate(**credentials),這里的authenticate其實(shí)是from django.contrib.auth import authenticate導(dǎo)入的authenticate,因?yàn)樵谡{(diào)用時(shí)authenticate前面沒有加self或者其他對象盼樟,在rest-framework的authentication.py中全局的authenticate就只有開始import的authenticate氢卡,那么在django/contrib/auth/init.py中的authenticate源碼如下:
def authenticate(**credentials):
"""
If the given credentials are valid, return a User object.
"""
for backend, backend_path in _get_backends(return_tuples=True):
try:
inspect.getcallargs(backend.authenticate, **credentials)
except TypeError:
# This backend doesn't accept these credentials as arguments. Try the next one.
continue
try:
user = backend.authenticate(**credentials) #重點(diǎn)關(guān)注
except PermissionDenied:
# This backend says to stop in our tracks - this user should not be allowed in at all.
break
if user is None:
continue
# Annotate the user object with the path of the backend.
user.backend = backend_path
return user
# The credentials supplied are invalid to all backends, fire signal
user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials))
這里的backend其實(shí)就是settings中指定的AUTHENTICATION_BACKENDS,一般也就是django/contrib/auth/backends.py中的ModelBackend類晨缴,那么看看backend.authenticate干了什么译秦?
class ModelBackend(object):
"""
Authenticates against settings.AUTH_USER_MODEL.
"""
def authenticate(self, username=None, password=None, **kwargs):
UserModel = get_user_model()
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
try:
user = UserModel._default_manager.get_by_natural_key(username)
except UserModel.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a non-existing user (#20760).
UserModel().set_password(password)
else:
if user.check_password(password) and self.user_can_authenticate(user):
return user
也就是去跟數(shù)據(jù)庫比對,用戶名和密碼是否匹配。如果匹配返回user
接下來就到了self.check_permissions(request)筑悴,
def check_permissions(self, request):
"""
Check if the request should be permitted.
Raises an appropriate exception if the request is not permitted.
"""
for permission in self.get_permissions():
if not permission.has_permission(request, self):
self.permission_denied(
request, message=getattr(permission, 'message', None)
)
如果存在驗(yàn)證不通過们拙,那么就執(zhí)行self.permission_denied,
def permission_denied(self, request, message=None):
"""
If request is not permitted, determine what kind of exception to raise.
"""
if request.authenticators and not request.successful_authenticator:
raise exceptions.NotAuthenticated()
raise exceptions.PermissionDenied(detail=message)
然后這個(gè)異常在dispatch函數(shù)中被捕捉阁吝,當(dāng)做結(jié)果傳遞給response睛竣。
對于API的權(quán)限,也可以在settings中進(jìn)行全局設(shè)置求摇,具體過程可參照:
http://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/
整個(gè)練習(xí)的開始是:
http://www.django-rest-framework.org/tutorial/quickstart/
而關(guān)于BasicAuthentication認(rèn)證的解釋,可以參見:
https://www.ibm.com/support/knowledgecenter/en/SSGMCP_5.1.0/com.ibm.cics.ts.internet.doc/topics/dfhtl2a.html
當(dāng)一次授權(quán)通過后殊者,再一次訪問這個(gè)API時(shí)与境,這時(shí)候的用戶名和密碼從哪兒來的?下一次來訪問的時(shí)候就是通過服務(wù)器通過cookie返回給client的sessionid去驗(yàn)證猖吴,通過谷歌瀏覽器用F12調(diào)試可以得到驗(yàn)證摔刁,下一次通過瀏覽器訪問時(shí),就會(huì)帶上類似下面的內(nèi)容:Cookie:csrftoken=FsvNBXNdyyUvECZwTMpj59DAnGPPurRFM8RqFQoVuvizeQ6OB1nSK3KxS8mjJiWE; sessionid=xxtj52tsqgow9kbur6e304fd1ygn7603海蔽。至于指定的BasicAuthentication認(rèn)證為什么第二次會(huì)走SessionAuthentication認(rèn)證共屈,暫時(shí)還不知道。
如果APIView類中的authentication_classes使用的是SessionAuthentication去驗(yàn)證党窜,那么就要在請求頭部帶上sessionid拗引,請求如下:
#!/usr/bin/env python
#coding=utf-8
import urllib2
url = 'http://127.0.0.1:8000/testapiview'
#headers={'Authorization': 'Token cc6d79b3669ceaea45efe028ad8e23fdc978b786'}
headers = {'Cookie': 'csrftoken=FsvNBXNdyyUvECZwTMpj59DAnGPPurRFM8RqFQoVuvizeQ6OB1nSK3KxS8mjJiWE; sessionid=xxtj52tsqgow9kbur6e304fd1ygn7603'}
request = urllib2.Request(url)
for header in headers:
request.add_header(header,headers[header])
res = urllib2.urlopen(request)
那可能會(huì)問sessionid從哪兒來,按照常規(guī)幌衣,我們登陸一個(gè)系統(tǒng)后矾削,服務(wù)端會(huì)根據(jù)我們第一次登陸提供的用戶名和密碼還有訪問的域名等其他信息生成一個(gè)session對象保存在服務(wù)端,并通過寫cookie返回給client豁护,當(dāng)下一次訪問相同的域名時(shí)就會(huì)在cookie中帶上相應(yīng)的sessionid信息哼凯,SessionAuthentication模塊根據(jù)sessionid去進(jìn)行權(quán)限驗(yàn)證。
同理楚里,如果如果APIView類中的authentication_classes使用的是TokenAuthentication去驗(yàn)證断部,那么就要在請求頭部帶上Token信息,代碼例子跟上面的session驗(yàn)證一樣班缎,只是把header換成token蝴光。同樣,token從哪兒來呢吝梅?Token一般是在服務(wù)器上跟用戶一起綁定生成的虱疏,然后存放在token數(shù)據(jù)庫中。在rest-framework中苏携,要是用Token驗(yàn)證做瞪,那么在settings中的INSTALL_APP中還要加上'rest_framework.authtoken',用來生成存放token的數(shù)據(jù)庫,當(dāng)創(chuàng)建好token后装蓬,下一次訪問時(shí)著拭,帶上Token就好了。我們一般都是用帶token的方式進(jìn)行訪問牍帚,在傳輸token過程中一般用https防止token泄漏儡遮,當(dāng)然我們也最好讓token有時(shí)效性,然后定時(shí)更新token暗赶,這樣保證多數(shù)情況下鄙币,即使token泄漏也不會(huì)造成很大安全風(fēng)險(xiǎn)。這個(gè)例子可以參考:
https://chrisbartos.com/articles/how-to-implement-token-authentication-with-django-rest-framework/
http://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication
當(dāng)然蹂随,你也可以自己定制用戶認(rèn)證的類十嘿,但是要明確一點(diǎn),調(diào)用這個(gè)認(rèn)證的類的authenticate函數(shù)一定要返回一個(gè)用戶對象給request.user還有request.auth岳锁,后續(xù)的權(quán)限驗(yàn)證都是依據(jù)這兩個(gè)進(jìn)行的绩衷,比如下面:
#!/usr/bin/env python
# coding: utf-8
import logging
from urlparse import urljoin
from urllib import quote as urlquote
from datetime import datetime
from django.conf import settings
from rest_framework.authentication import BaseAuthentication
from rest_framework import exceptions
import requests
from core.utils import getitems, retry, iso86012datetime
logger = logging.getLogger(__name__)
#定義一個(gè)用戶user類,authenticate函數(shù)會(huì)返回這樣一個(gè)實(shí)例給request.user
#這個(gè)類可以參照django/contrib/auth/models.py中的User類
class KeystoneTenant(object):
is_staff = True
is_superuser = False
def __init__(self, id, name, uid, user):
self.id = id
self.name = name
self.uid = uid
self.user = user
-------------------------------------省略---------------------------------------
@property
def pk(self):
return self.id
@property #必須包含的方法
def username(self):
return self.name
@property #必須包含的方法
def email(self):
return settings.ADMIN_EMAIL
@property #必須包含的方法
def is_authenticated(self):
return self.is_staff
@classmethod
def from_access_info(cls, access_info):
if not access_info:
return None
tenant = cls(
id=getitems(access_info, ["access", "token", "tenant", "id"]),
name=getitems(access_info, ["access", "token", "tenant", "name"]),
uid=getitems(access_info, ["access", "user", "id"]),
user=getitems(access_info, ["access", "user", "name"]),
)
tenant.is_superuser = bool(getitems(access_info, [
"access", "metadata", "is_admin",
], cls.is_superuser))
tenant.is_staff = bool(getitems(access_info, [
"access", "token", "tenant", "enabled",
], cls.is_staff))
return tenant
def get_x_auth_token(tenant_name, user_name, password):
logger.info("Get X-AUTH-TOKEN by tenant: %s", tenant_name)
response = requests.post(
urljoin(
settings.KEYSTONE_ENDPOINT, "/v2.0/tokens"
), json={
"auth": {
"passwordCredentials": {
"username": user_name,
"password": password,
},
"tenantName": tenant_name,
}
},
)
result = response.json()
try:
token = result["access"]["token"]["id"]
expiry = result["access"]["token"]["expires"]
except KeyError:
logger.exception(
"Unexpected response from keystone service: %s", result,
)
raise
return token, iso86012datetime(expiry)
#放在authentication_classes 中的用于進(jìn)行用戶認(rèn)證的類
class KeystoneV2Authentication(BaseAuthentication):
admin_token = None
admin_token_expiry = None
def get_admin_token(self):
if (
settings.KEYSTONE_TOKEN_CACHE and
KeystoneV2Authentication.admin_token_expiry
):
now = datetime.utcnow()
expiry_delta = now - KeystoneV2Authentication.admin_token_expiry
if expiry_delta.total_seconds() > 300:
return KeystoneV2Authentication.admin_token
admin_token, admin_token_expiry = get_x_auth_token(
settings.KEYSTONE_TENANT,
settings.KEYSTONE_USER,
settings.KEYSTONE_PASSWORD,
)
KeystoneV2Authentication.admin_token = admin_token
KeystoneV2Authentication.admin_token_expiry = admin_token_expiry
return admin_token
-------------------------------------省略---------------------------------------
#這個(gè)函數(shù)一定要返回一個(gè)User實(shí)例和auth屬性
def authenticate(self, request):
token = request.META.get("HTTP_X_AUTH_TOKEN")
if not token:
raise exceptions.AuthenticationFailed("X-Auth-Token is required")
try:
response = retry(
settings.DEFAULT_RETRY_TIMES,
requests.get,
urljoin(
settings.KEYSTONE_ENDPOINT,
"/v2.0/tokens/%s" % urlquote(token),
),
headers={
"X-Auth-Token": self.get_admin_token(),
}
)
except Exception as err:
logger.exception(err)
raise exceptions.AuthenticationFailed(
"Authorization error",
)
if response.status_code == 404:
raise exceptions.AuthenticationFailed(
"Authorization failed for token",
)
elif response.status_code == 401:
self.admin_token = None
raise exceptions.AuthenticationFailed(
"Keystone rejected admin token, resetting",
)
elif response.status_code != 200:
raise exceptions.AuthenticationFailed(
"Bad response code while validating token: %s" % (
response.status_code
),
)
access_info = response.json()
tenant = KeystoneTenant.from_access_info(access_info)
if not tenant.is_staff:
raise exceptions.AuthenticationFailed(
"Tenant inactive or deleted",
)
self._set_auth_headers(request, tenant)
return (tenant, None)
請求流程如下APIview.as_view -> dispatch -> initial(驗(yàn)證權(quán)限激率,方法是否是被允許的等一系列的操作) -> 根據(jù)請求方法調(diào)用APIview中的對應(yīng)的方法咳燕,比如get, put乒躺, post招盲,對于get, put聪蘸, post等這些方法宪肖,我們可以在自己實(shí)現(xiàn)的view類中直接定義這些方法,也可以繼承rest-framework/mixins.py中以及rest-framework/gernerics.py定義好了很多類中對應(yīng)的get健爬, put等操作控乾,比如RetrieveModelMixin類定義了查詢操作
class RetrieveModelMixin(object):
"""
Retrieve a model instance.
"""
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
return Response(serializer.data)
可能會(huì)疑問,這個(gè)retrieve函數(shù)誰去調(diào)用呢娜遵? 還記得上面APIview中的dispatch方法么蜕衡,dispatch會(huì)根據(jù)請求方法調(diào)用,對應(yīng)的比如get设拟。那么get的查詢操作如何和retrieve這個(gè)函數(shù)結(jié)合起來呢慨仿?見rest-framework處理流程分析。