疑問
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的原理。
同時作爲優秀的框架,源碼分析學習也是有助於我們學習如何高效開發、組織代碼,提高自己的開發質量。