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()
问题原因及修复方案
原因分析
PasswordField内置验证逻辑:django-zxcvbn-password的PasswordField自带弱密码检测,当识别到弱密码时,会自动将该字段从cleaned_data中移除,导致cleaned_data.get("password")返回None。- 确认密码字段名错误:代码中
cleaned_data.get("password2")是错误的,PasswordConfirmationField的字段名是confirm_password,而非password2,这会导致confirm_password同样为None,触发zxcvbn库的参数错误。 - 错误提示逻辑混乱:当前代码无论密码强度如何,都添加"Passwords do not match."的错误,与实际验证场景不符。
修复步骤
- 规避
None值传入zxcvbn:调用zxcvbn前先检查password是否存在,不存在则跳过(PasswordField已自动添加弱密码错误)。 - 修正确认密码字段名:将
cleaned_data.get("password2")改为cleaned_data.get("confirm_password")。 - 调整错误提示逻辑:区分密码强度不足和密码不匹配的错误信息,同时修正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
相关产品推荐
相关产品推荐

