|
|
@@ -20,8 +20,10 @@ class User(models.Model):
|
|
|
通过生成迁移和执行迁移操作,在数据库中创建对应的用户表。
|
|
|
|
|
|
```Shell
|
|
|
-python manage.py makemigrations 应用名
|
|
|
-python manage.py migrate
|
|
|
+(venv)$ python manage.py makemigrations vote
|
|
|
+...
|
|
|
+(venv)$ python manage.py migrate
|
|
|
+...
|
|
|
```
|
|
|
|
|
|
定制一个非常简单的注册模板页面。
|
|
|
@@ -62,13 +64,13 @@ python manage.py migrate
|
|
|
</html>
|
|
|
```
|
|
|
|
|
|
-注意,在上面的表单中,我们使用了模板指令`{% csrf_token %}`为表单添加一个隐藏域(type属性值为hidden的input标签),它的作用是在表单中生成一个随机令牌(token)来防范[跨站请求伪造](<https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0>)(通常简称为CSRF),这也是Django在提交表单时的硬性要求,除非我们专门设置了免除CSRF令牌。下图是一个关于CSRF简单生动的例子,它来自于[维基百科](<https://zh.wikipedia.org/wiki/Wikipedia:%E9%A6%96%E9%A1%B5>)。
|
|
|
+注意,在上面的表单中,我们使用了模板指令`{% csrf_token %}`为表单添加一个隐藏域(type属性值为hidden的input标签),它的作用是在表单中生成一个随机令牌(token)来防范[跨站请求伪造](<https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0>)(通常简称为CSRF),这也是Django在提交表单时的硬性要求,除非我们设置了免除CSRF令牌。下图是一个关于CSRF简单生动的例子,它来自于[维基百科](<https://zh.wikipedia.org/wiki/Wikipedia:%E9%A6%96%E9%A1%B5>)。
|
|
|
|
|
|

|
|
|
|
|
|
用户在提交注册表单时,我们还需要对用户的输入进行验证,例如我们的网站要求用户名必须由字母、数字、下划线构成且长度在4-20个字符之间,密码的长度为8-20个字符,确认密码必须跟密码保持一致。这些验证操作首先可以通过浏览器中的JavaScript代码来完成,但是即便如此,在服务器端仍然要对用户输入再次进行验证来避免将无效的数据库交给数据库,因为用户可能会禁用浏览器的JavaScript功能,也有可能绕过浏览器的输入检查将注册数据提交给服务器,所以服务器端的用户输入检查仍然是必要的。
|
|
|
|
|
|
-我们可以利用Django框架封装的表单功能来对用户输入的有效性进行检查,虽然Django封装的表单还能帮助我们定制出页面上的表单元素,但是这显然是一种灵活性很差的设计,这样的功能在实际开发中基本不考虑,所以表单主要的作用就在于数据验证,具体的做法如下所示。
|
|
|
+我们可以利用Django框架封装的表单功能来对用户输入的有效性进行检查,虽然Django封装的表单还能帮助我们定制出页面上的表单元素,但这显然是一种灵活性很差的设计,这样的功能在实际开发中基本不考虑,所以表单主要的作用就在于数据验证,具体的做法如下所示。
|
|
|
|
|
|
```Python
|
|
|
USERNAME_PATTERN = re.compile(r'\w{4,20}')
|
|
|
@@ -99,7 +101,7 @@ class RegisterForm(forms.ModelForm):
|
|
|
exclude = ('no', 'regdate')
|
|
|
```
|
|
|
|
|
|
-上面,我们定义了一个与User模型绑定的表单(继承自ModelForm),我们排除了用户编号(no)和注册日期(regdate)这两个属性,并添加了一个repassword属性用来接收从用户表单传给服务器的确认密码。我们在定义User模型时已经对用户名的最大长度进行了限制,上面我们又对确认密码的最小和最大长度进行了限制,但是这些都不足以完成我们对用户输入的验证。上面以`clean_`打头的方法就是我们自定义的验证规则。很明显,`clean_username`是对用户名的检查,而`clean_password`是对密码的检查。由于数据库二维表中不应该保存密码的原文,所以对密码做了一个简单的MD5摘要处理,实际开发中这样处理还不太够,因为有被实施反向查表法(利用彩虹表反向查询)破解用户密码的风险。为字符串生成MD5摘要的代码如下所示。
|
|
|
+上面,我们定义了一个与User模型绑定的表单(继承自ModelForm),我们排除了用户编号(no)和注册日期(regdate)这两个属性,并添加了一个repassword属性用来接收从用户表单传给服务器的确认密码。我们在定义User模型时已经对用户名的最大长度进行了限制,上面我们又对确认密码的最小和最大长度进行了限制,但是这些都不足以完成我们对用户输入的验证。上面以`clean_`打头的方法就是我们自定义的验证规则。很明显,`clean_username`是对用户名的检查,而`clean_password`是对密码的检查。由于数据库二维表中不应该保存密码的原文,所以对密码做了一个简单的MD5摘要处理,实际开发中如果只做出这样的处理还不太够,因为即便使用了摘要,仍然有利用彩虹表反向查询破解用户密码的风险,如何做得更好我们会在后续的内容中讲到。为字符串生成MD5摘要的代码如下所示。
|
|
|
|
|
|
```Python
|
|
|
def to_md5_hex(message):
|
|
|
@@ -131,18 +133,13 @@ from django.urls import path
|
|
|
from vote import views
|
|
|
|
|
|
urlpatterns = [
|
|
|
- path('', views.show_subjects),
|
|
|
- path('captcha/', views.get_captcha),
|
|
|
- path('teachers/', views.show_teachers),
|
|
|
- path('prise/', views.praise_or_criticize),
|
|
|
- path('criticize/', views.praise_or_criticize),
|
|
|
- path('login/', views.login, name='login'),
|
|
|
+ # 此处省略上面的代码
|
|
|
path('register/', views.register, name='register'),
|
|
|
- path('admin/', admin.site.urls),
|
|
|
+ # 此处省略下面的代码
|
|
|
]
|
|
|
```
|
|
|
|
|
|
-> 说明:上面的代码中我们把待会要用到的登录和验证码的URL也顺便做了映射。`path`函数还可以通过name参数给URL绑定一个逆向解析的名字,也就是说,如果需要可以从后面给的名字逆向得到对应的URL。
|
|
|
+> 说明:`path`函数可以通过name参数给URL绑定一个逆向解析的名字,也就是说,如果需要可以从后面给的名字逆向解析出对应的URL。
|
|
|
|
|
|
我们再来定制一个非常简单的登录页。
|
|
|
|
|
|
@@ -159,7 +156,6 @@ urlpatterns = [
|
|
|
<hr>
|
|
|
<p class="hint">{{ hint }}</p>
|
|
|
<form action="/login/" method="post">
|
|
|
- <input type="hidden" name="backurl" value="{{ backurl }}">
|
|
|
{% csrf_token %}
|
|
|
<div class="input">
|
|
|
<label for="username">用户名:</label>
|
|
|
@@ -184,9 +180,9 @@ urlpatterns = [
|
|
|
</html>
|
|
|
```
|
|
|
|
|
|
-上面的登录页中,我们要求用户提供验证码,验证码全称是**全自动区分计算机和人类的公开图灵测试**,它是一种用来区分系统的使用者是计算机还是人类的程序。简单的说就是程序出一个只有人类能够回答的问题,由系统使用者来解答,由于计算机理论上无法解答程序提出的问题,所以回答出问题的用户就可以被认为是人类。大多数的网站都使用了不同类型的验证码技术来防范计算机自动注册用户或模拟用户登录(暴力破解用户密码),因为验证码具有一次消费性,而没有通过图灵测试的计算机是不能够注册或登录的。
|
|
|
+上面的登录页中,我们要求用户提供验证码,验证码全称是**全自动区分计算机和人类的公开图灵测试**,它是一种用来区分系统的使用者是计算机还是人类的程序。简单的说就是程序出一个只有人类能够回答的问题,由系统使用者来解答,由于计算机理论上无法解答程序提出的问题,所以回答出问题的用户就可以被认为是人类。大多数的网站都使用了不同类型的验证码技术来防范用程序自动注册用户或模拟用户登录(暴力破解用户密码),因为验证码具有一次消费性,而没有通过图灵测试的程序是不能够完成注册或登录的。
|
|
|
|
|
|
-在Python程序中生成验证码并不算特别复杂,但需要三方库pillow的支持(PIL的分支)。我们可以借鉴现有的方法用Python稍作封装即可。下面的代码已经实现了生成验证码图片并得到图片二进制数据的功能。
|
|
|
+在Python程序中生成验证码并不算特别复杂,但需要三方库Pillow的支持(PIL的分支),因为要对验证码图片进行旋转、扭曲、拉伸以及加入干扰信息来防范那些用OCR(光学文字识别)破解验证码的程序。下面的代码封装了生成验证码图片的功能,大家可以直接用这些代码来生成图片验证码,不要“重复发明轮子”。
|
|
|
|
|
|
```Python
|
|
|
"""
|
|
|
@@ -234,15 +230,16 @@ class Captcha(object):
|
|
|
self._image = None
|
|
|
self._fonts = fonts if fonts else \
|
|
|
[os.path.join(os.path.dirname(__file__), 'fonts', font)
|
|
|
- for font in ['Action.ttf', 'Silom.ttf', 'Verdana.ttf']]
|
|
|
+ for font in ['ArialRB.ttf', 'ArialNI.ttf', 'Georgia.ttf', 'Kongxin.ttf']]
|
|
|
self._color = color if color else random_color(0, 200, random.randint(220, 255))
|
|
|
self._width, self._height = width, height
|
|
|
|
|
|
@classmethod
|
|
|
def instance(cls, width=200, height=75):
|
|
|
- if not hasattr(Captcha, "_instance"):
|
|
|
- cls._instance = cls(width, height)
|
|
|
- return cls._instance
|
|
|
+ prop_name = f'_instance_{width}_{height}'
|
|
|
+ if not hasattr(cls, prop_name):
|
|
|
+ setattr(cls, prop_name, cls(width, height))
|
|
|
+ return getattr(cls, prop_name)
|
|
|
|
|
|
def background(self):
|
|
|
"""绘制背景"""
|
|
|
@@ -266,7 +263,7 @@ class Captcha(object):
|
|
|
for ps in zip(*path)))
|
|
|
Draw(self._image).line(points, fill=color if color else self._color, width=width)
|
|
|
|
|
|
- def noise(self, number=62, level=2, color=None):
|
|
|
+ def noise(self, number=50, level=2, color=None):
|
|
|
"""绘制扰码"""
|
|
|
width, height = self._image.size
|
|
|
dx, dy = width / 10, height / 10
|
|
|
@@ -351,7 +348,9 @@ class Captcha(object):
|
|
|
self.background()
|
|
|
self.text(captcha_text, self._fonts,
|
|
|
drawings=['warp', 'rotate', 'offset'])
|
|
|
- self.curve(), self.noise(), self.smooth()
|
|
|
+ self.curve()
|
|
|
+ self.noise()
|
|
|
+ self.smooth()
|
|
|
image_bytes = BytesIO()
|
|
|
self._image.save(image_bytes, format=fmt)
|
|
|
return image_bytes.getvalue()
|
|
|
@@ -445,4 +444,49 @@ def login(request):
|
|
|
return render(request, 'login.html', {'hint': hint})
|
|
|
```
|
|
|
|
|
|
-需要指出,上面我们设定用户登录成功时直接返回首页,而且在用户登录时并没有验证用户输入的验证码是否正确,这些我们留到下一个单元再为大家讲解。
|
|
|
+映射URL。
|
|
|
+
|
|
|
+```Python
|
|
|
+from django.contrib import admin
|
|
|
+from django.urls import path
|
|
|
+
|
|
|
+from vote import views
|
|
|
+
|
|
|
+urlpatterns = [
|
|
|
+ # 此处省略上面的代码
|
|
|
+ path('login/', views.login, name='login'),
|
|
|
+ # 此处省略下面的代码
|
|
|
+]
|
|
|
+```
|
|
|
+
|
|
|
+需要指出,上面我们设定用户登录成功时直接返回首页,而且在用户登录时并没有验证用户输入的验证码是否正确,这些我们留到下一个单元再为大家讲解。另外,如果要在Django自带的管理后台中进行表单验证,可以在admin.py的模型管理类中指定`form`属性为自定义的表单即可,例如:
|
|
|
+
|
|
|
+```Python
|
|
|
+class UserForm(forms.ModelForm):
|
|
|
+ password = forms.CharField(min_length=8, max_length=20,
|
|
|
+ widget=forms.PasswordInput, label='密码')
|
|
|
+
|
|
|
+ def clean_username(self):
|
|
|
+ username = self.cleaned_data['username']
|
|
|
+ if not USERNAME_PATTERN.fullmatch(username):
|
|
|
+ raise ValidationError('用户名由字母、数字和下划线构成且长度为4-20个字符')
|
|
|
+ return username
|
|
|
+
|
|
|
+ def clean_password(self):
|
|
|
+ password = self.cleaned_data['password']
|
|
|
+ return to_md5_hex(self.cleaned_data['password'])
|
|
|
+
|
|
|
+ class Meta:
|
|
|
+ model = User
|
|
|
+ exclude = ('no', )
|
|
|
+
|
|
|
+
|
|
|
+class UserAdmin(admin.ModelAdmin):
|
|
|
+ list_display = ('no', 'username', 'password', 'email', 'tel')
|
|
|
+ ordering = ('no', )
|
|
|
+ form = UserForm
|
|
|
+ list_per_page = 10
|
|
|
+
|
|
|
+
|
|
|
+admin.site.register(User, UserAdmin)
|
|
|
+```
|