Django CBV加装饰器、Django中间件、auth模块

一. CBV加装饰器

  在视图层中,基于函数的视图叫FBV(function base views),基于类的视图叫CBV(class base views)。当需要用到装饰器时,例如之前的基于Cookie与基于Session的登录验证,给FBV加装饰器很简单,一个@语法糖就OK了,那么给CBV加装饰器呢,难道直接跟单例实现的方法之一一样,类上面一个@语法糖就行了吗。其实给CBV加装饰器有三种方法。

  需要先导入模块:

from django.utils.decorators import method_decorator

  装饰类时,语法为:@method_decorator(装饰器函数名, name=‘类内需要装饰的方法名’)

直接装饰类内的方法时,直接@method_decorator(装饰器函数名)即可。我们这里不用原生的装饰器是因为扩展性差,因为类方法有一个self参数,如果用原生的装饰器就需要加一个参数,可是加了之后装饰器又没法装饰普通的函数了。

# @method_decorator(login_auth,name='get')  # 第二种 name参数必须指定
    class MyHome(View):
        @method_decorator(login_auth)  # 第三种  get和post都会被装饰
        def dispatch(self, request, *args, **kwargs):
            super().dispatch(request,*args,**kwargs)
        # @method_decorator(login_auth)  # 第一种
        def get(self,request):
            return HttpResponse('get')

        def post(self,request):
            return HttpResponse('post')

二. 中间件

  看中间件之前,先看一下Django生命周期:

1. 什么是中间件

  中间件是一个用来处理Django的请求和响应的框架级别的钩子。它是一个轻量、低级别的插件系统,用于在全局范围内改变Django的输入和输出。每个中间件组件都负责做一些特定的功能。

  但是由于其影响的是全局,所以需要谨慎使用,使用不当会影响性能。

  说的直白一点中间件是帮助我们在视图函数执行之前和执行之后都可以做一些额外的操作,它本质上就是一个自定义类,类中定义了几个方法,Django框架会在请求的特定的时间去执行这些方法。

  中间件就像是Django的门户,数据在进入与离开时都要经过中间件。

2. Django默认的中间件及中间件的五种方法

  Django默认有七种中间件,可以在settings.py中查看,每个都有各自的功能。

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

  有兴趣可以看一下各中间件的源码(注意:以该种方式看完源码后要运行Django项目,记得把它们给注释或者删除了,不然会报Apps aren't loaded yet.等错误):

import re

from django.conf import settings
from django.http import HttpResponsePermanentRedirect
from django.utils.deprecation import MiddlewareMixin


class SecurityMiddleware(MiddlewareMixin):
    def __init__(self, get_response=None):
        self.sts_seconds = settings.SECURE_HSTS_SECONDS
        self.sts_include_subdomains = settings.SECURE_HSTS_INCLUDE_SUBDOMAINS
        self.sts_preload = settings.SECURE_HSTS_PRELOAD
        self.content_type_nosniff = settings.SECURE_CONTENT_TYPE_NOSNIFF
        self.xss_filter = settings.SECURE_BROWSER_XSS_FILTER
        self.redirect = settings.SECURE_SSL_REDIRECT
        self.redirect_host = settings.SECURE_SSL_HOST
        self.redirect_exempt = [re.compile(r) for r in settings.SECURE_REDIRECT_EXEMPT]
        self.get_response = get_response

    def process_request(self, request):
        path = request.path.lstrip("/")
        if (self.redirect and not request.is_secure() and
                not any(pattern.search(path)
                        for pattern in self.redirect_exempt)):
            host = self.redirect_host or request.get_host()
            return HttpResponsePermanentRedirect(
                "https://%s%s" % (host, request.get_full_path())
            )

    def process_response(self, request, response):
        if (self.sts_seconds and request.is_secure() and
                'strict-transport-security' not in response):
            sts_header = "max-age=%s" % self.sts_seconds
            if self.sts_include_subdomains:
                sts_header = sts_header + "; includeSubDomains"
            if self.sts_preload:
                sts_header = sts_header + "; preload"
            response["strict-transport-security"] = sts_header

        if self.content_type_nosniff and 'x-content-type-options' not in response:
            response["x-content-type-options"] = "nosniff"

        if self.xss_filter and 'x-xss-protection' not in response:
            response["x-xss-protection"] = "1; mode=block"

        return response
中间件SecurityMiddleware源码
import time
from importlib import import_module

from django.conf import settings
from django.contrib.sessions.backends.base import UpdateError
from django.core.exceptions import SuspiciousOperation
from django.utils.cache import patch_vary_headers
from django.utils.deprecation import MiddlewareMixin
from django.utils.http import cookie_date


class SessionMiddleware(MiddlewareMixin):
    def __init__(self, get_response=None):
        self.get_response = get_response
        engine = import_module(settings.SESSION_ENGINE)
        self.SessionStore = engine.SessionStore

    def process_request(self, request):
        session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
        request.session = self.SessionStore(session_key)

    def process_response(self, request, response):
        """
        If request.session was modified, or if the configuration is to save the
        session every time, save the changes and set a session cookie or delete
        the session cookie if the session has been emptied.
        """
        try:
            accessed = request.session.accessed
            modified = request.session.modified
            empty = request.session.is_empty()
        except AttributeError:
            pass
        else:
            # First check if we need to delete this cookie.
            # The session should be deleted only if the session is entirely empty
            if settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
                response.delete_cookie(
                    settings.SESSION_COOKIE_NAME,
                    path=settings.SESSION_COOKIE_PATH,
                    domain=settings.SESSION_COOKIE_DOMAIN,
                )
            else:
                if accessed:
                    patch_vary_headers(response, ('Cookie',))
                if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
                    if request.session.get_expire_at_browser_close():
                        max_age = None
                        expires = None
                    else:
                        max_age = request.session.get_expiry_age()
                        expires_time = time.time() + max_age
                        expires = cookie_date(expires_time)
                    # Save the session data and refresh the client cookie.
                    # Skip session save for 500 responses, refs #3881.
                    if response.status_code != 500:
                        try:
                            request.session.save()
                        except UpdateError:
                            raise SuspiciousOperation(
                                "The request's session was deleted before the "
                                "request completed. The user may have logged "
                                "out in a concurrent request, for example."
                            )
                        response.set_cookie(
                            settings.SESSION_COOKIE_NAME,
                            request.session.session_key, max_age=max_age,
                            expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
                            path=settings.SESSION_COOKIE_PATH,
                            secure=settings.SESSION_COOKIE_SECURE or None,
                            httponly=settings.SESSION_COOKIE_HTTPONLY or None,
                        )
        return response
中间件SessionMiddleware源码
import re
import warnings

from django import http
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.core.mail import mail_managers
from django.urls import is_valid_path
from django.utils.cache import (
    cc_delim_re, get_conditional_response, set_response_etag,
)
from django.utils.deprecation import MiddlewareMixin, RemovedInDjango21Warning
from django.utils.encoding import force_text
from django.utils.six.moves.urllib.parse import urlparse


class CommonMiddleware(MiddlewareMixin):
    """
    "Common" middleware for taking care of some basic operations:

        - Forbids access to User-Agents in settings.DISALLOWED_USER_AGENTS

        - URL rewriting: Based on the APPEND_SLASH and PREPEND_WWW settings,
          this middleware appends missing slashes and/or prepends missing
          "www."s.

            - If APPEND_SLASH is set and the initial URL doesn't end with a
              slash, and it is not found in urlpatterns, a new URL is formed by
              appending a slash at the end. If this new URL is found in
              urlpatterns, then an HTTP-redirect is returned to this new URL;
              otherwise the initial URL is processed as usual.

          This behavior can be customized by subclassing CommonMiddleware and
          overriding the response_redirect_class attribute.

        - ETags: If the USE_ETAGS setting is set, ETags will be calculated from
          the entire page content and Not Modified responses will be returned
          appropriately. USE_ETAGS is deprecated in favor of
          ConditionalGetMiddleware.
    """

    response_redirect_class = http.HttpResponsePermanentRedirect

    def process_request(self, request):
        """
        Check for denied User-Agents and rewrite the URL based on
        settings.APPEND_SLASH and settings.PREPEND_WWW
        """

        # Check for denied User-Agents
        if 'HTTP_USER_AGENT' in request.META:
            for user_agent_regex in settings.DISALLOWED_USER_AGENTS:
                if user_agent_regex.search(request.META['HTTP_USER_AGENT']):
                    raise PermissionDenied('Forbidden user agent')

        # Check for a redirect based on settings.PREPEND_WWW
        host = request.get_host()
        must_prepend = settings.PREPEND_WWW and host and not host.startswith('www.')
        redirect_url = ('%s://www.%s' % (request.scheme, host)) if must_prepend else ''

        # Check if a slash should be appended
        if self.should_redirect_with_slash(request):
            path = self.get_full_path_with_slash(request)
        else:
            path = request.get_full_path()

        # Return a redirect if necessary
        if redirect_url or path != request.get_full_path():
            redirect_url += path
            return self.response_redirect_class(redirect_url)

    def should_redirect_with_slash(self, request):
        """
        Return True if settings.APPEND_SLASH is True and appending a slash to
        the request path turns an invalid path into a valid one.
        """
        if settings.APPEND_SLASH and not request.path_info.endswith('/'):
            urlconf = getattr(request, 'urlconf', None)
            return (
                not is_valid_path(request.path_info, urlconf) and
                is_valid_path('%s/' % request.path_info, urlconf)
            )
        return False

    def get_full_path_with_slash(self, request):
        """
        Return the full path of the request with a trailing slash appended.

        Raise a RuntimeError if settings.DEBUG is True and request.method is
        POST, PUT, or PATCH.
        """
        new_path = request.get_full_path(force_append_slash=True)
        if settings.DEBUG and request.method in ('POST', 'PUT', 'PATCH'):
            raise RuntimeError(
                "You called this URL via %(method)s, but the URL doesn't end "
                "in a slash and you have APPEND_SLASH set. Django can't "
                "redirect to the slash URL while maintaining %(method)s data. "
                "Change your form to point to %(url)s (note the trailing "
                "slash), or set APPEND_SLASH=False in your Django settings." % {
                    'method': request.method,
                    'url': request.get_host() + new_path,
                }
            )
        return new_path

    def process_response(self, request, response):
        """
        Calculate the ETag, if needed.

        When the status code of the response is 404, it may redirect to a path
        with an appended slash if should_redirect_with_slash() returns True.
        """
        # If the given URL is "Not Found", then check if we should redirect to
        # a path with a slash appended.
        if response.status_code == 404:
            if self.should_redirect_with_slash(request):
                return self.response_redirect_class(self.get_full_path_with_slash(request))

        if settings.USE_ETAGS and self.needs_etag(response):
            warnings.warn(
                "The USE_ETAGS setting is deprecated in favor of "
                "ConditionalGetMiddleware which sets the ETag regardless of "
                "the setting. CommonMiddleware won't do ETag processing in "
                "Django 2.1.",
                RemovedInDjango21Warning
            )
            if not response.has_header('ETag'):
                set_response_etag(response)

            if response.has_header('ETag'):
                return get_conditional_response(
                    request,
                    etag=response['ETag'],
                    response=response,
                )
        # Add the Content-Length header to non-streaming responses if not
        # already set.
        if not response.streaming and not response.has_header('Content-Length'):
            response['Content-Length'] = str(len(response.content))

        return response

    def needs_etag(self, response):
        """
        Return True if an ETag header should be added to response.
        """
        cache_control_headers = cc_delim_re.split(response.get('Cache-Control', ''))
        return all(header.lower() != 'no-store' for header in cache_control_headers)


class BrokenLinkEmailsMiddleware(MiddlewareMixin):

    def process_response(self, request, response):
        """
        Send broken link emails for relevant 404 NOT FOUND responses.
        """
        if response.status_code == 404 and not settings.DEBUG:
            domain = request.get_host()
            path = request.get_full_path()
            referer = force_text(request.META.get('HTTP_REFERER', ''), errors='replace')

            if not self.is_ignorable_request(request, path, domain, referer):
                ua = force_text(request.META.get('HTTP_USER_AGENT', '<none>'), errors='replace')
                ip = request.META.get('REMOTE_ADDR', '<none>')
                mail_managers(
                    "Broken %slink on %s" % (
                        ('INTERNAL ' if self.is_internal_request(domain, referer) else ''),
                        domain
                    ),
                    "Referrer: %s\nRequested URL: %s\nUser agent: %s\n"
                    "IP address: %s\n" % (referer, path, ua, ip),
                    fail_silently=True)
        return response

    def is_internal_request(self, domain, referer):
        """
        Returns True if the referring URL is the same domain as the current request.
        """
        # Different subdomains are treated as different domains.
        return bool(re.match("^https?://%s/" % re.escape(domain), referer))

    def is_ignorable_request(self, request, uri, domain, referer):
        """
        Return True if the given request *shouldn't* notify the site managers
        according to project settings or in situations outlined by the inline
        comments.
        """
        # The referer is empty.
        if not referer:
            return True

        # APPEND_SLASH is enabled and the referer is equal to the current URL
        # without a trailing slash indicating an internal redirect.
        if settings.APPEND_SLASH and uri.endswith('/') and referer == uri[:-1]:
            return True

        # A '?' in referer is identified as a search engine source.
        if not self.is_internal_request(domain, referer) and '?' in referer:
            return True

        # The referer is equal to the current URL, ignoring the scheme (assumed
        # to be a poorly implemented bot).
        parsed_referer = urlparse(referer)
        if parsed_referer.netloc in ['', domain] and parsed_referer.path == uri:
            return True

        return any(pattern.search(uri) for pattern in settings.IGNORABLE_404_URLS)
中间件CommonMiddleware源码
"""
Cross Site Request Forgery Middleware.

This module provides a middleware that implements protection
against request forgeries from other sites.
"""
from __future__ import unicode_literals

import logging
import re
import string

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.urls import get_callable
from django.utils.cache import patch_vary_headers
from django.utils.crypto import constant_time_compare, get_random_string
from django.utils.deprecation import MiddlewareMixin
from django.utils.encoding import force_text
from django.utils.http import is_same_domain
from django.utils.six.moves import zip
from django.utils.six.moves.urllib.parse import urlparse

logger = logging.getLogger('django.security.csrf')

REASON_NO_REFERER = "Referer checking failed - no Referer."
REASON_BAD_REFERER = "Referer checking failed - %s does not match any trusted origins."
REASON_NO_CSRF_COOKIE = "CSRF cookie not set."
REASON_BAD_TOKEN = "CSRF token missing or incorrect."
REASON_MALFORMED_REFERER = "Referer checking failed - Referer is malformed."
REASON_INSECURE_REFERER = "Referer checking failed - Referer is insecure while host is secure."

CSRF_SECRET_LENGTH = 32
CSRF_TOKEN_LENGTH = 2 * CSRF_SECRET_LENGTH
CSRF_ALLOWED_CHARS = string.ascii_letters + string.digits
CSRF_SESSION_KEY = '_csrftoken'


def _get_failure_view():
    """
    Returns the view to be used for CSRF rejections
    """
    return get_callable(settings.CSRF_FAILURE_VIEW)


def _get_new_csrf_string():
    return get_random_string(CSRF_SECRET_LENGTH, allowed_chars=CSRF_ALLOWED_CHARS)


def _salt_cipher_secret(secret):
    """
    Given a secret (assumed to be a string of CSRF_ALLOWED_CHARS), generate a
    token by adding a salt and using it to encrypt the secret.
    """
    salt = _get_new_csrf_string()
    chars = CSRF_ALLOWED_CHARS
    pairs = zip((chars.index(x) for x in secret), (chars.index(x) for x in salt))
    cipher = ''.join(chars[(x + y) % len(chars)] for x, y in pairs)
    return salt + cipher


def _unsalt_cipher_token(token):
    """
    Given a token (assumed to be a string of CSRF_ALLOWED_CHARS, of length
    CSRF_TOKEN_LENGTH, and that its first half is a salt), use it to decrypt
    the second half to produce the original secret.
    """
    salt = token[:CSRF_SECRET_LENGTH]
    token = token[CSRF_SECRET_LENGTH:]
    chars = CSRF_ALLOWED_CHARS
    pairs = zip((chars.index(x) for x in token), (chars.index(x) for x in salt))
    secret = ''.join(chars[x - y] for x, y in pairs)  # Note negative values are ok
    return secret


def _get_new_csrf_token():
    return _salt_cipher_secret(_get_new_csrf_string())


def get_token(request):
    """
    Returns the CSRF token required for a POST form. The token is an
    alphanumeric value. A new token is created if one is not already set.

    A side effect of calling this function is to make the csrf_protect
    decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie'
    header to the outgoing response.  For this reason, you may need to use this
    function lazily, as is done by the csrf context processor.
    """
    if "CSRF_COOKIE" not in request.META:
        csrf_secret = _get_new_csrf_string()
        request.META["CSRF_COOKIE"] = _salt_cipher_secret(csrf_secret)
    else:
        csrf_secret = _unsalt_cipher_token(request.META["CSRF_COOKIE"])
    request.META["CSRF_COOKIE_USED"] = True
    return _salt_cipher_secret(csrf_secret)


def rotate_token(request):
    """
    Changes the CSRF token in use for a request - should be done on login
    for security purposes.
    """
    request.META.update({
        "CSRF_COOKIE_USED": True,
        "CSRF_COOKIE": _get_new_csrf_token(),
    })
    request.csrf_cookie_needs_reset = True


def _sanitize_token(token):
    # Allow only ASCII alphanumerics
    if re.search('[^a-zA-Z0-9]', force_text(token)):
        return _get_new_csrf_token()
    elif len(token) == CSRF_TOKEN_LENGTH:
        return token
    elif len(token) == CSRF_SECRET_LENGTH:
        # Older Django versions set cookies to values of CSRF_SECRET_LENGTH
        # alphanumeric characters. For backwards compatibility, accept
        # such values as unsalted secrets.
        # It's easier to salt here and be consistent later, rather than add
        # different code paths in the checks, although that might be a tad more
        # efficient.
        return _salt_cipher_secret(token)
    return _get_new_csrf_token()


def _compare_salted_tokens(request_csrf_token, csrf_token):
    # Assume both arguments are sanitized -- that is, strings of
    # length CSRF_TOKEN_LENGTH, all CSRF_ALLOWED_CHARS.
    return constant_time_compare(
        _unsalt_cipher_token(request_csrf_token),
        _unsalt_cipher_token(csrf_token),
    )


class CsrfViewMiddleware(MiddlewareMixin):
    """
    Middleware that requires a present and correct csrfmiddlewaretoken
    for POST requests that have a CSRF cookie, and sets an outgoing
    CSRF cookie.

    This middleware should be used in conjunction with the csrf_token template
    tag.
    """
    # The _accept and _reject methods currently only exist for the sake of the
    # requires_csrf_token decorator.
    def _accept(self, request):
        # Avoid checking the request twice by adding a custom attribute to
        # request.  This will be relevant when both decorator and middleware
        # are used.
        request.csrf_processing_done = True
        return None

    def _reject(self, request, reason):
        logger.warning(
            'Forbidden (%s): %s', reason, request.path,
            extra={
                'status_code': 403,
                'request': request,
            }
        )
        return _get_failure_view()(request, reason=reason)

    def _get_token(self, request):
        if settings.CSRF_USE_SESSIONS:
            try:
                return request.session.get(CSRF_SESSION_KEY)
            except AttributeError:
                raise ImproperlyConfigured(
                    'CSRF_USE_SESSIONS is enabled, but request.session is not '
                    'set. SessionMiddleware must appear before CsrfViewMiddleware '
                    'in MIDDLEWARE%s.' % ('_CLASSES' if settings.MIDDLEWARE is None else '')
                )
        else:
            try:
                cookie_token = request.COOKIES[settings.CSRF_COOKIE_NAME]
            except KeyError:
                return None

            csrf_token = _sanitize_token(cookie_token)
            if csrf_token != cookie_token:
                # Cookie token needed to be replaced;
                # the cookie needs to be reset.
                request.csrf_cookie_needs_reset = True
            return csrf_token

    def _set_token(self, request, response):
        if settings.CSRF_USE_SESSIONS:
            request.session[CSRF_SESSION_KEY] = request.META['CSRF_COOKIE']
        else:
            response.set_cookie(
                settings.CSRF_COOKIE_NAME,
                request.META['CSRF_COOKIE'],
                max_age=settings.CSRF_COOKIE_AGE,
                domain=settings.CSRF_COOKIE_DOMAIN,
                path=settings.CSRF_COOKIE_PATH,
                secure=settings.CSRF_COOKIE_SECURE,
                httponly=settings.CSRF_COOKIE_HTTPONLY,
            )
            # Set the Vary header since content varies with the CSRF cookie.
            patch_vary_headers(response, ('Cookie',))

    def process_request(self, request):
        csrf_token = self._get_token(request)
        if csrf_token is not None:
            # Use same token next time.
            request.META['CSRF_COOKIE'] = csrf_token

    def process_view(self, request, callback, callback_args, callback_kwargs):
        if getattr(request, 'csrf_processing_done', False):
            return None

        # Wait until request.META["CSRF_COOKIE"] has been manipulated before
        # bailing out, so that get_token still works
        if getattr(callback, 'csrf_exempt', False):
            return None

        # Assume that anything not defined as 'safe' by RFC7231 needs protection
        if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
            if getattr(request, '_dont_enforce_csrf_checks', False):
                # Mechanism to turn off CSRF checks for test suite.
                # It comes after the creation of CSRF cookies, so that
                # everything else continues to work exactly the same
                # (e.g. cookies are sent, etc.), but before any
                # branches that call reject().
                return self._accept(request)

            if request.is_secure():
                # Suppose user visits http://example.com/
                # An active network attacker (man-in-the-middle, MITM) sends a
                # POST form that targets https://example.com/detonate-bomb/ and
                # submits it via JavaScript.
                #
                # The attacker will need to provide a CSRF cookie and token, but
                # that's no problem for a MITM and the session-independent
                # secret we're using. So the MITM can circumvent the CSRF
                # protection. This is true for any HTTP connection, but anyone
                # using HTTPS expects better! For this reason, for
                # https://example.com/ we need additional protection that treats
                # http://example.com/ as completely untrusted. Under HTTPS,
                # Barth et al. found that the Referer header is missing for
                # same-domain requests in only about 0.2% of cases or less, so
                # we can use strict Referer checking.
                referer = force_text(
                    request.META.get('HTTP_REFERER'),
                    strings_only=True,
                    errors='replace'
                )
                if referer is None:
                    return self._reject(request, REASON_NO_REFERER)

                referer = urlparse(referer)

                # Make sure we have a valid URL for Referer.
                if '' in (referer.scheme, referer.netloc):
                    return self._reject(request, REASON_MALFORMED_REFERER)

                # Ensure that our Referer is also secure.
                if referer.scheme != 'https':
                    return self._reject(request, REASON_INSECURE_REFERER)

                # If there isn't a CSRF_COOKIE_DOMAIN, require an exact match
                # match on host:port. If not, obey the cookie rules (or those
                # for the session cookie, if CSRF_USE_SESSIONS).
                good_referer = (
                    settings.SESSION_COOKIE_DOMAIN
                    if settings.CSRF_USE_SESSIONS
                    else settings.CSRF_COOKIE_DOMAIN
                )
                if good_referer is not None:
                    server_port = request.get_port()
                    if server_port not in ('443', '80'):
                        good_referer = '%s:%s' % (good_referer, server_port)
                else:
                    # request.get_host() includes the port.
                    good_referer = request.get_host()

                # Here we generate a list of all acceptable HTTP referers,
                # including the current host since that has been validated
                # upstream.
                good_hosts = list(settings.CSRF_TRUSTED_ORIGINS)
                good_hosts.append(good_referer)

                if not any(is_same_domain(referer.netloc, host) for host in good_hosts):
                    reason = REASON_BAD_REFERER % referer.geturl()
                    return self._reject(request, reason)

            csrf_token = request.META.get('CSRF_COOKIE')
            if csrf_token is None:
                # No CSRF cookie. For POST requests, we insist on a CSRF cookie,
                # and in this way we can avoid all CSRF attacks, including login
                # CSRF.
                return self._reject(request, REASON_NO_CSRF_COOKIE)

            # Check non-cookie token for match.
            request_csrf_token = ""
            if request.method == "POST":
                try:
                    request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
                except IOError:
                    # Handle a broken connection before we've completed reading
                    # the POST data. process_view shouldn't raise any
                    # exceptions, so we'll ignore and serve the user a 403
                    # (assuming they're still listening, which they probably
                    # aren't because of the error).
                    pass

            if request_csrf_token == "":
                # Fall back to X-CSRFToken, to make things easier for AJAX,
                # and possible for PUT/DELETE.
                request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')

            request_csrf_token = _sanitize_token(request_csrf_token)
            if not _compare_salted_tokens(request_csrf_token, csrf_token):
                return self._reject(request, REASON_BAD_TOKEN)

        return self._accept(request)

    def process_response(self, request, response):
        if not getattr(request, 'csrf_cookie_needs_reset', False):
            if getattr(response, 'csrf_cookie_set', False):
                return response

        if not request.META.get("CSRF_COOKIE_USED", False):
            return response

        # Set the CSRF cookie even if it's already set, so we renew
        # the expiry timer.
        self._set_token(request, response)
        response.csrf_cookie_set = True
        return response
中间件CsrfViewMiddleware源码
from django.conf import settings
from django.contrib import auth
from django.contrib.auth import load_backend
from django.contrib.auth.backends import RemoteUserBackend
from django.core.exceptions import ImproperlyConfigured
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import SimpleLazyObject


def get_user(request):
    if not hasattr(request, '_cached_user'):
        request._cached_user = auth.get_user(request)
    return request._cached_user


class AuthenticationMiddleware(MiddlewareMixin):
    def process_request(self, request):
        assert hasattr(request, 'session'), (
            "The Django authentication middleware requires session middleware "
            "to be installed. Edit your MIDDLEWARE%s setting to insert "
            "'django.contrib.sessions.middleware.SessionMiddleware' before "
            "'django.contrib.auth.middleware.AuthenticationMiddleware'."
        ) % ("_CLASSES" if settings.MIDDLEWARE is None else "")
        request.user = SimpleLazyObject(lambda: get_user(request))


class SessionAuthenticationMiddleware(MiddlewareMixin):
    """
    Formerly, a middleware for invalidating a user's sessions that don't
    correspond to the user's current session authentication hash. However, it
    caused the "Vary: Cookie" header on all responses.

    It's now a shim to allow a single settings file to more easily support
    multiple versions of Django. Will be RemovedInDjango20Warning.
    """
    def process_request(self, request):
        pass


class RemoteUserMiddleware(MiddlewareMixin):
    """
    Middleware for utilizing Web-server-provided authentication.

    If request.user is not authenticated, then this middleware attempts to
    authenticate the username passed in the ``REMOTE_USER`` request header.
    If authentication is successful, the user is automatically logged in to
    persist the user in the session.

    The header used is configurable and defaults to ``REMOTE_USER``.  Subclass
    this class and change the ``header`` attribute if you need to use a
    different header.
    """

    # Name of request header to grab username from.  This will be the key as
    # used in the request.META dictionary, i.e. the normalization of headers to
    # all uppercase and the addition of "HTTP_" prefix apply.
    header = "REMOTE_USER"
    force_logout_if_no_header = True

    def process_request(self, request):
        # AuthenticationMiddleware is required so that request.user exists.
        if not hasattr(request, 'user'):
            raise ImproperlyConfigured(
                "The Django remote user auth middleware requires the"
                " authentication middleware to be installed.  Edit your"
                " MIDDLEWARE setting to insert"
                " 'django.contrib.auth.middleware.AuthenticationMiddleware'"
                " before the RemoteUserMiddleware class.")
        try:
            username = request.META[self.header]
        except KeyError:
            # If specified header doesn't exist then remove any existing
            # authenticated remote-user, or return (leaving request.user set to
            # AnonymousUser by the AuthenticationMiddleware).
            if self.force_logout_if_no_header and request.user.is_authenticated:
                self._remove_invalid_user(request)
            return
        # If the user is already authenticated and that user is the user we are
        # getting passed in the headers, then the correct user is already
        # persisted in the session and we don't need to continue.
        if request.user.is_authenticated:
            if request.user.get_username() == self.clean_username(username, request):
                return
            else:
                # An authenticated user is associated with the request, but
                # it does not match the authorized user in the header.
                self._remove_invalid_user(request)

        # We are seeing this user for the first time in this session, attempt
        # to authenticate the user.
        user = auth.authenticate(request, remote_user=username)
        if user:
            # User is valid.  Set request.user and persist user in the session
            # by logging the user in.
            request.user = user
            auth.login(request, user)

    def clean_username(self, username, request):
        """
        Allows the backend to clean the username, if the backend defines a
        clean_username method.
        """
        backend_str = request.session[auth.BACKEND_SESSION_KEY]
        backend = auth.load_backend(backend_str)
        try:
            username = backend.clean_username(username)
        except AttributeError:  # Backend has no clean_username method.
            pass
        return username

    def _remove_invalid_user(self, request):
        """
        Removes the current authenticated user in the request which is invalid
        but only if the user is authenticated via the RemoteUserBackend.
        """
        try:
            stored_backend = load_backend(request.session.get(auth.BACKEND_SESSION_KEY, ''))
        except ImportError:
            # backend failed to load
            auth.logout(request)
        else:
            if isinstance(stored_backend, RemoteUserBackend):
                auth.logout(request)


class PersistentRemoteUserMiddleware(RemoteUserMiddleware):
    """
    Middleware for Web-server provided authentication on logon pages.

    Like RemoteUserMiddleware but keeps the user authenticated even if
    the header (``REMOTE_USER``) is not found in the request. Useful
    for setups when the external authentication via ``REMOTE_USER``
    is only expected to happen on some "logon" URL and the rest of
    the application wants to use Django's authentication mechanism.
    """
    force_logout_if_no_header = False
中间件AuthenticationMiddleware源码
from django.conf import settings
from django.contrib.messages.storage import default_storage
from django.utils.deprecation import MiddlewareMixin


class MessageMiddleware(MiddlewareMixin):
    """
    Middleware that handles temporary messages.
    """

    def process_request(self, request):
        request._messages = default_storage(request)

    def process_response(self, request, response):
        """
        Updates the storage backend (i.e., saves the messages).

        If not all messages could not be stored and ``DEBUG`` is ``True``, a
        ``ValueError`` is raised.
        """
        # A higher middleware layer may return a request which does not contain
        # messages storage, so make no assumption that it will be there.
        if hasattr(request, '_messages'):
            unstored_messages = request._messages.update(response)
            if unstored_messages and settings.DEBUG:
                raise ValueError('Not all temporary messages could be stored.')
        return response
中间件MessageMiddleware源码
"""
Clickjacking Protection Middleware.

This module provides a middleware that implements protection against a
malicious site loading resources from your site in a hidden frame.
"""

from django.conf import settings
from django.utils.deprecation import MiddlewareMixin


class XFrameOptionsMiddleware(MiddlewareMixin):
    """
    Middleware that sets the X-Frame-Options HTTP header in HTTP responses.

    Does not set the header if it's already set or if the response contains
    a xframe_options_exempt value set to True.

    By default, sets the X-Frame-Options header to 'SAMEORIGIN', meaning the
    response can only be loaded on a frame within the same site. To prevent the
    response from being loaded in a frame in any site, set X_FRAME_OPTIONS in
    your project's Django settings to 'DENY'.

    Note: older browsers will quietly ignore this header, thus other
    clickjacking protection techniques should be used if protection in those
    browsers is required.

    https://en.wikipedia.org/wiki/Clickjacking#Server_and_client
    """
    def process_response(self, request, response):
        # Don't set it if it's already in the response
        if response.get('X-Frame-Options') is not None:
            return response

        # Don't set it if they used @xframe_options_exempt
        if getattr(response, 'xframe_options_exempt', False):
            return response

        response['X-Frame-Options'] = self.get_xframe_options_value(request,
                                                                    response)
        return response

    def get_xframe_options_value(self, request, response):
        """
        Gets the value to set for the X_FRAME_OPTIONS header.

        By default this uses the value from the X_FRAME_OPTIONS Django
        settings. If not found in settings, defaults to 'SAMEORIGIN'.

        This method can be overridden if needed, allowing it to vary based on
        the request or response.
        """
        return getattr(settings, 'X_FRAME_OPTIONS', 'SAMEORIGIN').upper()
中间件XFrameOptionsMiddleware源码

  通过源码,我们明确的知道中间件其实就是一个个的类,这就意味着我们其实是可以通过继承类MiddlewareMixin自定义中间件的。首先我们来了解一下中间件可以定义的五个方法:

  • process_request(self, request)
  • process_view(self, request, view_func, view_args, view_kwargs)
  • process_template_response(self, request, response)
  • process_exception(self, request, exception)
  • process_response(self, request, response)

  每个方法都有对应的触发条件,接下来我们自定义两个中间件来看看(在应用下新建一个文件夹,里面新建一个py文件,文件夹和文件名字没有要求,然后里面写自定义中间件,之后再settings.py中注册该中间件即可):

  相应视图函数及执行的结果:

  发现process_exception和process_template_response方法没有触发,我们修改一下视图函数:

  接着再修改一下视图函数:

  由此可见(使用多个自定义中间件即可得出结论):

  • 不同中间件之间传递的request都是同一个对象
  • 中间件的执行是有序的,依照settings.py中的MIDDLEWARE中的注册顺序
  • 中间件的process_request方式是在执行视图函数之前执行的,按照注册顺序从上往下执行,如果中途有个中间件的该方法return HttpResponse对象,那么就不会接下往下执行,而是从该中间件的process_response方法开始执行(没有该方法的中间件会直接跳过),依次从下往下,将结果返回给浏览器。(其实无论哪个中间件的哪个方法被触发时return HttpResponse对象,就会马上回头,从当前中间件开始,按从下往上的顺序开始执行process_response方法来向浏览器返回数据)

  • 中间件的process_view方法是请求通过了所有中间件的process_request后成功匹配路由,准备执行对应视图函数之前执行的。
  • 中间件的process_exception方法是执行视图函数的过程中出现报错信息时执行的。
  • 中间件的process_template_response是当return的HttpResponse对象中含有render属性时触发的,属性render对应的值是一个内存地址,比如说函数名或者类名,但是要求他们执行时可以返回一个HttpResponse对象,不然会报错。
  • 中间件的process_response,response是视图函数返回的HttpResponse对象(也就是说这是Django后台处理完之后给出一个的一个具体的视图)。该方法的返回值(必须要有返回值)也必须是HttpResponse对象。如果不返回response而返回其他对象,则浏览器不会拿到Django后台给他的视图,而是我的中间件中返回的对象。多个中间件中的process_response方法是按照MIDDLEWARE中的注册顺序倒序执行的,也就是说第一个中间件的process_request方法首先执行,而它的process_response方法最后执行,最后一个中间件的process_request方法最后一个执行,它的process_response方法是最先执行。

  其实中间件可以为我们实现很多事情,比如涉及到全局权限、控制用户访问频率、全局登录校验、用户访问白名单、黑名单等。

 3. csrf跨站请求伪造

  CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装成受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。

  钓鱼网站就是建一个和正常的网站一模一样的网站,然后用户在输入的时候调的也是正常网站的接口去处理,所以用户的钱会扣掉,但是并没有转给指定的人,其实就是建了一个和正常网站一模一样的东西,然后偷偷的在转给目标用户那里,偷偷的将input框当前的name去掉,然后用了一个hidden隐藏起来,在隐藏起来的input框中给一个默认的value,具体示例如下:

def transfer(request):
    if request.method == 'POST':
        username = request.POST.get('name')
        money = request.POST.get('money')
        others = request.POST.get('others')
        print('%s 给 %s 转了 %s块钱'%(username,others,money))
    return render(request,'transfer.html')
正常网站视图函数
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
    <link rel="stylesheet" href="/static/bootstrap-3.3.7-dist/css/bootstrap.min.css">
    <script src="/static/bootstrap-3.3.7-dist/js/bootstrap.min.js"></script>
</head>
<body>
<h1>正经网站</h1>
<form action="" method="post">
    <p>name:<input type="text" name="name"></p>
    <p>money:<input type="text" name="money"></p>
    <p>others:<input type="text" name="others"></p>
    <input type="submit">
</form>
</body>
</html>
正常网站HTMl
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
    <link rel="stylesheet" href="/static/bootstrap-3.3.7-dist/css/bootstrap.min.css">
    <script src="/static/bootstrap-3.3.7-dist/js/bootstrap.min.js"></script>
</head>
<body>
<h1>钓鱼网站</h1>
<form action="http://127.0.0.1:8000/transfer" method="post">
    <p>name:<input type="text" name="name"></p>
    <p>money:<input type="text" name="money"></p>
    <p>others:
        <input type="text" >
        <input type="hidden" name="others" value="mcc" style="display: none" >
    </p>
    <input type="submit">
</form>
</body>
</html>
钓鱼网站HTML

   在此之外,我们通过form表单和ajax往后端提交数据时都会报大黄页:

  其实csrf中间件就是用来防止跨站请求伪造的,它的原理也是在HTML页面中隐藏一个Input框,name固定是'csrfmiddlewaretoken',value是动态生成的字符串,每次页面刷新就会变化,如果csrf拿到的的value跟它想的不一样,就会报大黄页的错误,而之前我们form表单和ajax中都没有csrf token,所以报错是必然的。

  form表单中增加csrf token的方法:

<form action="" method="post">
    {% csrf_token %} <!--就是这一句-->
    username:<input type="text" name="username">
    password:<input type="text" name="password">
    <input type="submit">
</form>

   随后就能发现页面检查时有csrf token了:

  ajax增加csrf token的方法

<script>
    $('button').click(function () {
        $.ajax({
            url:'',
            type:'post',
            data:{'name': 'hello', 'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val()},
            success:function (data) {
                console.log(data)
            }
        })
    })
</script>

   ajax增加csrf token的方法二:

<script>
    $('button').click(function () {
        $.ajax({
            url:'',
            type:'post',
            data:{'name': 'hello', 'csrfmiddlewaretoken': '{{ csrf_token }}'},
            success:function (data) {
                console.log(data)
            }
        })
    })
</script>

  第二种方法一定要将{{ csrf_token }}放在 ''里面,不然会出现后端没有报403 forbidden,但是你的数据就是无法提交至后端的情况。

  ajax的该方法其实就是将form表单中的csrf token拿出来放进自己的data。

4. 局部与全局的csrf校验装饰器

  首先需要导入一共三个装饰器:

from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt,csrf_protect

   method_decorator是给类装饰用的装饰器,而csrf_exempt是忽略csrf校验的函数装饰器,csrf_protect是使用csrf校验的函数装饰器。

  对于FBV而言

  局部禁用csrf校验:

@csrf_exempt
def index1(request):
    return HttpResponse('ok')

  局部使用csrf校验:

@csrf_protect
def login(request):
  pass

  对于CBV而言

  csrf_protect跟正常的CBV装饰器一样,可以有三种方式(开始CBV装饰器提到的三种)。

而csrf_exempt只有两种方式:

@method_decorator(csrf_exempt,name='dispatch')  # 第一种 
class Index3(View):
# @method_decorator(csrf_exempt)   # 第二种  
    def dispatch(self, request, *args, **kwargs):
          super().dispatch(request,*args,**kwargs)  
#其实都是给dispatch加

三. auth模块

  我们在开发一个网站的时候,无可避免的需要设计实现网站的用户系统。此时我们需要实现包括用户注册、用户登录、用户认证、注销、修改密码等功能,这还真是个麻烦的事情呢。

  Django作为一个完美主义者的终极框架,当然也会想到用户的这些痛点。它内置了强大的用户认证系统--auth,它默认使用 auth_user 表来存储用户数据。

  首先也要先导入auth模块:

from django.contrib import auth

3.1 命令行创建管理员

  管理员账号可以登录管理系统,即Django urls.py自带的第一个路由:

3.2 auth.authenticate()

  提供了用户认证功能,即验证用户名以及密码是否正确,至少需要username 、password两个关键字参数。

  如果认证成功(用户名和密码正确有效),便会返回一个 User 对象。

  authenticate()会在该 User 对象上设置一个属性来标识后端已经认证了该用户,且该信息在后续的登录过程中是需要的。

auth.authenticate(request,username=username,password=password)

 3.2 auth.login(HttpRequest, user)

  该函数接受一个HttpRequest对象,以及一个经过认证的User对象。

  该函数实现一个用户登录的功能。它本质上会在后端为该用户生成相关session数据。

from django.contrib.auth import authenticate, login
   
def auth_login(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        # models.User.objects.filter(username=username,password=password).first()
        user_obj = auth.authenticate(request,username=username,password=password)
        if user_obj:
            # 记录用户状态
            # request.session['name'] = 'jason'
            auth.login(request,user_obj)  # 一旦记录了,可以在任意的地方通过request.user获取到当前登录对象
            return HttpResponse('ok')

    return render(request,'auth_login.html')

  auth.authenticate()和auth.login()的优点:

# 只要登陆成功执行了auth.login(request,user)
# 之后在其他任意的视图函数中都通过request.user获取当前登陆用户对象

# 当没有执行auth.login, request.user打印出来的是匿名用户。将session表数据删除即可演示该效果
# 如何判断request.user用户是否通过auth.login登陆呢?request.user.is_authenticated()

# 为何执行auth.login之后,其他视图函数中就可以通过request.user拿到当前登陆对象呢?
  想想django的中间件中有没有一个叫Auth啥的中间件,它干了件什么事,能不能推导一下?
  取出session去表里面查相应的数据,然后放到request.user中,
  点进去看一下这个中间件确实如此

3.3 logout(request)

  该函数接受一个HttpRequest对象,无返回值。

  当调用该函数时,当前请求的session信息会全部清除。该用户即使没有登录,使用该函数也不会报错。

auth.logout(request)
# 等价于删除session数据request.session.flush()

3.4  is_authenticated()

  判断当前用户是否已经通过验证

def auth_index(request):
    print(request.user.is_authenticated())  # 判断当前用户是否已经通过验证
    print(request.user, type(request.user))  # 获取当前登录用户对象
    return HttpResponse('ok')

3.5 check_password(password)与set_password(password)

  auth 提供的一个检查密码是否正确的方法,需要提供当前请求用户的密码。密码正确返回True,否则返回False。

def auth_password(request):
    print(request.user.password)
    is_res = request.user.check_password('jason123')  # 校验密码是否一致
    if is_res:
        request.user.set_password('666')  # 设置新密码
        request.user.save()  # 修改密码必须save保存  不然无效
    return HttpResponse('ok')

3.6 create_user()

  auth 提供的一个创建新用户的方法,需要提供必要参数(username、password)等

from django.contrib.auth.models import User
user = User.objects.create_user(username='用户名',password='密码',email='邮箱',...)

  如果你继承AbstractUser扩展了auth_user类,假设扩展后的类叫UserInfo,那么创建用户时代码要修改一下:

from app01 import models
user = models.UserInfo.objects.create_user(username='用户名',password='密码',email='邮箱',...)

3.7 create_superuser()

  auth 提供的一个创建新的超级用户的方法,需要提供必要参数(username、password)等。

from django.contrib.auth.models import User
user = User.objects.create_superuser(username='用户名',password='密码',email='邮箱',...)

 3.8 User对象的属性

  • User对象属性:username, password等
  • is_staff : 用户是否拥有网站的管理权限.
  • is_active : 是否允许用户登录, 设置为 False,可以在不删除用户的前提下禁止用户登录。

3.9 login_required()

  auth 给我们提供的一个装饰器工具,用来快捷的给某个视图添加登录校验。

  局部配置

from django.contrib.auth.decorators import login_required
@login_required(login_url='/auth_login/')  # 局部配置
def auth_home(request):
    return HttpResponse('home必须登录才能访问')

  全局配置

  在setting.py中写配置:

LOGIN_URL = '/auth_login/'  # 写未登录时转向的路由
# 之后都这么写即可
@login_required
def auth_xxx(request):
    return HttpResponse('xxx必须登录才能访问')

3.10 扩展默认的auth user表

  内置的auth_user表字段有限,有时没法满足我们需求,这时可以通过继承AbstractUser类来扩展该表。

  第一种方式:一对一关联表(不推荐)

from django.contrib.auth.model import User

class UserDetail(models.Models):
  phone = models.CharField(max_length=11)
  user = models.OnoToOneField(to=User)

  第二种:面向对象的继承

from django.contrib.auth.models import User,AbstractUser
class UserInfo(AbstractUser):
  phone = models.CharField(max_length=32)

# 需要在配置文件中,指定我不再使用默认的auth_user表而是使用我自己创建的Userinfo表
AUTH_USER_MODEL = "app名.models里面对应的模型表名"


"""
自定义认证系统默认使用的数据表之后,我们就可以像使用默认的auth_user表那样使用我们的UserInfo表了。
库里面也没有auth_user表了,原来auth表的操作方法,现在全部用自定义的表均可实现
"""

3.11 auth组件同ajax共同使用的注意事项

  当使用auth组件进行认证登录之后,只要是需要同后端发post等请求的ajax,就会遇到403Forbidden,而这个403跟中间件csrf是没有关系的。这个时候我们需要在ajax携带的数据(data)中加上‘csrfmiddlewaretoken’,如果是前后端不分离,那么可以采用上述的两种加csrfmiddlewaretoken的方式;但是ajax是写在单独的js文件中,那么只能采用'csrfmiddlewaretoken': $("[name='csrfmiddlewaretoken']").val()}的方式。加上该方式之后,虽然后端还是会提示403Forbidden,但是数据是可能正常交互的。

3.12 关于不使用密码通过auth.authenticate认证

  采用auth组件进行登录验证时,是需要传用户名和密码的,这是源码里面的需求:

  可是我们有手机号与验证码就可以登录的需求,那就需要重写该方法,同时还需要在项目中的settings.py文件中配置我们重写的类。

  这里是在user APP下新建authenticate.py文件,里面重写ModelBackend的authenticate方法:

from django.contrib.auth.backends import ModelBackend
from rest_framework.throttling import SimpleRateThrottle
from user import models
import re


class LoginWithPhoneBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        # 这里通过正则校验是否是手机号的格式
        if not re.match(r'^1[3-9]\d{9}$', username):
            user = models.User.objects.filter(username=username).first()
            if user.check_password(password) and self.user_can_authenticate(user):
                return user
        else:
            user = models.User.objects.filter(phone=username).first()
            # 校验用户是否处于激活状态,以is_active字段的值为依据
            if self.user_can_authenticate(user):
                return user

  然后在设置文件中配置:

# auth认证中返回user对象的方法,默认是ModelBackend
AUTHENTICATION_BACKENDS = ['user.authenticates.LoginWithPhoneBackend']

  这样即可实现通过手机号也能通过auth.authenticate认证,从而登录。

  关于发送短信可以看这篇博客,使用的是容联(注册后可以免费发160条短信),接口使用也不会很复杂。点击前往发送短信的博客

 

posted @ 2019-06-18 22:28  maoruqiang  阅读(442)  评论(0编辑  收藏  举报