Django 实现有点安全的网站登录认证机制——注册页面的用户枚举

Django 开发 专栏收录该内容
5 篇文章 0 订阅

前面讲了用户枚举这个漏洞以及一些常规的防护方法,现在再补充讲另一种用户枚举漏洞的存在情况——在注册页面存在的用户枚举

特殊的用户枚举

先看我们之前实现的认证机制中处理注册请求的视图函数,明显存在用户枚举漏洞
zcd
之前我们说的一种防护手段是:把服务器回显的错误提示信息修改成一致的。那这里是否也可以应用这种防护?

显然不能。首先这会影响到用户注册功能的服务效果,而我们所需要的安全,是在不妨碍服务正常运行这个前提下的最大安全

其次也是主要的,即使我们把错误提示信息都改成一样,也么得用。这里只有两种异常情况,而第二种异常(两次密码输入不一致)的发生与否是用户能够控制的。所以当有异常发生时,即便服务器反馈的错误提示信息一致,攻击者同样能够确定发生的是哪一种异常

限制用户可控制

对于上面注册页面的用户枚举,一个较安全的防护方法就是,禁止用户自定义用户名

如果在注册时,用户只能输入确认口令和一些其他的用户信息,而由服务器生成一个用户名来完成注册,那么攻击者就无法控制用户名的内容,自然不能进行枚举,显然这会是一个很安全的防护方法

实现如下

# views.py

from django.contrib.auth.hashers import make_password
from django.shortcuts import render
from Blog.models import User

def register(request):
    if request.session.get('is_login', None):
        request.session.flush()
    if request.method == 'POST':
        password = request.POST.get('password', '').strip()
        re_password = request.POST.get('re_password', '').strip()
        if password == re_password:
        	while True:
        		username = username_generater()
            	try:
                	User.objects.get(username=username)
            	except User.DoesNotExist:
                	encrypted_password = make_password(password, None, 'pbkdf2_sha256')
                	new_user = User(username=username, password=encrypted_password)
                	new_user.save()
                	return render(request, 'register.html', {'mess': '用户注册成功', 'username': username})
                else:
                	pass
        else:
            return render(request, 'register.html', {'mess': '两次密码输入不一致'})
    return render(request, 'register.html')

上面的 username_generater( ) 是用户名生成器,具体可实现的方案有很多,这里不复现

一个较好的生成方案应该有几点:生成的用户名不要呈现某种分布或变化规律,否则容易被攻击者逆推猜测出其他的用户名。同时生成的用户名不能出现碰撞,最好还能兼顾一下效率

注册成功后,服务器存储用户数据到数据库,同时把成功提示和用户名返回至客户端

注册页面的模板也需要进行相应修改

<!-- register.html -->

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>用户注册</title>
</head>
<body>
	<div>
		<h2>请输入待注册的用户信息</h2>
		<form action="/register/" method="post">
			{% csrf_token %}
			<p>密码:
				<input type="password" name="password">
			</p>
			<p>再次输入:
				<input type="password" name="re_password">
			</p>
			<br/>
			<input type="submit" value="注册用户">
		</form>
	</div>
	<a href="/login/">返回登录</a>
	<div>{{ mess }}</div>
	<div>
		{% if username %}
			<p>请记住你的用户名:{{ username }}</p>
		{% endif %}
	</div>
</body>
</html>

(这样看着,好像有点丑)


凡事有利必有弊。禁止用户名自定义可以完全杜绝用户枚举,但也同样存在一些缺点

比如,用户可能对于服务器分配的用户名不熟悉,容易忘记。又或者,服务器生成的用户名有可能是用户不喜欢的数字或字符。这样用户体验感显然比不上自定义用户名服务来得好

所以,对于一些安全要求不是特别高的网站,我们可以保留自定义用户名的功能,选用其他的方法来进行防护

限制漏洞可利用

如果保留自定义用户名的功能,攻击者就可以通过查询用户是否已被注册来确定该用户的存在情况,漏洞存在且可利用

不能修复漏洞,我们可以选择增加漏洞利用的成本。如果漏洞利用的成本超出了攻击者的预期收益,那么在一定程度上可以说是安全的

前面说过用户枚举的两种攻击情况,明显第二种使用自动化脚本工具进行大量尝试攻击更具有威胁性,我们可以通过限制自动化工具的使用来增加攻击的难度,提高漏洞利用成本

可以增加一个鉴别自动化工具的机制——全自动区分计算机和人类的图灵测试 ,即 Completely Automated Public Turing Test to Tell Computers and Humans Apart,简称 Captcha,也就是我们说的验证码

这里直接使用模块 django-simple-captcha,使用 pip 命令安装

pip instsall django-simple-captcha

setting.py 文件中对 captcha 进行配置

# setting.py

# 添加 captcha 到应用注册
INSTALLED_APPS += [
    'captcha',
]

# django_simple_captcha 验证码配置 

# 输出格式
CAPTCHA_OUTPUT_FORMAT = u'%(text_field)s %(hidden_field)s %(image)s'

# 图片大小
CAPTCHA_IMAGE_SIZE = (110, 25)

# 图片背景颜色
CAPTCHA_BACKGROUND_COLOR = '#ffffff'

# 设置图片中的字符类型
CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.random_char_challenge'  # 随机英文字母

# 字符个数
CAPTCHA_LENGTH = 4

# 验证码有效时间 (minutes)
CAPTCHA_TIMEOUT = 1

# 噪点样式
CAPTCHA_NOISE_FUNCTIONS = (
    'captcha.helpers.noise_null',  # 没有样式
    # 'captcha.helpers.noise_arcs',  # 线
    # 'captcha.helpers.noise_dots',  # 点
)

迁移数据,在数据库生成 captcha_captchastore

python manage.py migrate

数据
修改视图函数

# views.py

from django.contrib.auth.hashers import make_password
from django.shortcuts import render
from Blog.models import User
from captcha.models import CaptchaStore
from captcha.helpers import captcha_image_url
from django.http import JsonResponse

def captcha_generater(request):
	hashkey = CaptchaStore.generate_key()  # 生成验证码
	request.session['hashkey'] = hashkey  # 存入 Session
	image_url = captcha_image_url(hashkey)
	return image_url

def register(request):
    if request.session.get('is_login', None):
        request.session.flush()
    if request.method == 'POST':
        username = request.POST.get('username', '').strip()
        password = request.POST.get('password', '').strip()
        re_password = request.POST.get('re_password', '').strip()
        captcha = request.POST.get('captcha', '').strip()
        if not captcha:
        	return render(request, 'register.html', {'mess': '请输入验证码', 'image_url': captcha_generater(request)})
        hashkey = request.session.get('hashkey')  # 在这里,显然是存在的
        try:
	        mycaptcha = CaptchaStore.objects.get(hashkey=hashkey)
	    except CaptchaStore.DoesNotExist:
	    	return render(request, 'register.html', {'mess': '服务器故障,请重新提交', 'image_url': captcha_generater(request)})
        if captcha.lower() != mycaptcha.response:
        	return render(request, 'register.html', {'mess': '验证码输入错误', 'image_url': captcha_generater(request)})
        if password == re_password:
            try:
                User.objects.get(username=username)
            except User.DoesNotExist:
                encrypted_password = make_password(password, None, 'pbkdf2_sha256')
                new_user = User(username=username, password=encrypted_password)
                new_user.save()
                return render(request, 'register.html', {'mess': '用户注册成功', 'image_url': captcha_generater(request)})
            else:
                return render(request, 'register.html', {'mess': '该用户已被注册', 'image_url': captcha_generater(request)})
        else:
            return render(request, 'register.html', {'mess': '两次密码输入不一致', 'image_url': captcha_generater(request)})
    return render(request, 'register.html', {'image_url': captcha_generater(request)})
e_url, 'mess': mess})

# 动态刷新验证码
def refresh_captcha(request):
    return JsonResponse({'status': 1, 'image_url': captcha_generater(request)})

注册页面模板

<!-- register.html -->

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>用户注册</title>
	{% load static %}
	<script src="{% static 'js/jquery-3.4.1.js' %}"></script>
</head>
<body>
	<div class="reg">
		<h2>请输入待注册的用户信息</h2>
		<form action="/register/" method="post">
			{% csrf_token %}
			<p>账号:
		  		<input type="text" name="username">
			</p>
			<p>密码:
				<input type="password" name="password">
			</p>
			<p>再次输入:
				<input type="password" name="repassword">
			</p>
			<p>验证码:
				<input type="text" name="captcha">
				<img src="{{ image_url }}" id="captcha_img">
				<!--a id="captcha_refresh">换一张</a-->
				<span>(点击图片刷新)</span>
			</p>
			<br/>
			<input type="submit" value="注册用户" class="button">
		</form>
	</div>
	<a href="/login/" class="tologin">返回登录</a>
	<div class="mess">{{ mess }}</div>
	<script>
		$(document).ready(function() {
			$('#captcha_img').click(function() {
				$.getJSON("/refresh_captcha/", function(result) {
					$('#captcha_img').attr('src', result['image_url']);
				});
			});
		});
	</script>
</body>
</html>

最后,修改 urls.py 文件

# urls.py

from django.contrib import admin
from django.urls import path, include
from . import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', views.login),
    path('logout/', views.logout),
    path('register/', views.register),
    path('index/', views.index),
    path('captcha/', include('captcha.urls')),  # 添加 captcha 路由
    path('refresh_captcha/', views.refresh_captcha),  # 异步刷新验证码
]

增加了验证码之后的注册界面
在这里插入图片描述
图中的验证码太过简单,很容易被工具识别出来,我们需要更加复杂的验证码

修改 setting.py 文件,给验证码增加噪点

# setting.py

# 噪点样式
CAPTCHA_NOISE_FUNCTIONS = (
    #'captcha.helpers.noise_null',   没有样式
    'captcha.helpers.noise_arcs',  # 线
    'captcha.helpers.noise_dots',  # 点
)

最终页面
在这里插入图片描述

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

打赏
文章很值,打赏犒劳作者一下
相关推荐
©️2020 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页

打赏

Bcdfxg

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值