基于rest-framework对django的RESTful API进行权限设置

当我们通过django框架创建RESTful API对外提供后,我们希望这些API只有相关权限的人才可以调用,这个怎么做呢?可以采用在django框架之上rest-framework去做,当然必须安装rest-framework,然后在django的setting中的INSTALLED_APPS加上rest_framework。
基于rest-framework的请求处理,与常规的url配置不同,通常一个django的url请求对应一个视图函数,在使用rest-framework时,我们要基于视图对象,然后调用视图对象的as_view函数,as_view函数中会调用rest_framework/views.py中的dispatch函数,这个函数会根据request请求方法,去调用我们在view对象中定义的对应的方法,就像这样:

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()函数会调用dispatch函数,dispatch根据request.METHOD,这里是get,去调用testView类的get方法,这就跟通常的url->视图函数的流程一样了。

但是权限验证是在执行请求之前做的,所以其实就是在dispatch函数之中做的,具体见源码rest-framework/views.py中APIView类中的dispatch函数:

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)  #重点关注

            # 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.initial(request, *args, **kwargs)函数,对于这个函数

    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) 验证某个用户

        """
        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
    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

所以关注self._authenticate()函数就好了

    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) #重点
            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()

验证用户就是authenticator.authenticate,那么self.authenticators从哪儿来的呢?
关注文章开头给出的testView类中的

authentication_classes = (
        BasicAuthentication,
    )
    permission_classes = (
        IsAuthenticated,
    )

authentication_classes 里面放的就是可以用来验证一个用户的类,他是一个元组,验证用户时,按照这个元组顺序,直到验证通过或者遍历整个元组还没有通过。
同理self.check_permissions(request)是验证该用户是否具有API的使用权限。关于对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/
所以,这里剩下的就是实现校验用户的BasicAuthentication类了。对于像BasicAuthentication这样的类,必须实现authenticate方法,并且返回一个用户,赋值给request.user,这个request.user就是系统中进行用户认证的user对象,后续的权限验证一般都是通过判断request.user的user对象是否拥有某个权限。rest-framework默认的就是BasicAuthentication,也就是跟admin登陆用的一样的认证。其源码如下:

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) #重点关注

        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

对于上述函数调用流程,重点关注user = authenticate(**credentials),这里的authenticate其实是from django.contrib.auth import authenticate导入的authenticate,因为在调用时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) #重点关注
        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其实就是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

也就是去跟数据库比对,用户名和密码是否匹配。如果匹配返回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)
                )

如果存在验证不通过,那么就执行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)

然后这个异常在dispatch函数中被捕捉,当做结果传递给response。

对于API的权限,也可以在settings中进行全局设置,具体过程可参照:
http://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/
整个练习的开始是:
http://www.django-rest-framework.org/tutorial/quickstart/
而关于BasicAuthentication认证的解释,可以参见:
https://www.ibm.com/support/knowledgecenter/en/SSGMCP_5.1.0/com.ibm.cics.ts.internet.doc/topics/dfhtl2a.html

当一次授权通过后,再一次访问这个API时,这时候的用户名和密码从哪儿来的?下一次来访问的时候就是通过服务器通过cookie返回给client的sessionid去验证,通过谷歌浏览器用F12调试可以得到验证,下一次通过浏览器访问时,就会带上类似下面的内容:Cookie:csrftoken=FsvNBXNdyyUvECZwTMpj59DAnGPPurRFM8RqFQoVuvizeQ6OB1nSK3KxS8mjJiWE; sessionid=xxtj52tsqgow9kbur6e304fd1ygn7603。至于指定的BasicAuthentication认证为什么第二次会走SessionAuthentication认证,暂时还不知道。

如果APIView类中的authentication_classes使用的是SessionAuthentication去验证,那么就要在请求头部带上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)

那可能会问sessionid从哪儿来,按照常规,我们登陆一个系统后,服务端会根据我们第一次登陆提供的用户名和密码还有访问的域名等其他信息生成一个session对象保存在服务端,并通过写cookie返回给client,当下一次访问相同的域名时就会在cookie中带上相应的sessionid信息,SessionAuthentication模块根据sessionid去进行权限验证。

同理,如果如果APIView类中的authentication_classes使用的是TokenAuthentication去验证,那么就要在请求头部带上Token信息,代码例子跟上面的session验证一样,只是把header换成token。同样,token从哪儿来呢?Token一般是在服务器上跟用户一起绑定生成的,然后存放在token数据库中。在rest-framework中,要是用Token验证,那么在settings中的INSTALL_APP中还要加上'rest_framework.authtoken',用来生成存放token的数据库,当创建好token后,下一次访问时,带上Token就好了。我们一般都是用带token的方式进行访问,在传输token过程中一般用https防止token泄漏,当然我们也最好让token有时效性,然后定时更新token,这样保证多数情况下,即使token泄漏也不会造成很大安全风险。这个例子可以参考:
https://chrisbartos.com/articles/how-to-implement-token-authentication-with-django-rest-framework/
http://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication

当然,你也可以自己定制用户认证的类,但是要明确一点,调用这个认证的类的authenticate函数一定要返回一个用户对象给request.user还有request.auth,后续的权限验证都是依据这两个进行的,比如下面:

#!/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__)

#定义一个用户user类,authenticate函数会返回这样一个实例给request.user
#这个类可以参照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 中的用于进行用户认证的类
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
-------------------------------------省略---------------------------------------

    #这个函数一定要返回一个User实例和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(验证权限,方法是否是被允许的等一系列的操作) -> 根据请求方法调用APIview中的对应的方法,比如get, put, post,对于get, put, post等这些方法,我们可以在自己实现的view类中直接定义这些方法,也可以继承rest-framework/mixins.py中以及rest-framework/gernerics.py定义好了很多类中对应的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)

可能会疑问,这个retrieve函数谁去调用呢? 还记得上面APIview中的dispatch方法么,dispatch会根据请求方法调用,对应的比如get。那么get的查询操作如何和retrieve这个函数结合起来呢?见rest-framework处理流程分析。

推荐阅读更多精彩内容