Django自定义认证系统原理及源码分析解读

news2024/11/25 16:29:54

Django自定义认证系统原理及源码分析解读

疑问

Django在如何自定义用户登录认证系统的时候,大家都会里面立马说 自定义一个 或者多个backend,比如通过账号+密码、邮箱+密码,邮箱+验证码、手机号+短信验证码等等。 然后设置 在settings中配置一个 AUTHENTICATION_BACKENDS就行。

但是为什么要这么做呢? 原理是什么呢?

今天就带大家分析一波Django的认证相关的源码逻辑,告诉你为什么要这么做。

关于认证登录

结论预告 >>>>

Django 默认的认证保持功能主要是通过 用户名+密码 发送给后端之后,会先去通过 authenticate 函数验证 用户名和密码是否正确; 如果正确则进行 login 登录,login字后会把对应的用户 user 存入到session中。并且request.user 为当前 user(而不是默认的 AnonymousUser)

所以Django的认证核心是两步

# 验证用户
user = auth.authenticate(username=username, password=password)
# 然后登录
auth.login(request, user)

源码解读

对于Django自带的管理后台的登录,首先要明确几个点

1、自定义的应用都是通过 admin.site.register() 注册到 Admin后台去的

2、对于Django自带的 admin 应用,它也是把自己注册到 AdminSite 中去的 (源码位置: django/contrib/admin.sites.py 中 AdminSite类的 __init__() 方法中)

Django新增项目之后,在项目目录下的urls.py 文件配置的所有项目的路由地址入口,后续新增的应用的也都是通过这里进行include配置。

# proj/urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
]

Django自带后台的登录地址 http://127.0.0.1:8000/admin/login 所以登录逻辑入口就是 admin.site.urls

定位登录入口

从源码 django/contrib/admin.sites.py

class AdminSite:
    ... ...
    def get_urls(self):
        ... ...
        # Admin-site-wide views.
        urlpatterns = [
            path('', wrap(self.index), name='index'),
            path('login/', self.login, name='login'),
            ... ...
        ]
            
    @property
    def urls(self):
        return self.get_urls(), 'admin', self.name
    
    ... ...
    @never_cache
    def login(self, request, extra_context=None):
        ... ...
        from django.contrib.admin.forms import AdminAuthenticationForm
        from django.contrib.auth.views import LoginView
        ... ...
        defaults = {
            'extra_context': context,
            # 这里 self.login_form为空, 所以 authentication_form是 AdminAuthenticationForm
            'authentication_form': self.login_form or AdminAuthenticationForm,
            'template_name': self.login_template or 'admin/login.html',
        }
        request.current_app = self.name
        return LoginView.as_view(**defaults)(request)

admin.site.urls 最终调用 get_urls() 方法, 在该方法中定义了 login 路由,对应的视图函数是self.login

然后查阅login函数发现它返回的是 LoginView 类视图, 来源于 django.contrib.auth.views

另外这里也需要注意下 django.contrib.admin.forms.AdminAuthenticationForm 因为最后实际登录的时候用到的Form表单就是这个

重点看LoginView视图

# django/contrib/auth/views.py
class LoginView(SuccessURLAllowedHostsMixin, FormView):
    """
    Display the login form and handle the login action.
    """
    form_class = AuthenticationForm
    authentication_form = None
    redirect_field_name = REDIRECT_FIELD_NAME
    template_name = 'registration/login.html'
    redirect_authenticated_user = False
    extra_context = None

    @method_decorator(sensitive_post_parameters())
    @method_decorator(csrf_protect)
    @method_decorator(never_cache)
    def dispatch(self, request, *args, **kwargs):
        if self.redirect_authenticated_user and self.request.user.is_authenticated:
            redirect_to = self.get_success_url()
            if redirect_to == self.request.path:
                raise ValueError(
                    "Redirection loop for authenticated user detected. Check that "
                    "your LOGIN_REDIRECT_URL doesn't point to a login page."
                )
            return HttpResponseRedirect(redirect_to)
        return super().dispatch(request, *args, **kwargs)

    def get_success_url(self):
        url = self.get_redirect_url()
        return url or resolve_url(settings.LOGIN_REDIRECT_URL)

    def get_redirect_url(self):
        """Return the user-originating redirect URL if it's safe."""
        redirect_to = self.request.POST.get(
            self.redirect_field_name,
            self.request.GET.get(self.redirect_field_name, '')
        )
        url_is_safe = url_has_allowed_host_and_scheme(
            url=redirect_to,
            allowed_hosts=self.get_success_url_allowed_hosts(),
            require_https=self.request.is_secure(),
        )
        return redirect_to if url_is_safe else ''

    def get_form_class(self):
        return self.authentication_form or self.form_class

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs['request'] = self.request
        return kwargs

    def form_valid(self, form):
        """Security check complete. Log the user in."""
        auth_login(self.request, form.get_user())
        return HttpResponseRedirect(self.get_success_url())

这里 SuccessURLAllowedHostsMixin 可以先忽略(它是判断允许那些主机来访问URL),

LoginView继承自 FormView, FormView 继承自 TemplateResponseMixin 和 BaseFormView,而BaseFormView又继承自FormMixin 和 ProcessFormView

# django/views/generic/edit.py
class ProcessFormView(View):
    """Render a form on GET and processes it on POST."""
    def get(self, request, *args, **kwargs):
        """Handle GET requests: instantiate a blank version of the form."""
        return self.render_to_response(self.get_context_data())

    def post(self, request, *args, **kwargs):
        """
        Handle POST requests: instantiate a form instance with the passed
        POST variables and then check if it's valid.
        """
        form = self.get_form()
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)



class BaseFormView(FormMixin, ProcessFormView):
    """A base view for displaying a form."""


class FormView(TemplateResponseMixin, BaseFormView):
    """A view for displaying a form and rendering a template response."""


同样的TemplateResponseMixin是定义返回结果格式的一个Mixin,可以先忽略。

定位Post

我们知道 login 最终发送的是一个 post 请求。

对于Django 类视图的请求解析路径大概流程是:

1) 通过XxxxView.as_view() 最终到 View 类(位于 django/views/generic/base.py)中 请求 as_view 方法

2)as_view方法中调用 setup() 方法, setup() 方法初始化 request/args/kwargs 参数 这里划个**重点**

3)然后在as_view方法中继续调用 dispatch() 方法,该方法获取handler,这个handler就是最终调用方法

http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

所以通过继承关系,知道最终 post 调用的是 ProcessFormView类中的方法

这里先 获取 form 然后判断 form的有效性,通过之后执行form_valid(form)

表单详解

然后我们拆开来分析上面的简单的三个步骤

获取form表单

通过上面的源码知道了 BaseFormView 继承 FormMixin 和 ProcessFormView 两个类, 在 ProcessFormView 中的post中使用了 self.get_form() 方法, 该方法其实位于 FormMixin类

# django/views/generic/edit.py
class FormMixin(ContextMixin):
    """Provide a way to show and handle a form in a request."""
    initial = {}
    form_class = None
    success_url = None
    prefix = None
	... ...

    def get_form_class(self):
        """Return the form class to use."""
        return self.form_class

    def get_form(self, form_class=None):
        """Return an instance of the form to be used in this view."""
        if form_class is None:
            form_class = self.get_form_class()
        return form_class(**self.get_form_kwargs())

特别注意📢

1)ProcessFormView 中的Post 方法中 form = self.get_form() 是没有参数的,所以在 FormMixin 中的 get_form() 中获取 form_class的时候 是通过

form_class = self.get_form_class()

2)但是在 LoginView中 该方法被覆盖了

def get_form_class(self):
        return self.authentication_form or self.form_class

3)另外讲到在 基类 View中 setup 方法会设置 kwargs 等参数

4)回忆在最开始的LoginView 中

LoginView.as_view(**defaults)(request)

这里的 **defautls 中有个 authentication_form 的值是 AdminAuthenticationForm

所以虽然 LoginView类的最开始定义了 form_class 是 AuthenticationForm

class LoginView:
    form_class = AuthenticationForm
    authentication_form = None

但是 authentication_form 通过 setup() 方法被赋值了,然后 LoginView中的 get_form_class是先判断获取 authentication_form的。

所以最终 Django Admin后台登录的时候 form_class 是 AdminAuthenticationForm

但其实阅读源码不难发现AdminAuthenticationForm 是继承自 AuthenticationForm的

# django/contrib/admin/forms.py
class AdminAuthenticationForm(AuthenticationForm):
    """
    A custom authentication form used in the admin app.
	... ... 

而 AdminAuthenticationForm 类中只定义了 confirm_login_allowed 方法,其他方法使用的还是父类的方法,比如 clean() 方法,这里也划**重点**

判断form有效性

对于 form对象的 is_valid() 方法,该方法一般都是对于 Form基类中,很少被重写

从上面知道目前的 form 对象对AdminAuthenticationForm类的对象,而 AdminAuthenticationForm 继承 AuthenticationForm ,AuthenticationForm 也没有重写 is_valid 方法,所以得知 is_valid() 方法存在于基类BaseForm 中 (AuthenticationForm(forms.Form) 而 Form基础自BaseForm)

# django/forms/forms.py
class Form(BaseForm, metaclass=DeclarativeFieldsMetaclass):
    "A collection of Fields, plus their associated data."

    
class BaseForm:
	... ...
    @property
    def errors(self):
        """Return an ErrorDict for the data provided for the form."""
        if self._errors is None:
            self.full_clean()
        return self._errors

    def is_valid(self):
        """Return True if the form has no errors, or False otherwise."""
        print("lcDebug-> here is_valid?")
        return self.is_bound and not self.errors

    def full_clean(self):
        """
        Clean all of self.data and populate self._errors and self.cleaned_data.
        """
        self._errors = ErrorDict()
        if not self.is_bound:  # Stop further processing.
            return
        self.cleaned_data = {}
        # If the form is permitted to be empty, and none of the form data has
        # changed from the initial data, short circuit any validation.
        if self.empty_permitted and not self.has_changed():
            return

        self._clean_fields()
        self._clean_form()
        self._post_clean()

    def _clean_form(self):
        try:
            cleaned_data = self.clean()
        except ValidationError as e:
            self.add_error(None, e)
        else:
            if cleaned_data is not None:
                self.cleaned_data = cleaned_data
    ... ...

1)从上述源码看到 is_valid() 方法检查 self.is_bound 和 self.errors

2)errors() 这里是个方法,如果表单没有问题的时候执行 self.full_clean()方法

3)full_clean 方法中重点关注 self._clean_form() 方法

4)_clean_form() 方法中 cleaned_data = self.clean()

还记得在获取表单那个小章节划的重点么? form类的clean() 方法,这个方法是在 AuthenticationForm 类中被重写的

from django.contrib.auth import (
    authenticate, get_user_model, password_validation,
)

class AuthenticationForm(forms.Form):
    """
    Base class for authenticating users. Extend this to get a form that accepts
    username/password logins.
    """
    username = UsernameField(widget=forms.TextInput(attrs={'autofocus': True}))
    password = forms.CharField(
        label=_("Password"),
        strip=False,
        widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}),
    )

    error_messages = {
        'invalid_login': _(
            "Please enter a correct %(username)s and password. Note that both "
            "fields may be case-sensitive."
        ),
        'inactive': _("This account is inactive."),
    }

    def __init__(self, request=None, *args, **kwargs):
        ... ...

    def clean(self):
        username = self.cleaned_data.get('username')
        password = self.cleaned_data.get('password')

        if username is not None and password:
            self.user_cache = authenticate(self.request, username=username, password=password)
            if self.user_cache is None:
                raise self.get_invalid_login_error()
            else:
                self.confirm_login_allowed(self.user_cache)

        return self.cleaned_data

查阅 clean() 方法源码,看到在通过 self.cleaned_data (self.cleaned_data 的赋值是在 BaseForm的 self._clean_fields 方法中完成的)获取用户名和密码之后,如果不为空,就进行**认证authenticate**

而这里的 authenticate方法 来自于 django/crontrib/auth/__init__.py

# django/contrib/auth/__init__.py

@sensitive_variables('credentials')
def authenticate(request=None, **credentials):
    """
    If the given credentials are valid, return a User object.
    """
    for backend, backend_path in _get_backends(return_tuples=True):
        backend_signature = inspect.signature(backend.authenticate)
        try:
            backend_signature.bind(request, **credentials)
        except TypeError:
            # This backend doesn't accept these credentials as arguments. Try the next one.
            continue
        try:
            user = backend.authenticate(request, **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), request=request)
    
def _get_backends(return_tuples=False):
    backends = []
    for backend_path in settings.AUTHENTICATION_BACKENDS:
        # print("backend: ", backend_path)
        backend = load_backend(backend_path)
        backends.append((backend, backend_path) if return_tuples else backend)
    if not backends:
        raise ImproperlyConfigured(
            'No authentication backends have been defined. Does '
            'AUTHENTICATION_BACKENDS contain anything?'
        )
    return backends

核心源码

user = backend.authenticate(request, **credentials)

会获取 所有的 backends 进行遍历,利用对应的backend中的 authenticate 方法进行认证

通过 _get_backends 方法知道默认获取的是 settings.AUTHENTICATION_BACKENDS

# django/conf/global_settings.py
AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend']

ModelBackend 类源码

# django/contrib/auth/backends.py
class ModelBackend(BaseBackend):
    """
    Authenticates against settings.AUTH_USER_MODEL.
    """

    def authenticate(self, request, username=None, password=None, **kwargs):
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD)
        if username is None or password is None:
            return
        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 nonexistent user (#20760).
            UserModel().set_password(password)
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user

看到最终是通过默认的 ModelBackend类的 authenticate 认证对应的用户名和密码,然后 返回对应的 user 对象

简单来讲就是

form.is_valid() 方法调用了 form.clean() 方法,在 form.clean() 方法中调用了 对应的 authenticate() 方法,该方法查找可能得backends利用对应的backend的authenticate() 方法返回user对象

form_valid 表单

验证完毕 form表单有效性,并且完成了 authenticate认证得到了 user对象。

然后调用 form_valid(form) 方法,通过源码知道该方法是在LoginView中被重写

# django/contrib/auth/views.py

from django.contrib.auth import  login as auth_login
class LoginView:
    ... ...
        def form_valid(self, form):
        """Security check complete. Log the user in."""
        auth_login(self.request, form.get_user())
        return HttpResponseRedirect(self.get_success_url())

看到这里实际是调用了 auth_login 进行了用户登录,登录成功进行跳转

# django/contrib/auth/__init__.py
def login(request, user, backend=None):
    """
    Persist a user id and a backend in the request. This way a user doesn't
    have to reauthenticate on every request. Note that data set during
    the anonymous session is retained when the user logs in.
    """
    session_auth_hash = ''
    if user is None:
        user = request.user
    if hasattr(user, 'get_session_auth_hash'):
        session_auth_hash = user.get_session_auth_hash()

    if SESSION_KEY in request.session:
        if _get_user_session_key(request) != user.pk or (
                session_auth_hash and
                not constant_time_compare(request.session.get(HASH_SESSION_KEY, ''), session_auth_hash)):
            # To avoid reusing another user's session, create a new, empty
            # session if the existing session corresponds to a different
            # authenticated user.
            request.session.flush()
    else:
        request.session.cycle_key()

    try:
        backend = backend or user.backend
    except AttributeError:
        backends = _get_backends(return_tuples=True)
        if len(backends) == 1:
            _, backend = backends[0]
        else:
            raise ValueError(
                'You have multiple authentication backends configured and '
                'therefore must provide the `backend` argument or set the '
                '`backend` attribute on the user.'
            )
    else:
        if not isinstance(backend, str):
            raise TypeError('backend must be a dotted import path string (got %r).' % backend)

    request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
    request.session[BACKEND_SESSION_KEY] = backend
    request.session[HASH_SESSION_KEY] = session_auth_hash
    if hasattr(request, 'user'):
        request.user = user
    rotate_token(request)
    user_logged_in.send(sender=user.__class__, request=request, user=user)

这里login函数的核心是 通过 authenticate 的到的 user 对象,然后

1) 设置相关的session值,用于后续的判断处理

2)通过 user_logged_in 信号去更新 用户的 last_login 字段(如果有的话)


截止当前,通过源码解读,大家应该能回答最开始的问题了哈。

也希望带着大家进行源码解读让大家更好的理解Django的原理。

同时作为优秀的框架,源码分析学习也是有助于我们学习如何高效开发、组织代码,提高自己的开发质量。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/168044.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【尚硅谷】Java数据结构与算法笔记11 - 树结构的实际应用

文章目录一、堆排序1.1 堆排序基本介绍1.2 堆排序的基本思想1.3 堆排序步骤图解1.4 堆排序思路总结1.5 堆排序代码实现二、赫夫曼树2.1 基本介绍2.2 重要概念1.3 赫夫曼树构建思路图解1.4 赫夫曼树代码实现三、赫夫曼编码3.1 基本介绍3.2 原理剖析3.3 实践:数据压缩…

java运算符2023010

运算符: Java语言中的运算符可分为如下几种。 ➢ 算术运算符 ➢ 赋值运算符,/—/各种和等号组合的都是赋值运算符,赋值表达式是有值的,赋值表达式的值就是右边 被赋的值。例如String str2str表达式的值就是str。因此,赋…

Java技能树-操作符(一)-练习篇

算术运算符 执行完下面的代码,变量b的值是: java int a 1; int b a; 答案是:D 在后,先赋值再运算 自动递增和递减 下面代码执行后的结果是: int a 0; a a; int b 0; b b; System.out.println("a " a); S…

Numpy的轴及numpy数组转置换轴

Numpy的轴 import numpy as np 数组np.array([[[1,2],[4,5],[7,8]],[[8,9],[11,12],[14,15]],[[10,11],[13,14],[16,17]],[[19,20],[22,23],[25,26]]]) print(数组.shape) # 返回 (4, 3, 2)最内层一对 [ ] 可以代表一个1维数组 加粗的一对 [ ] 里面有3个一维数组,也…

Layout布局(element ui)

Layout布局嘚吧嘚gutter示例发现el-row行内容居中默认局左上角水平居中垂直居中水平垂直居中嘚吧嘚 其实layout布局的使用在element官网上都有相关描述,也有相关示例,很容易快速上手。但是在实际使用的过程还是发现一些问题,于是做了一些学习…

递归(基础)

目录 一、递归的定义 1、什么时候会用到递归的方法 1. 定义是递归的 2. 数据结构是递归的 3. 问题的解法是递归的 2、应用递归的原则 3、递归调用顺序问题 1. 首先递归的过程可以总结为以下几点: 2. 递归工作栈​​​​​​​ 二、 递归和非递归的转化 …

Allegro如何快速把Class高亮成不同的颜色操作指导

Allegro如何快速把Class高亮成不同的颜色操作指导 在做PCB设计的时候,高亮Class组是一个非常频繁的操作,Allegro支持快速的将Class高亮成不同的颜色,并且还可以形成一个列表,如下图 具体操作如下 选择File选择Change Editor

select ( ) for update 锁行还是锁表?

select ( ) for update 锁行还是锁表? 一、验证 创建SQL表 //id为主键 //name 为唯一索引 CREATE TABLE user (id INT ( 11 ) NOT NULL AUTO_INCREMENT,name VARCHAR ( 255 ) DEFAULT NULL,age INT ( 11 ) DEFAULT NULL,code VARCHAR ( …

SpringCloud微服务项目实战 - 6.延迟任务

我没有失约,我与春风共至,我与夏蝉共鸣,我与秋叶共舞,我与凛冬共至,唯独你背道而行! 系列文章目录 项目搭建App登录及网关App文章自媒体平台(博主后台)内容审核(自动)延迟任务 - 精…

JVM快速入门学习笔记(一)

参考: https://blog.csdn.net/m0_38075425/article/details/81627349 www.kuangstudy.com JVM 常问面试题 请你谈谈你对JVM的理解? java—>class---->Java8虚拟机和之前的变化更新?什么是OOM 内存溢出什么是栈溢出StackOverFlowErr…

Matplotlab绘制散点图小节

前言现有一堆数据,是散点坐标形式,现在需要将它们绘制成散点图,并解决了关于Matplotlib绘图不能显示汉字的问题。读取数据数据格式如下图。第一行为一个数字,表示当前文件共有多少行数据。 第二行开始为真正的数据,各数…

如何冻结Excel中的行

在Excel中有一个冻结行的功能。在冻结行的帮助下,我们可以固定我们选择的窗格或行,以超出特定的限制工作表。 可以从“视图”菜单选项卡的“窗口”部分的“冻结窗格”下拉列表中访问“冻结行”。首先,要冻结列,请选择要冻结的列或将光标放在该列的任何位置,然后从列表中选…

vue 使用hook 对 chekbox 做简单的逻辑抽离,一个核心多套模板

现在的组件库都会包含些相同的基础组件,功能大差不差,只是不同UI规范下的具体实现。这些基础组件基本能满足大部分的开发需求。 但世上无银弹,有时我们需要对组件做细微的调整可能是功能上的,可能是UI上的,例如 tab切换…

JavaWeb基础(三) Request和Response详解

JavaWeb基础(三) Request和Response详解 1,Request和Response的概述 Request是请求对象,Response是响应对象。 此时,我们就需要思考一个问题request和response这两个参数的作用是什么? request: 获取请求数据 浏览器会发送HTTP请求到后台…

跨站脚本攻击漏洞(XSS)-基础篇

数据来源 跨站脚本攻击 1、什么是跨站脚本攻击? 跨站脚本( Cross-site Scripting)攻击,攻击者通过网站注入点注入客户端可执行解析的 payload(脚本代码),当用户访问网页时,恶意 payload自动加载并执行,…

索引(index)

索引(index) 1、什么是索引: 索引是在数据库表的字段上添加的,是为了提高查询效率存在的一种机制。一张表的一个字段可以添加一个索引,当然多个字段联合起来也可以添加索引,索引相当于一本书的目录&#xf…

Spring Boot 3 步完成日志脱敏,简单实用!

在我们写代码的时候,会书写许多日志代码,但是有些敏感数据是需要进行安全脱敏处理的。 对于日志脱敏的方式有很多,常见的有: 使用conversionRule标签,继承MessageConverter 书写一个脱敏工具类,在打印日志…

springboot+mybatisplus实现分页

在日常开发中,多记录的列表查询可能会遇到分页处理的场景,在springboot项目中传统是引入mybatis组件进行持久化,然后通过pagehelper组件进行分页实现。下面体验一下在springboot项目中引入mybatisplus组件,通过其自带分页插件实现…

cpu简述--指令集架构

很多初级开发者其实都对cpu了解不多,个人兴趣原因想要了解一下cpu的相关知识,所以开几篇文章记录一下吧。 2002年8月10日,中国科学院计算技术研究所的青年科学家胡伟武带领研制组,研制出我国首枚拥有自主知识产权的通用高性能微处理芯片——“龙芯一…

MindOpt安装配置教程(Windows系统)

1 前言 官网有很多的说明文档、教程,但是可能有些地方(这里仅仅补充安装配置部分,其他操作建议自行去官网进行探索)不是很详细,踩了一些坑,所以进行了一些总结。 2 下载安装 url:求解器SDK下载…