Django登录/注册用户为普通表单加入验证码功能

问题来源于对一段使用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因为某种原因,不让往表格里面填,所以缺失。

什么原因呢?对此做了几点猜想

  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的问题和不能构造的问题

  2. 难道是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>
    

    没有报错,可以混用!

  3. 那问题就出在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>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值