基于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处理流程分析。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容