官网:https://www.djangoproject.com/
博客:https://www.liujiangblog.com/
本博客内容参考git:https://gitcode.net/mirrors/jackfrued/Python-100-Days 一些细节问题,大家可以查看git连接。本文主要的改变为把代码升级为django4.1版本。
Django静态文件问题备注:
参考:
Django测试开发-20-settings.py中templates配置,使得APP下的模板以及根目录下的模板均可生效
解决django 多个APP时 static文件的问题
django配置app中的静态文件步骤
Django多APP加载静态文件
django.short包参考:https://docs.djangoproject.com/en/4.1/topics/http/shortcuts/
我们继续来完成上一章节中的项目,实现“用户登录”的功能,并限制只有登录的用户才能投票。
之前我们讲解过如果通过Django的ORM实现从二维表到模型的转换(反向工程),这次我们尝试把模型变成二维表(正向工程)。
class User(models.Model):"""用户"""no = models.AutoField(primary_key=True, verbose_name='编号')username = models.CharField(max_length=20, unique=True, verbose_name='用户名')password = models.CharField(max_length=32, verbose_name='密码')tel = models.CharField(max_length=20, verbose_name='手机号')reg_date = models.DateTimeField(auto_now_add=True, verbose_name='注册时间')last_visit = models.DateTimeField(null=True, verbose_name='最后登录时间')class Meta:db_table = 'tb_user'verbose_name = '用户'verbose_name_plural = '用户'
使用下面的命令生成迁移文件并执行迁移,将User模型直接变成关系型数据库中的二维表tb_user。
python manage.py makemigrations polls
输出为:
E:\vscode\vip3-django\djangoproject>python manage.py makemigrations polls
Migrations for ‘polls’:
polls\migrations\0002_user.py
- Create model User
python manage.py migrate polls
输出为:
E:\vscode\vip3-django\djangoproject>python manage.py migrate polls
Operations to perform:
Apply all migrations: polls
Running migrations:
Applying polls.0002_user… OK
我们在应用下增加一个名为utils.py的模块用来保存需要使用的工具函数。Python标准库中的hashlib模块封装了常用的哈希算法,包括:MD5、SHA1、SHA256等。下面是使用hashlib中的md5类将字符串处理成MD5摘要的函数如下所示。
import hashlibdef gen_md5_digest(content):return hashlib.md5(content.encode()).hexdigest()if __name__=="__main__":print("admin123456-->{}".format(gen_md5_digest("admin123456")))# admin123456-->a66abb5684c45962d887564f08346e8d
MD5消息摘要算法是一种被广泛使用的密码哈希函数(散列函数),可以产生出一个128位(比特)的哈希值(散列值),用于确保信息传输完整一致。在使用哈希值时,通常会将哈希值表示为16进制字符串,因此128位的MD5摘要通常表示为32个十六进制符号。
insert into `tb_user`(`username`, `password`, `tel`, `reg_date`)
values('user1', 'a66abb5684c45962d887564f08346e8d', '13122334455', now()),('user2', 'a66abb5684c45962d887564f08346e8d', '13890006789', now());
说明:上面创建的两个用户user1和user2密码是admin123456。
在polls/views.py添加渲染登录页面的视图函数:
from django.http import HttpRequest, HttpResponsedef login(request: HttpRequest) -> HttpResponse:hint = ''return render(request, '/login.html', {'hint': hint})
在urls.py文件中添加路由
path('login/', polls_views.login),# 登录path('logout/', polls_views.logout),# 注销path('captcha/', polls_views.get_captcha),# 验证码path('register/', polls_views.logout),# 注册
CSRF的作用,参考
本博客内容参考git:https://gitcode.net/mirrors/jackfrued/Python-100-Days
在templates目录下创建login.html文件
用户登录
在服务器端,创建一个session对象,通过这个对象就可以把用户相关的信息都保存起来。我们可以给每个session对象分配一个全局唯一的标识符来识别session对象,我们姑且称之为sessionid,每次客户端发起请求时,只要携带上这个sessionid,就有办法找到与之对应的session对象,从而实现在两次请求之间记住该用户的信息,也就是我们之前说的用户跟踪。
要让客户端记住并在每次请求时带上sessionid又有以下几种做法:
- URL重写。所谓URL重写就是在URL中携带sessionid,例如:http://www.example.com/index.html?sessionid=123456,服务器通过获取sessionid参数的值来取到与之对应的session对象。
- 隐藏域(隐式表单域)。在提交表单的时候,可以通过在表单中设置隐藏域向服务器发送额外的数据。例如:。
- 本地存储。现在的浏览器都支持多种本地存储方案,包括:cookie、localStorage、sessionStorage、IndexedDB等。在这些方案中,cookie是历史最为悠久也是被诟病得最多的一种方案,也是我们接下来首先为大家讲解的一种方案。
在创建Django项目时,默认的配置文件settings.py文件中已经激活了一个名为SessionMiddleware的中间件,因为这个中间件的存在,我们可以直接通过请求对象的session属性来操作会话对象。与此同时,SessionMiddleware中间件还封装了对cookie的操作,在cookie中保存了sessionid。
在默认情况下,Django将session的数据序列化后保存在关系型数据库中,在后面的章节中将session保存到缓存服务中以提升系统的性能。
首先,我们在刚才的polls/utils.py文件中编写生成随机验证码的函数gen_random_code,内容如下所示。
import randomALL_CHARS = '23456789abcdefghjklmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'def gen_random_code(length=4):return ''.join(random.choices(ALL_CHARS, k=length))
创建polls/fonts目录,放置arialbd.ttf
在polls目录下编写生成验证码图片的类Captcha。
"""
图片验证码
"""
import os
import random
from io import BytesIOfrom PIL import Image
from PIL import ImageFilter
from PIL.ImageDraw import Draw
from PIL.ImageFont import truetypeclass Bezier:"""贝塞尔曲线"""def __init__(self):self.tsequence = tuple([t / 20.0 for t in range(21)])self.beziers = {}def make_bezier(self, n):"""绘制贝塞尔曲线"""try:return self.beziers[n]except KeyError:combinations = pascal_row(n - 1)result = []for t in self.tsequence:tpowers = (t ** i for i in range(n))upowers = ((1 - t) ** i for i in range(n - 1, -1, -1))coefs = [c * a * b for c, a, b in zip(combinations,tpowers, upowers)]result.append(coefs)self.beziers[n] = resultreturn resultclass Captcha:"""验证码"""def __init__(self, width, height, fonts=None, color=None):self._image = Noneself._fonts = fonts if fonts else \[os.path.join(os.path.dirname(__file__), 'fonts', font)for font in ['arialbd.ttf']]# arialbd.ttf#for font in ['Arial.ttf', 'Georgia.ttf', 'Action.ttf']]# arialbd.ttfself._color = color if color else random_color(0, 200, random.randint(220, 255))self._width, self._height = width, height@classmethoddef instance(cls, width=200, height=75):"""用于获取Captcha对象的类方法"""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):"""绘制背景"""Draw(self._image).rectangle([(0, 0), self._image.size],fill=random_color(230, 255))def _smooth(self):"""平滑图像"""return self._image.filter(ImageFilter.SMOOTH)def _curve(self, width=4, number=6, color=None):"""绘制曲线"""dx, height = self._image.sizedx /= numberpath = [(dx * i, random.randint(0, height))for i in range(1, number)]bcoefs = Bezier().make_bezier(number - 1)points = []for coefs in bcoefs:points.append(tuple(sum([coef * p for coef, p in zip(coefs, ps)])for ps in zip(*path)))Draw(self._image).line(points, fill=color if color else self._color, width=width)def _noise(self, number=50, level=2, color=None):"""绘制扰码"""width, height = self._image.sizedx, dy = width / 10, height / 10width, height = width - dx, height - dydraw = Draw(self._image)for i in range(number):x = int(random.uniform(dx, width))y = int(random.uniform(dy, height))draw.line(((x, y), (x + level, y)),fill=color if color else self._color, width=level)def _text(self, captcha_text, fonts, font_sizes=None, drawings=None, squeeze_factor=0.75, color=None):"""绘制文本"""color = color if color else self._colorfonts = tuple([truetype(name, size)for name in fontsfor size in font_sizes or (65, 70, 75)])draw = Draw(self._image)char_images = []for c in captcha_text:font = random.choice(fonts)c_width, c_height = draw.textsize(c, font=font)char_image = Image.new('RGB', (c_width, c_height), (0, 0, 0))char_draw = Draw(char_image)char_draw.text((0, 0), c, font=font, fill=color)char_image = char_image.crop(char_image.getbbox())for drawing in drawings:d = getattr(self, drawing)char_image = d(char_image)char_images.append(char_image)width, height = self._image.sizeoffset = int((width - sum(int(i.size[0] * squeeze_factor)for i in char_images[:-1]) -char_images[-1].size[0]) / 2)for char_image in char_images:c_width, c_height = char_image.sizemask = char_image.convert('L').point(lambda i: i * 1.97)self._image.paste(char_image,(offset, int((height - c_height) / 2)),mask)offset += int(c_width * squeeze_factor)@staticmethoddef _warp(image, dx_factor=0.3, dy_factor=0.3):"""图像扭曲"""width, height = image.sizedx = width * dx_factordy = height * dy_factorx1 = int(random.uniform(-dx, dx))y1 = int(random.uniform(-dy, dy))x2 = int(random.uniform(-dx, dx))y2 = int(random.uniform(-dy, dy))warp_image = Image.new('RGB',(width + abs(x1) + abs(x2), height + abs(y1) + abs(y2)))warp_image.paste(image, (abs(x1), abs(y1)))width2, height2 = warp_image.sizereturn warp_image.transform((width, height),Image.QUAD,(x1, y1, -x1, height2 - y2, width2 + x2, height2 + y2, width2 - x2, -y1))@staticmethoddef _offset(image, dx_factor=0.1, dy_factor=0.2):"""图像偏移"""width, height = image.sizedx = int(random.random() * width * dx_factor)dy = int(random.random() * height * dy_factor)offset_image = Image.new('RGB', (width + dx, height + dy))offset_image.paste(image, (dx, dy))return offset_image@staticmethoddef _rotate(image, angle=25):"""图像旋转"""return image.rotate(random.uniform(-angle, angle),Image.BILINEAR, expand=1)def generate(self, captcha_text='', fmt='PNG'):"""生成验证码(文字和图片):param captcha_text: 验证码文字:param fmt: 生成的验证码图片格式:return: 验证码图片的二进制数据"""self._image = Image.new('RGB', (self._width, self._height), (255, 255, 255))self._background()self._text(captcha_text, self._fonts,drawings=['_warp', '_rotate', '_offset'])self._curve()self._noise()self._smooth()image_bytes = BytesIO()self._image.save(image_bytes, format=fmt)return image_bytes.getvalue()def pascal_row(n=0):"""生成毕达哥拉斯三角形(杨辉三角)"""result = [1]x, numerator = 1, nfor denominator in range(1, n // 2 + 1):x *= numeratorx /= denominatorresult.append(x)numerator -= 1if n & 1 == 0:result.extend(reversed(result[:-1]))else:result.extend(reversed(result))return resultdef random_color(start=0, end=255, opacity=255):"""获得随机颜色"""red = random.randint(start, end)green = random.randint(start, end)blue = random.randint(start, end)if opacity is None:return red, green, bluereturn red, green, blue, opacity
说明:上面的代码中用到了三个字体文件,字体文件位于polls/fonts目录下,大家可以自行添加字体文件,但是需要注意字体文件的文件名跟上面代码的第45行保持一致。
接下来,我们先完成提供验证码的视图函数。
from polls.Captcha import Captcha
from polls.utils import gen_random_codedef get_captcha(request: HttpRequest) -> HttpResponse:"""验证码"""captcha_text = gen_random_code()request.session['captcha'] = captcha_textimage_data = Captcha.instance().generate(captcha_text)return HttpResponse(image_data, content_type='image/png')
注意上面代码中的第4行,我们将随机生成的验证码字符串保存到session中,稍后用户登录时,我们要将保存在session中的验证码字符串和用户输入的验证码字符串进行比对,如果用户输入了正确的验证码才能够执行后续的登录流程,代码如下所示。
from polls.Captcha import Captcha
from polls.utils import gen_md5_digest, gen_random_codefrom polls.models import Userdef login(request: HttpRequest) -> HttpResponse:hint = ''if request.method == 'POST':username = request.POST.get('username')password = request.POST.get('password')if username and password:password = gen_md5_digest(password)user = User.objects.filter(username=username, password=password).first()if user:request.session['userid'] = user.norequest.session['username'] = user.usernamereturn redirect('/')else:hint = '用户名或密码错误'else:hint = '请输入有效的用户名和密码'return render(request, 'login.html', {'hint': hint})
说明:上面的代码没有对用户名和密码没有进行验证,实际项目中建议使用正则表达式验证用户输入信息,否则有可能将无效的数据交给数据库进行处理或者造成其他安全方面的隐患。
上面的代码中,我们设定了登录成功后会在session中保存用户的编号(userid)和用户名(username),页面会重定向到首页。
如果用户没有登录,页面会显示登录和注册的超链接;而用户登录成功后,页面上会显示用户名和注销的链接,注销链接对应的视图函数如下所示,URL的映射与之前讲过的类似,不再赘述。
def logout(request):"""注销"""request.session.flush()return redirect('/')
上面的代码通过session对象flush方法来销毁session,一方面清除了服务器上session对象保存的用户数据,一方面将保存在浏览器cookie中的sessionid删除掉,稍后我们会对如何读写cookie的操作加以说明。
我们可以通过项目使用的数据库中名为django_session 的表来找到所有的session,该表的结构如下所示:
其中,第1列就是浏览器cookie中保存的sessionid;第2列是经过BASE64编码后的session中的数据。
接下来,我们就可以限制只有登录用户才能为老师投票,修改后的praise_or_criticize函数如下所示,我们通过从request.session中获取userid来判定用户是否登录。
def praise_or_criticize(request: HttpRequest) -> HttpResponse:if request.session.get('userid'):try:tno = int(request.GET.get('tno'))teacher = Teacher.objects.get(no=tno)if request.path.startswith('/praise/'):teacher.good_count += 1count = teacher.good_countelse:teacher.bad_count += 1count = teacher.bad_countteacher.save()data = {'code': 20000, 'mesg': '投票成功', 'count': count}except (ValueError, Teacher.DoesNotExist):data = {'code': 20001, 'mesg': '投票失败'}else:data = {'code': 20002, 'mesg': '请先登录'}return JsonResponse(data)
当然,在修改了视图函数后,teachers.html也需要进行调整,用户如果没有登录,就将用户引导至登录页,登录成功再返回到投票页,此处不再赘述。
window.location.href='http://127.0.0.1:8000/login/';
def register(request: HttpRequest) -> HttpResponse:from datetime import datetimereg_date = datetime.now()hint = ''if request.method == 'POST':username = request.POST.get('username')password = request.POST.get('password')password = gen_md5_digest(password)tel = request.POST.get('tel')user = User.objects.filter(username=username).first()if user:hint = '用户名已存在'return render(request, 'register.html', {'hint': hint})print(reg_date,"<--------reg_date")user = User(username=username,password=password,tel=tel,reg_date=reg_date,)user.save()user = User.objects.filter(username=username).first()if user:hint="用户 {} 注册成功".format(username)return redirect('/')else:hint = '请重新注册'return render(request, 'register.html', {'hint': hint})return render(request, 'register.html', {'hint': hint})
path('register/', polls_views.register),# 注册 本部分新增 前面如果已经添加九不要重复添加
templates/register.html
用户注册
本文主要是Django系列博客。本文是Django静态资源与Ajax请求示例。
1.创建静态资源目录
2.配置settings.py文件
3.修改urls.py文件
4.修改views.py文件
5.修改teachers.html文件