Django实践-05Cookie和Session
官网: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.short包参考:https://docs.djangoproject.com/en/4.1/topics/http/shortcuts/
Django实践-05Cookie和Session
我们继续来完成上一章节中的项目,实现“用户登录”的功能,并限制只有登录的用户才能投票。
用户登录的准备工作
1. 创建用户模型。
之前我们讲解过如果通过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 = '用户'
2. 正向工程生成数据库表
使用下面的命令生成迁移文件并执行迁移,将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
3.写utils.py文件,密码转md5
我们在应用下增加一个名为utils.py的模块用来保存需要使用的工具函数。Python标准库中的hashlib模块封装了常用的哈希算法,包括:MD5、SHA1、SHA256等。下面是使用hashlib中的md5类将字符串处理成MD5摘要的函数如下所示。
import hashlib def gen_md5_digest(content): return hashlib.md5(content.encode()).hexdigest() if __name__=="__main__": print("admin123456-->{}".format(gen_md5_digest("admin123456"))) # admin123456-->a66abb5684c45962d887564f08346e8d
4.给数据表tb_user中插入测试数据
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。
5.编写用户登录的视图函数和模板页。
在polls/views.py添加渲染登录页面的视图函数:
from django.http import HttpRequest, HttpResponse def login(request: HttpRequest) -> HttpResponse: hint = '' return render(request, '/login.html', {'hint': hint})
6.编写urls.py。
在urls.py文件中添加路由
path('login/', polls_views.login),# 登录 path('logout/', polls_views.logout),# 注销 path('captcha/', polls_views.get_captcha),# 验证码 path('register/', polls_views.register),# 注册
6.增加login.html模板页:
CSRF的作用,参考
本博客内容参考git:https://gitcode.net/mirrors/jackfrued/Python-100-Days
在templates目录下创建login.html文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>用户登录</title> <style> #container { width: 520px; margin: 10px auto; } .input { margin: 20px 0; width: 460px; height: 40px; } .input>label { display: inline-block; width: 140px; text-align: right; } .input>img { width: 150px; vertical-align: middle; } input[name=captcha] { vertical-align: middle; } form+div { margin-top: 20px; } form+div>a { text-decoration: none; color: darkcyan; font-size: 1.2em; } .button { width: 500px; text-align: center; margin-top: 20px; } .hint { color: red; font-size: 12px; } </style> </head> <body> <div id="container"> <h1>用户登录</h1> <hr> <p class="hint">{{ hint }}</p> <form action="/login/" method="post"> {% csrf_token %} <fieldset> <legend>用户信息</legend> <div class="input"> <label>用户名:</label> <input type="text" name="username"> </div> <div class="input"> <label>密码:</label> <input type="password" name="password"> </div> <div class="input"> <label>验证码:</label> <input type="text" name="captcha"> <img id="code" src="/captcha/" alt="" width="150" height="40"> </div> </fieldset> <div class="button"> <input type="submit" value="登录"> <input type="reset" value="重置"> </div> </form> <div> <a href="/">返回首页</a> <a href="/register/">注册新用户</a> </div> </div> </body> </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框架对session的支持
在创建Django项目时,默认的配置文件settings.py文件中已经激活了一个名为SessionMiddleware的中间件,因为这个中间件的存在,我们可以直接通过请求对象的session属性来操作会话对象。与此同时,SessionMiddleware中间件还封装了对cookie的操作,在cookie中保存了sessionid。
在默认情况下,Django将session的数据序列化后保存在关系型数据库中,在后面的章节中将session保存到缓存服务中以提升系统的性能。
实现用户登录验证
生成验证码随机数
首先,我们在刚才的polls/utils.py文件中编写生成随机验证码的函数gen_random_code,内容如下所示。
import random ALL_CHARS = '23456789abcdefghjklmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ' def gen_random_code(length=4): return ''.join(random.choices(ALL_CHARS, k=length))
添加字体与验证码图片类Captcha
创建polls/fonts目录,放置arialbd.ttf
在polls目录下编写生成验证码图片的类Captcha。
""" 图片验证码 """ import os import random from io import BytesIO from PIL import Image from PIL import ImageFilter from PIL.ImageDraw import Draw from PIL.ImageFont import truetype class 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] = result return result class Captcha: """验证码""" def __init__(self, width, height, fonts=None, color=None): self._image = None self._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.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): """用于获取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.size dx /= number path = [(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.size dx, dy = width / 10, height / 10 width, height = width - dx, height - dy draw = 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._color fonts = tuple([truetype(name, size) for name in fonts for 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.size offset = 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.size mask = 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) @staticmethod def _warp(image, dx_factor=0.3, dy_factor=0.3): """图像扭曲""" width, height = image.size dx = width * dx_factor dy = height * dy_factor x1 = 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.size return warp_image.transform( (width, height), Image.QUAD, (x1, y1, -x1, height2 - y2, width2 + x2, height2 + y2, width2 - x2, -y1)) @staticmethod def _offset(image, dx_factor=0.1, dy_factor=0.2): """图像偏移""" width, height = image.size dx = 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 @staticmethod def _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, n for denominator in range(1, n // 2 + 1): x *= numerator x /= denominator result.append(x) numerator -= 1 if n & 1 == 0: result.extend(reversed(result[:-1])) else: result.extend(reversed(result)) return result def 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, blue return red, green, blue, opacity
说明:上面的代码中用到了三个字体文件,字体文件位于polls/fonts目录下,大家可以自行添加字体文件,但是需要注意字体文件的文件名跟上面代码的第45行保持一致。
修改polls/views.py文件处理验证码请求与修改登录请求
接下来,我们先完成提供验证码的视图函数。
from polls.Captcha import Captcha from polls.utils import gen_random_code def get_captcha(request: HttpRequest) -> HttpResponse: """验证码""" captcha_text = gen_random_code() request.session['captcha'] = captcha_text image_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_code from polls.models import User def 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.no request.session['username'] = user.username return redirect('/') else: hint = '用户名或密码错误' else: hint = '请输入有效的用户名和密码' return render(request, 'login.html', {'hint': hint})
说明:上面的代码没有对用户名和密码没有进行验证,实际项目中建议使用正则表达式验证用户输入信息,否则有可能将无效的数据交给数据库进行处理或者造成其他安全方面的隐患。
上面的代码中,我们设定了登录成功后会在session中保存用户的编号(userid)和用户名(username),页面会重定向到首页。
修改polls/views.py文件,logout函数
如果用户没有登录,页面会显示登录和注册的超链接;而用户登录成功后,页面上会显示用户名和注销的链接,注销链接对应的视图函数如下所示,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中的数据。
修改polls/views.py文件praise_or_criticize函数,限制只有登录的用户才能投票
接下来,我们就可以限制只有登录用户才能为老师投票,修改后的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 += 1 count = teacher.good_count else: teacher.bad_count += 1 count = teacher.bad_count teacher.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,如果没有登录,跳转到登录页
当然,在修改了视图函数后,teachers.html也需要进行调整,用户如果没有登录,就将用户引导至登录页,登录成功再返回到投票页,此处不再赘述。
window.location.href='http://127.0.0.1:8000/login/';
实现用户注册
修改polls/views.py添加register函数
def register(request: HttpRequest) -> HttpResponse: from datetime import datetime reg_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})
修改urls.py文件,添加注册路由
path('register/', polls_views.register),# 注册 本部分新增 前面如果已经添加九不要重复添加
创建templates/register.html
templates/register.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>用户注册</title> <style> #container { width: 520px; margin: 10px auto; } .input { margin: 20px 0; width: 460px; height: 40px; } .input>label { display: inline-block; width: 140px; text-align: right; } .input>img { width: 150px; vertical-align: middle; } input[name=captcha] { vertical-align: middle; } form+div { margin-top: 20px; } form+div>a { text-decoration: none; color: darkcyan; font-size: 1.2em; } .button { width: 500px; text-align: center; margin-top: 20px; } .hint { color: red; font-size: 12px; } </style> </head> <body> <div id="container"> <h1>用户注册</h1> <hr> <p class="hint">{{ hint }}</p> <form action="/register/" method="post"> {% csrf_token %} <fieldset> <legend>用户信息</legend> <div class="input"> <label>用户名:</label> <input type="text" name="username"> </div> <div class="input"> <label>密码:</label> <input type="password" name="password"> </div> <div class="input"> <label>手机:</label> <input type="text" name="tel"> </div> <div class="input"> <label>验证码:</label> <input type="text" name="captcha"> <img id="code" src="/captcha/" alt="" width="150" height="40"> </div> </fieldset> <div class="button"> <input type="submit" value="注册"> <input type="reset" value="重置"> </div> </form> <div> <a href="/">返回首页</a> <a href="/login/">登录</a> </div> </div> </body> </html>
总结
本文主要是Django系列博客。本文是Django静态资源与Ajax请求示例。
1.创建静态资源目录
2.配置settings.py文件
3.修改urls.py文件
4.修改views.py文件
5.修改teachers.html文件