django基于authenticate()函数的源码解析

如果我们使用自身的一个账号和密码进行登录验证的话,不得不使用authenticate()函数,
至于authenticate()是怎么实现的,下面一一道来。
下面这个代码是个登录视图,省略了一部分代码,应该可以看懂,看不懂的话,拉到最后,看完整版的:

class LoginView(View):
    def post(self, request):
       #。。。代码省略
        user = authenticate(username=username, password=password)
	   #。。。代码省略

上图是一个继承了View的登录视图,username和password就是从浏览器传递过来的,虽然省略了很多代码,但是不影响理解,只需要知道,如果验证成功后,返回的是登录用户的user对象就行了。
直接进去authenticate()函数里探个究竟:

def authenticate(request=None, **credentials):
    """
    If the given credentials are valid, return a User object.
    如果给定的凭据有效,则返回一个User对象。
    """
    for backend, backend_path in _get_backends(return_tuples=True):
    	#_get_backends获取所有的认证模板,可以自定义,也可以用django默认的
        try:# backend_path = 'django.contrib.auth.backends.ModelBackend' backend = ModelBackend对象
        	#下面这段代码可以暂时性忽略
            inspect.getcallargs(backend.authenticate, request, **credentials)
        except TypeError:
            # This backend doesn't accept these credentials as arguments. Try the next one.
            #此后端不接受这些凭据作为参数。试试下一个,就是说验证不通过,试试下一个验证后端。
            #Django在使用他们的时候,会遍历所有的auth backends,一旦发现有一个backend校验通过,即返回User对象,那么将会停止下面backend的校验,并且将校验成功的backend绑定在该用户上放入session中,此后如果再次调用该方法,那么将会使用session中的backend进行校验,而不再遍历所有backend了。
            continue
        try:#credentials = {"username":13569784692,"password":123456abc}
            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

_get_backends()这个部分的源码我没写,可以参考别人写的关于认证后端的博客接下来注意下面这段代码,它其实就是去调用每个认证后台的authenticate(),只要同过一个,那么我们就认为都通过:

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

django默认的认证后端只有ModelBackend这一个(详细参考),点进去ModelBackend类的authenticate()方法看一下:

    def authenticate(self, request, username=None, password=None, **kwargs):
        if username is None:
        	#UserModel.USERNAME_FIELD代表你用那个变量验证,是用户名还是手机号,
        	#可以在setting.py文件里自定义,也可以在程序里定义,这样就能实现,用户名可	以是手机号,邮箱或者其他字段
            username = kwargs.get(UserModel.USERNAME_FIELD)
        try:
            user = UserModel._default_manager.get_by_natural_key(username)#把username作为条件,在这一步查了数据库 user:13569784692
        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

接下来进入check_password()函数里看一下:

    def check_password(self, raw_password):
        """
        Return a boolean of whether the raw_password was correct. Handles
        hashing formats behind the scenes.
        返回raw_password是否正确的布尔值。处理
        幕后的哈希格式。
        """
        def setter(raw_password):
            self.set_password(raw_password)
            # Password hash upgrades shouldn't be considered password changes.
            self._password = None
            self.save(update_fields=["password"])
        return check_password(raw_password, self.password, setter)

setter是干嘛的直接不管,因为它暂时还没有被用到,目前只是一个参数。直接进入check_password()函数:

def check_password(password, encoded, setter=None, preferred='default'):
	#is_password_usable()检查提供的字符串是否是可以用check_password()验证的哈希密码。
    if password is None or not is_password_usable(encoded):
        return False

    preferred = get_hasher(preferred)##django默认选中加密列表的第一个
    #代码略。。。

进入get_hasher()看一下:

def get_hasher(algorithm='default'):
    """
    返回一个已加载的密码散列器实例。
    如果algorithm是'default',则返回默认的散列值。懒加载的形式导入hashers
    如果需要,在项目的设置文件中指定。
    """
    if hasattr(algorithm, 'algorithm'):
        return algorithm

    elif algorithm == 'default':
        return get_hashers()[0]

    else:
        hashers = get_hashers_by_algorithm()#获取所有加密算法的类的字典{"加密算法名":"django.contrib.auth.hashers.PBKDF2PasswordHasher",....}
        try:
            return hashers[algorithm]#根据algorithm从众多加密算法的字典中选一个合适的算法
        except KeyError:
            raise ValueError("Unknown password hashing algorithm '%s'. "
                             "Did you specify it in the PASSWORD_HASHERS "
                             "setting?" % algorithm)

再回到def check_password()函数:

def check_password(password, encoded, setter=None, preferred='default'):
  	#代码略。。。

    preferred = get_hasher(preferred)##django默认选中加密列表的第一个,这里暂时和数据库中的那个密码的加密方式没关系,后面才进行对比
    try:
        hasher = identify_hasher(encoded)
    except ValueError:
        # encoded is gibberish or uses a hasher that's no longer installed.
        #编码是一个随机的散列值或者使用一个从没有被注册过的散列器。
        return False
	#代码略。。。

到identify_hasher(encoded)代码里看一下:

def  identify_hasher(encoded):
  	#判断数据库里的密码属于那种加密类型
    if ((len(encoded) == 32 and '$' not in encoded) or
            (len(encoded) == 37 and encoded.startswith('md5$$'))):
        algorithm = 'unsalted_md5'
    # Ancient versions of Django accepted SHA1 passwords with an empty salt.
    elif len(encoded) == 46 and encoded.startswith('sha1$$'):
        algorithm = 'unsalted_sha1'
    else:
        algorithm = encoded.split('$', 1)[0]
    return get_hasher(algorithm)#返回使用的加密算法的类的名字

再回到def check_password()函数:

def check_password(password, encoded, setter=None, preferred='default'):
   
   #代码略。。。

    preferred = get_hasher(preferred)##django默认选中加密列表的第一个,因为要通过它对最原生的密码进行加密
    try:
        hasher = identify_hasher(encoded)
    except ValueError:
        # encoded is gibberish or uses a hasher that's no longer installed.
        #编码是一个随机的散列值或者使用一个从没有被注册过的散列器。
        return False

    #判断两者的加密方式是否是同一个
    hasher_changed = hasher.algorithm != preferred.algorithm
    # 官方文档解释为,用户登录之后,如果他们的密码没有以首选的密码算法来储存,Django会自动将算法升级为首	选的那个。
    #个人理解,如果使用了不同的加密方法,把数据库中密码的加密方法改成和preferred的加密方法,使双方保持一致,这里只是进行判断,传递进来的setter()才是真正进行修改的方法。
    must_update = hasher_changed or preferred.must_update(encoded)
    #这段代码其实就是把明文密码按照hasher对应的加密方式进行加密,然后再和encoded进行比较。
    is_correct = hasher.verify(password, encoded)

    if not is_correct and not hasher_changed and must_update:
    	#我也不知道它在干什么
        hasher.harden_runtime(password, encoded)
	#如果传递了setter,并且密码是对的and两个加密方式不一样,需要修改,才掉setter()
    if setter and is_correct and must_update:
        setter(password)
    #如果密码输对了,没啥事,就直接返回了
    return is_correct


至此,check_password()函数执行完,一路返回到

class ModelBackend:
   

    def authenticate(self, request, username=None, password=None, **kwargs):
     #代码略。。。
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user

接下来执行user_can_authenticate(),这个函数很好理解,不解释,直接看代码:

    def user_can_authenticate(self, user):
        """
        Reject users with is_active=False. Custom user models that don't have
        that attribute are allowed.
        拒绝is_active=False的用户。定制用户模型没有
        这个属性是允许的
        """
        is_active = getattr(user, 'is_active', None)
        return is_active or is_active is None

此函数执行完,一路返回另一个authenticate()函数:

def authenticate(request=None, **credentials):
    """
    If the given credentials are valid, return a User object.
    如果给定的凭据有效,则返回一个User对象。
    """
    for backend, backend_path in _get_backends(return_tuples=True):
        try:# backend_path = 'django.contrib.auth.backends.ModelBackend' backend = ModelBackend对象
            inspect.getcallargs(backend.authenticate, request, **credentials)
        except TypeError:
            # This backend doesn't accept these credentials as arguments. Try the next one.
            #此后端不接受这些凭据作为参数。试试下一个
            continue
        try:#credentials = {"username":13569784692,"password":123456abc}
            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.
        #一旦发现有一个backend校验通过,即返回User对象,那么将会停止下面backend的校验,并且将校验成功的backend绑定在该用户上放入session中,此后如果再次调用该方法,那么将会使用session中的backend进行校验,而不再遍历所有backend了。
        user.backend = backend_path
        return user

接下俩,不用多说,直接调用django内置的login()登录函数登录就行了:

class LoginView(View):
    def post(self, request):
        req_data = json.loads(request.body.decode())
        username = req_data.get('username')
        password = req_data.get('password')
        remember = req_data.get('remember')

        import re
        if re.match(r'^1[3-9]\d{9}$', username):
            User.USERNAME_FIELD = 'mobile'
        else:
            User.USERNAME_FIELD = 'username'

        if not all([username, password]):
            return JsonResponse({'code': 400,
                                 'message': '缺少必传参数'})
        user = authenticate(username=username, password=password)

        if user is None:
            return JsonResponse({'code': 400,
                                 'message': '用户名或密码错误'})

        # ② 保存登录用户的状态信息

        login(request, user)

        if not remember:
            # 如果未选择记住登录,浏览器关闭即失效
            request.session.set_expiry(0)

        # ③ 返回响应,登录成功
        response = JsonResponse({'code': 0,
                                 'message': 'OK'})

        # 设置 cookie 保存 username 用户名
        response.set_cookie('username',
                            user.username,
                            max_age=3600 * 24 * 14)

        return response

参考:https://www.cnblogs.com/wangwei916797941/p/7398976.html
参考:官方文档

上一篇:最简单安全的Android服务器后端


下一篇:android – 使用Kii Cloud SDK修改存储桶写入访问级别时出现异常