问题来源于对一段使用django-simple-captcha的思考与改进:
# form.py
class CaptchaTestForm(forms.Form):
username = forms.CharField(label='用户名')
password = forms.CharField(label='密码', widget=forms.PasswordInput)
captcha = CaptchaField(error_messages={'required': u'验证码不能为空', 'invalid': u'验证码错误'})
# views.py
def loginView(request):
tips = '请登录'
userLogin = True
if request.method == 'POST':
print(request.POST)
user = CaptchaTestForm(request.POST)
if user.is_valid(): # 注意验证了哪些内容
u = user.cleaned_data['username']
p = user.cleaned_data['password']
if MyUser.objects.filter(username=u):
uu = authenticate(username=u, password=p)
# Django2.0 版本以后用authenticate进行用户效验,即便用户名和密码输入正确
# 但是is_active属性为0的话依然返回None, 可以在settings.py中设置改变
if uu:
if uu.is_active:
login(request, uu)
tips = '登陆成功'
return redirect(reverse('user:success'))
else:
tips = '用户已被封禁,请联系管理员'
else:
tips = '账号密码错误,请重新输入'
else:
tips = '用户不存在,请注册'
else:
tips = '登陆失败,验证码错误'
user = CaptchaTestForm()
return render(request, 'user.html', locals())
(验证码的正确性在前端用Ajax判断,返回0/1,user.is_valid()由此判断表单的有效性)
其实这里还有点问题:逻辑模糊与重复
逻辑模糊:user.is_valid()检查username,password,captcha三个字段,倘若我给username和password字段设置验证器,凭什么敢肯定user.is_valid()=False就一定是captcha的问题?(上述代码敢肯定是因为没给username和password设限,这两项怎样都能通过表单验证,username和password的具体问题留到 if user.is_valid()
后面进行逐一筛选了)
逻辑重复:万一用的是UserCreationForm、AuthenticationForm等与数据库联系起来的表单,user.is_valid()会检查数据库中用户名重复、密码过于简单、登录密码错误等等信息,只若账号异常,会给出一个笼统的Form is invalid提示,并不知道具体是哪里发生了错误。并且和if user.is_valid()
后面那些判断重复了,user.is_valid()已经检查过一遍账号信息了,if里面又细细检查并给出不同的tips,逻辑重复!
对此给出改进:剥离验证码字段,其他交给数据库处理
# form.py
class CaptchaForm(forms.Form):
captcha = CaptchaField(error_messages={'required': u'验证码不能为空', 'invalid': u'验证码错误'})
# views.py
def register(request):
title = '注册博客'
pageTitle = '用户注册'
confirmPassword = True
button = '注册'
urlText = '用户登录'
urlName = 'userLogin'
if request.method == 'POST':
u = request.POST.get('username', '')
p = request.POST.get('password', '')
cp = request.POST.get('cp', '')
# 单独为验证码表单构建一个实例
query_dict = QueryDict(mutable=True)
query_dict['captcha_1'] = request.POST.get('captcha_1')
query_dict['captcha_0'] = request.POST.get('captcha_0')
print(query_dict)
form = CaptchaForm(query_dict)
if MyUser.objects.filter(username=u):
tips = '用户已存在'
elif cp != p:
tips = '两次密码输入不一致'
elif not form.is_valid():
print(form.errors)
tips = '验证码错误'
else:
d = {
'username': u, 'password': p,
'is_superuser': 0, 'is_staff': 0
}
user = MyUser.objects.create_user(**d)
user.save()
tips = '注册成功,请登录'
logout(request)
return redirect(reverse('userLogin'))
form = CaptchaForm()
return render(request, 'user.html', locals())
在得出这段正确代码之前,卡住了很久:构建验证码表单实例
print(fom)观察它的结构:
<tr>
<th><label for="id_captcha_1">Captcha:</label></th>
<td>
<ul class="errorlist"><li>验证码不能为空</li></ul>
<input autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false" id="id_captcha_1" name="captcha_1" type="text" />
<input id="id_captcha_0" name="captcha_0" type="hidden" value="416f3d9c1d895377922e8826c48aae7faa0f10cd" />
<img src="/captcha/image/416f3d9c1d895377922e8826c48aae7faa0f10cd/" alt="captcha" class="captcha" />
</td>
</tr>
加载界面时生成三部分内容:验证码输入框captcha_1,隐藏状态的钥匙id_captcha_0,验证码图片
思路是获取captcha_1和id_captcha_0的内容,生成表单,交给前端Ajax判断,返回结果0/1决定form.is_valid()
要命的是我忘了传钥匙id_captcha_0的值,由于CaptchaForm里面只有captcha一个字段,我想当然地只获取了用户的输入值captcha_1,传给表单,可想而知,表单一直报数据缺失的错。
我并不知道数据缺失的真正含义,以为我传的captcha_1因为某种原因,不让往表格里面填,所以缺失。
什么原因呢?对此做了几点猜想
-
csrf码缺失。根据之前的经验,form实例化方式都是形如form = CaptchaForm(request.POST),模仿request.POST的数据格式,就可以用实例化表单。打印request.POST:
<QueryDict: {'csrfmiddlewaretoken': ['JM05sYYqKXl6HuVWIKpTjtAEGULaKqzvoLG1cFp8ZROJ0OYFkpXQZFn1R5eFkJ9u'], 'username': ['user'], 'password': ['1111'], 'cp': ['1111'], 'captcha_1': ['36 '], 'captcha_0': ['817e550cc526abe3f626066a599efc9e8b637df3'], 'Submit': ['注册']}>
只要用captcha_1的值,构造QueryDict格式的数据就行(如何构造的问题让我了解QueryDict对象的特点,官网有介绍),还有一个例子Django: Can I create a QueryDict from a dictionary?
尝试构造:capt = {'captcha': request.POST.get('captcha_1')} query_dict = QueryDict('', mutable=True) query_dict.update(capt) print(query_dict) form = CaptchaForm(query_dict)
打印构造出来的对象:
<QueryDict: {'captcha': ['36']}>
和request.POST唯一显著差别是,没有csrf字段,难道没有csrf字段就不让传?可是我永远无法提前知道csrf字段啊。Django获得form数据,自动随机生成的csrf码,并和form数据一齐上传给服务器。
好像走到绝路了?
会不会根本就不能用自制的QueryDict去实例化Form,我打印出来的request.POST只是它的可读部分,内部还有深层的不可访问的结构导致我永远无法模仿?
我将captcha换成普通的字符串表单:# form.py class CaptchaForm(forms.Form): captcha = forms.CharField() # views.py 没变 # user.html 验证码字段不用{{form.captcha}},改用手写组件形式 <li class="login-item"> <span>验证码:</span> <input type="text" name="captcha_1" class="login_input" > <span id="count-msg" class="error"></span> </li>
哎成功,是可以构造的,是我想复杂了!
继续搜索,看到跟我一样构造QueryDict对象的帖子:
OK,排除csrf的问题和不能构造的问题 -
难道是user.html 中模板语法和组件不能共用?不应该呀,上面打印form结构,也就是一堆标签。保持captcha普通的字符串表单不变,将验证码字段改用{{form.captcha}}
<form name="Login" method="post" action=""> {% csrf_token %} <li class="login-item"> <span>用户名:</span> <input type="text" name="username" class="login_input" > <span id="count-msg" class="error"></span> </li> <li class="login-item"> <span>密 码:</span> <input type="password" name="password" class="login_input"> <span id="password-msg" class="error"></span> </li> {% if confirmPassword %} <li class="login-item"> <span>确认密码:</span> <input type="password" name="cp" class="login_input"> <span id="password-msg" class="error"></span> </li> {% endif %} {# !!!更改这里 !!! #} <li class="login-item"> <span>验证码:</span> {{ form.captcha }} <span id="count-msg" class="error"></span> </li> <div>{{ tips }}</div> <li class="login-sub"> <input type="submit" name="Submit" value="{{ button }}"> <div class="turn-url"> <a style="color: #45B572;" href="{% url urlName %}">>>{{ urlText }}</a> </div> </li> </form>
没有报错,可以混用!
-
那问题就出在captcha字段!!为什么别的类型字段可以传,验证码字段就不行?仔细观察定义的验证码字段表单,发现验证码表单和别的表单的区别是,他有两个字段captcha_1和id_captcha_0,我从来没考虑过id_captcha_0,是不是它也得传?
抱着试一试的心态,写出了此文开头第二部分的改进代码!效果图:功能正确,就是有点丑。。。
结尾附上本次涉及到的代码汇总:
# 总url.py:
urlpatterns = [
path('user/', include('account.urls')),
# 导入Django Simple Captcha 路由,生成验证码图片地址
path('captcha/', include('captcha.urls'))
]
# account应用 url.py
urlpatterns = [
path('register.html', register, name='register'),
path('login.html', userLogin, name='userLogin'),
# 验证用户输入的验证码
path('ajax_val', ajax_val, name='ajax_val')
]
# models.py
class MyUser(AbstractUser):
name = models.CharField('姓名', max_length=50, default='匿名用户')
introduce = models.TextField('简介', default='暂无介绍')
company = models.CharField('公司', max_length=100, default='暂无信息')
# ......
def __str__(self):
return self.name
# form.py
class CaptchaForm(forms.Form):
captcha = CaptchaField(error_messages={'required': u'验证码不能为空', 'invalid': u'验证码错误'})
# views.py
def register(request):
title = '注册博客'
pageTitle = '用户注册'
confirmPassword = True
button = '注册'
urlText = '用户登录'
urlName = 'userLogin'
if request.method == 'POST':
u = request.POST.get('username', '')
p = request.POST.get('password', '')
cp = request.POST.get('cp', '')
# 单独为验证码表单构建一个实例
query_dict = QueryDict(mutable=True)
query_dict['captcha_1'] = request.POST.get('captcha_1')
query_dict['captcha_0'] = request.POST.get('captcha_0')
form = CaptchaForm(query_dict)
if MyUser.objects.filter(username=u):
tips = '用户已存在'
elif cp != p:
tips = '两次密码输入不一致'
elif not form.is_valid():
print(form.errors)
tips = '验证码错误'
else:
d = {
'username': u, 'password': p,
'is_superuser': 0, 'is_staff': 0
}
user = MyUser.objects.create_user(**d)
user.save()
tips = '注册成功,请登录'
logout(request)
return redirect(reverse('userLogin'))
form = CaptchaForm()
return render(request, 'user.html', locals())
def userLogin(request):
title = '登录博客'
pageTitle = '用户登录'
button = '登录'
urlText = '用户注册'
urlName = 'register'
if request.method == 'POST':
u = request.POST.get('username', '')
p = request.POST.get('password', '')
query_dict = QueryDict(mutable=True)
query_dict['captcha_1'] = request.POST.get('captcha_1')
query_dict['captcha_0'] = request.POST.get('captcha_0')
form = CaptchaForm(query_dict)
if MyUser.objects.filter(username=u):
# 原本authenticate包含user.is_active验证
# settings.py:设置authenticate不包含user.is_active验证
# AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.AllowAllUsersModelBackend']
user = authenticate(username=u, password=p)
if user:
if user.is_active:
if form.is_valid():
login(request, user)
return HttpResponse('登录成功')
else:
tips = '验证码错误'
else:
tips = '账号异常,请联系管理员'
else:
tips = '账号密码错误,请重新输入'
else:
tips = '用户不存在,请注册'
else:
if request.user.username:
return HttpResponse('您已登录')
form = CaptchaForm()
return render(request, 'user.html', locals())
# ajax接口,实现动态验证验证码
def ajax_val(request):
if request.is_ajax():
# 用户输入的验证码结果
r = request.GET['response']
# 隐藏域的value值
h = request.GET['hashkey']
cs = CaptchaStore.objects.filter(response=r, hashkey=h)
# 若存在cs,则验证成功,否则验证失败
if cs:
json_data = {'status':1}
else:
json_data = {'status':0}
return JsonResponse(json_data)
else:
json_data = {'status':0}
return JsonResponse(json_data)
# user.html
<!DOCTYPE html>
<html>
<head>
{% load static %}
<title>{{ title }}</title>
<link rel="stylesheet" href="{% static "css/reset.css" %}">
<link rel="stylesheet" href="{% static "css/user.css" %}">
<script src="{% static "js/jquery.min.js" %}"></script>
<script src="{% static "js/user.js" %}"></script>
</head>
<body>
<div class="page">
<div class="loginwarrp">
<div class="logo">徐庶博客系统</div>
<div class="logo">{{ pageTitle }}</div>
<div class="login_form">
<form name="Login" method="post" action="">
{% csrf_token %}
<li class="login-item">
<span>用户名:</span>
<input type="text" name="username" class="login_input" >
<span id="count-msg" class="error"></span>
</li>
<li class="login-item">
<span>密 码:</span>
<input type="password" name="password" class="login_input">
<span id="password-msg" class="error"></span>
</li>
{% if confirmPassword %}
<li class="login-item">
<span>确认密码:</span>
<input type="password" name="cp" class="login_input">
<span id="password-msg" class="error"></span>
</li>
{% endif %}
<li class="login-item">
<span>验证码:</span>
{{ form.captcha }}
<span id="count-msg" class="error"></span>
</li>
<div>{{ tips }}</div>
<li class="login-sub">
<input type="submit" name="Submit" value="{{ button }}">
<div class="turn-url">
<a style="color: #45B572;" href="{% url urlName %}">>>{{ urlText }}</a>
</div>
</li>
</form>
</div>
</div>
</div>
<script type="text/javascript">
window.onload = function() {
var config = {
vx : 4,
vy : 4,
height : 2,
width : 2,
count : 100,
color : "121, 162, 185",
stroke : "100, 200, 180",
dist : 6000,
e_dist : 20000,
max_conn : 10
}
CanvasParticle(config);
}
</script>
<script src="{% static "js/canvas-particle.js" %}"></script>
<script>
$(function(){
$('.captcha').click(function(){
console.log('click');
$.getJSON("/captcha/refresh/",
function(result){
$('.captcha').attr('src', result['image_url']);
$('#id_captcha_0').val(result['key'])
});});
$('#id_captcha_1').blur(function(){
json_data={
'response':$('#id_captcha_1').val(),
'hashkey':$('#id_captcha_0').val()
}
$.getJSON('/user/ajax_val', json_data, function(data){
$('#captcha_status').remove()
if(data['status']){
$('#id_captcha_1').after('<span id="captcha_status">*验证码正确</span>')
}else{
$('#id_captcha_1').after('<span id="captcha_status">*验证码错误</span>')
}
});
});
})
</script>
</body>
</html>