You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django使用django-zxcvbn-password时弱密码触发NoneType错误

Django注册表单弱密码时password为NoneType的问题排查

问题现象

Django项目中使用django-zxcvbn-password库实现注册表单的密码安全验证,输入强密码时功能正常,但输入弱密码时,password字段无法获取值,变为NoneType,进而触发TypeError: object of type 'NoneType' has no len()错误;未使用该库时功能完全正常。

注册表单代码

class SignUpForm(forms.Form):

    """password_regex = RegexValidator(
        regex=r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$',
        message="Password must be at least 8 characters in length and contain at least one uppercase letter, "
                "one lowercase letter, and one digit. "
    )"""
    
    name_regex = RegexValidator(
        regex=r'^[A-Z][a-z]{2,149}$',
        message="Names must begin with an uppercase letter followed by lowercase letters."
    )
    username_regex = RegexValidator(
        regex=r'^[a-zA-Z][a-zA-Z0-9]{5,149}$',
        message="Username must begin with a letter followed by letters or digits and have a length of at least 6 "
                "characters and a maximum of 150 characters. "
    )
    username = forms.CharField(validators=[username_regex], min_length=5, max_length=150)
    email = forms.EmailField(validators=[EmailValidator],
                             error_messages={'invalid': 'Please enter a valid email address.'})
    first_name = forms.CharField(validators=[name_regex], min_length=2, max_length=150)
    last_name = forms.CharField(validators=[name_regex], min_length=2, max_length=150)
    password = PasswordField()
    confirm_password = PasswordConfirmationField(confirm_with='password')
    #password = forms.CharField(widget=forms.PasswordInput, validators=[password_regex], min_length=8)
    #confirm_password = forms.CharField(widget=forms.PasswordInput)

    def clean(self):
        cleaned_data = super().clean()
        password = cleaned_data.get("password")
        confirm_password = cleaned_data.get("password2")
        print(password)
        score = zxcvbn(password, confirm_password)['score']
        if score <= 2:
            self.add_error("password", "Passwords do not match.")
        elif score == 3:
            self.add_error("password", "Passwords do not match.")

        if password and confirm_password and password != confirm_password:
            self.add_error("confirm_password", "Passwords do not match.")

模板代码

{% load static %}
<!DOCTYPE html>
{% autoescape on %}
<html>
<head>
  <title>SIGN UP</title>
  <link rel="stylesheet" href="{% static 'css/signup.css' %}">
</head>
<body>
  <h1>SIGN UP</h1>
  <form method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Submit">
    <h3> If you already have an account sign in</h3>
    <button type="button"><a href='signin'>Sign In</a></button>
  </form>
</body>
</html>
{% endautoescape %}

错误栈信息

Traceback (most recent call last):
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\django\contrib\staticfiles\handlers.py", line 80, in __call__
    return self.application(environ, start_response)
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\django\core\handlers\wsgi.py", line 131, in __call__
    response = self.get_response(request)
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\django\core\handlers\base.py", line 140, in get_response
    response = self._middleware_chain(request)
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\django\core\handlers\exception.py", line 57, in inner
    response = response_for_exception(request, exc)
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\django\core\handlers\exception.py", line 140, in response_for_exception
    response = handle_uncaught_exception(
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\django\core\handlers\exception.py", line 181, in handle_uncaught_exception
    return debug.technical_500_response(request, *exc_info)
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\django_extensions\management\technical_response.py", line 40, in null_technical_500_response
    raise exc_value.with_traceback(tb)
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\django\core\handlers\exception.py", line 55, in inner
    response = get_response(request)
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\django\core\handlers\base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "C:\Users\User.DESKTOP-TJSM0H2\git\agenda_project\agenda\views.py", line 41, in signup
    print(form)
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\django\forms\utils.py", line 67, in render
    context = context or self.get_context()
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\django\forms\forms.py", line 326, in get_context
    top_errors = self.non_field_errors().copy()
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\django\forms\forms.py", line 362, in non_field_errors
    return self.errors.get(
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\django\forms\forms.py", line 200, in errors
    self.full_clean()
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\django\forms\forms.py", line 438, in full_clean
    self._clean_form()
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\django\forms\forms.py", line 459, in _clean_form
    cleaned_data = self.clean()
  File "C:\Users\User.DESKTOP-TJSM0H2\git\agenda_project\agenda\forms.py", line 55, in clean
    score = zxcvbn(password, confirm_password)['score']
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\zxcvbn\__init__.py", line 27, in zxcvbn
    matches = matching.omnimatch(password, ranked_dictionaries)
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\zxcvbn\matching.py", line 90, in omnimatch
    matches.extend(matcher(password, _ranked_dictionaries=_ranked_dictionaries))
  File "C:\Users\User.DESKTOP-TJSM0H2\AppData\Local\Programs\Python\Python310\lib\site-packages\zxcvbn\matching.py", line 98, in dictionary_match
    length = len(password)
TypeError: object of type 'NoneType' has no len()

问题原因及修复方案

原因分析

  1. PasswordField内置验证逻辑:django-zxcvbn-password的PasswordField自带弱密码检测,当识别到弱密码时,会自动将该字段从cleaned_data中移除,导致cleaned_data.get("password")返回None。
  2. 确认密码字段名错误:代码中cleaned_data.get("password2")是错误的,PasswordConfirmationField的字段名是confirm_password,而非password2,这会导致confirm_password同样为None,触发zxcvbn库的参数错误。
  3. 错误提示逻辑混乱:当前代码无论密码强度如何,都添加"Passwords do not match."的错误,与实际验证场景不符。

修复步骤

  1. 规避None值传入zxcvbn:调用zxcvbn前先检查password是否存在,不存在则跳过(PasswordField已自动添加弱密码错误)。
  2. 修正确认密码字段名:将cleaned_data.get("password2")改为cleaned_data.get("confirm_password")。
  3. 调整错误提示逻辑:区分密码强度不足和密码不匹配的错误信息,同时修正zxcvbn的调用方式(第二个参数是辅助验证的字符串,如用户名、邮箱,而非确认密码)。

修改后的clean方法代码:

def clean(self):
    cleaned_data = super().clean()
    password = cleaned_data.get("password")
    confirm_password = cleaned_data.get("confirm_password")
    
    # 仅当password存在时进行强度验证
    if password:
        score = zxcvbn(password)['score']
        if score <= 2:
            self.add_error("password", "Password is too weak. Please choose a stronger password.")
        elif score == 3:
            self.add_error("password", "Password could be stronger. Consider adding special characters or increasing length.")
    
    # 验证密码是否匹配
    if password and confirm_password and password != confirm_password:
        self.add_error("confirm_password", "Passwords do not match.")
    
    return cleaned_data

内容的提问来源于stack exchange,提问作者Junior

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 06:46:04