Django REST framework 登录之实战

4,478 阅读3分钟

写在前面

Django REST framework 登录之JWT

上文提到JWT的认证方式,配置实现起来很方便;但是公司项目中的登录认证是多种多样的,比如:内部系统的SSO,Oauth等认证方式;

那今天就聊聊在DRF的框架下如何自定义登录认证。

本文的代码是以 Django REST framework (三)为基础开发的

正文开始

1. sso接口准备

新增 demo/mock_view.py 该文件主要是用于模拟 sso 登录、token验证的接口

  • 登录 支持用户密码登录 (只为逻辑完整性)
  • token验证 不实现具体逻辑只为演示整体逻辑

注意:在set cookie 的时候会直接影响到浏览器的默认行为,如果公司期望是一个token访问所有的服务,那么这里一定要配置顶级域名;类似 baidu.com 而不是 baike.baidu.com;

# 1. 新增文件 

from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.authentication import BaseAuthentication

from django.conf.urls import url


class AnyAuthentication(BaseAuthentication):
    def authenticate(self, request):
        return


@api_view(['POST'])
@authentication_classes((AnyAuthentication,))
@permission_classes((AllowAny, ))
def login(request):
    token = request.COOKIES.get('auth', "auth")
    password = request.data.get('password', '')
    username = request.data.get('username', '')

    # user center check username password
    response = Response({'user': "user_info", 'token': token})
    response.set_cookie('auth', token, domain="0.0.0.0", expires=30 * 24 * 60 * 60)
    return response


@api_view(['GET'])
@authentication_classes((AnyAuthentication,))
@permission_classes((AllowAny,))
def check_token(request, token):

    token = request.COOKIES.get('auth')

    # user center check token  ...

    data = {
        "user_info": {
            "username": "admin",
            "user_id": 1
        },
        "token": token
    }
    return Response(data)


mock_urls = [
    url('^login/', login),
    url(r"^check_token/(?P<token>[A-Za-z0-9]+)/$", check_token)
]
2. urls.py 新增路由
from .mock_view import mock_urls
...

urlpatterns += mock_urls

2. 安装依赖

由于会远程调用 mock的服务需要引入 网络请求的包

pipenv install requests

3. 实现逻辑

新增文件 utils/authentication.py

# 1. 新增 utils/authentication.py
import requests

from rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication
from django.utils.translation import ugettext_lazy
from django.contrib.auth.models import User


class MyAuthentication(BaseAuthentication):

    def authenticate(self, request):

        token = request.COOKIES.get('auth')

        if token is not None:
            username = self.check_token(token)
            if username is not None:
                return User.objects.get(username=username), token
            else:
                raise exceptions.AuthenticationFailed(ugettext_lazy('Invalid token.'))

    @staticmethod
    def check_token(token):
        # 模拟请求token的验证
        response = requests.get("http://localhost:8000/check_token/" + token)
        if response.status_code == 200:
            return response.json().get("user_info", {}).get("username")

2. settings.py 中新增认证的配置

'DEFAULT_AUTHENTICATION_CLASSES': (
        'utils.authentication.MyAuthentication',
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),

4. 验证结果

# 默认的 basci 认证方式
$ curl -H 'Accept: application/json; indent=4' -u admin:admin http://127.0.0.1:8000/api/article/1/ 
{
    "id": 1,
    "creator": "admin",
    "tag": "现代诗",
    "title": "如果",
    "content": "今生今世 永不再将你想起\n除了\n除了在有些个\n因落泪而湿润的夜里 如果\n如果你愿意"
}

# 自定义token认证方式
$ curl -H 'Accept: application/json; indent=4' --cookie "auth=123" http://127.0.0.1:8000/api/article/1/
{
    "id": 1,
    "creator": "admin",
    "tag": "现代诗",
    "title": "如果",
    "content": "今生今世 永不再将你想起\n除了\n除了在有些个\n因落泪而湿润的夜里 如果\n如果你愿意"
}

总结

至此 自定义认证的逻辑已经完成,关于认证的几个问题总结一下:

  1. 自定义认证与默认的认证执行顺序是什么?

答:在settings文件中我们配置了 DEFAULT_AUTHENTICATION_CLASSES 元祖,默认的执行顺序是自上而下的验证,如果都验证失败则抛出失败的异常,这里的异常最终会被DRF补获,返回给前端。

  1. 实现 MyAuthentication 的时候需要注意的地方? 答:下面是 BaseAuthentication 的源码,可以看到的 DRF所有的认证都需要继承BaseAuthentication且必须实现authenticate,需要注意的是返回值为一个 元组 (user, token);当然这就是在django中使用 request.user 获取当前用户的底层逻辑。
class BaseAuthentication:
    """
    All authentication classes should extend BaseAuthentication.
    """

    def authenticate(self, request):
        """
        Authenticate the request and return a two-tuple of (user, token).
        """
        raise NotImplementedError(".authenticate() must be overridden.")

    def authenticate_header(self, request):
        """
        Return a string to be used as the value of the `WWW-Authenticate`
        header in a `401 Unauthenticated` response, or `None` if the
        authentication scheme should return `403 Permission Denied` responses.
        """
        pass

  1. 这样我们的系统就支持多种认证方式,无论是何种登录也都可以快速支持;人生苦短,我用python。

下图为django中request类的部分代码截取,就可以解释 执行顺序以及user对象的获取

image.png

最后多说一句无论是自己实现的认证还是 DRF的认证都是继承 BaseAuthentication 并实现authenticate;其实源码也不难,只怕你用心。

参考资料