11.Djiango-用户认证

用户认证

Cookie和Session

cookie是http请求header中的一个属性,是浏览器持久化存储数据的一种机制,因为网页无法直接访问浏览器所在主机的文件系统,所以网页要通过向cookie中存储键值对(Cookie中的键值对是程序员自定义的)的方式来把数据存储到浏览器所在主机的硬盘中。

cookie中保存的是键值对,最终还是要把这个键值对发送回服务器,因为服务器要使用cookie中的键值对来完成一些业务逻辑。
比如客户端请求中的cookie包含个人id,服务器就会通过这个个人id在数据库中查询出来这个个人id所关联的信息。
然而每个人都有一个个人id和个人id所关联的信息。这些数据是存储在数据库中的,服务器代码执行过程中这些数据会先临时保存到某个内存结构(相当于一个哈希表)中,后续需要对这些信息进行修改时,就修改内存结构(哈希表)中的内容,然后再将修改后的信息写入数据库。
这里服务器所涉及到的内存结构(哈希表)叫“会话”(session)。

  • cookie是客户端存储数据的机制。
  • session是服务器存储数据的机制(不是持久化存储)。

在Cookie键值对中存储的用户身份标识经常会理解成sessionId(会话Id)。

服务器会存储很多的session信息(会话信息)和sessionId(会话Id),每个用户都有一个session信息以及sessionId。服务器会通过类似于哈希表这样的数据结构来存储session信息和sessionId,sessionId就是key,session信息就是value,session信息里可以存储用户自身的各种信息(session信息里也是程序员自定义的键值对)。

login.html

登录页面

{% extends 'layout.html' %}

{% block css %}
<style>
    .account {
        width: 400px;
        border: 1px solid #dddddd;
        border-radius: 5px;
        box-shadow: 5px 5px 20px #aaa;

        margin-left: auto;
        margin-right: auto;
        margin-top: 100px;
        padding: 20px 40px;
    }

    .account h2 {
        margin-top: 10px;
        text-align: center;
    }
</style>
{% endblock %}

{% block content %}
<div class="account">
    <h2>用户登录</h2>
    <div class="panel-body">
        <form method="post">
            {% csrf_token %}
            <div class="form-group">
                <label>用户名</label>
                {{ form.username }}
                <span style="color: red;">{{ form.errors.username.0 }}</span>

            </div>
            <div class="form-group">
                <label>密码</label>
                {{ form.password }}
                <span style="color: red;">{{ form.errors.password.0 }}</span>
            </div>
            <button type="submit" class="btn btn-primary center-block" style="width: 80px;">登录</button>
        </form>
    </div>
</div>
{% endblock %}

登录视图函数 account.py 登录、COOKIES、SESSION实现

# urls.py  path('login/', account.login),
# 前端\django\myweb\blog\views\account.py
from django.shortcuts import render, redirect
from blog.models import Admin
from blog.utils.pagination import Pagination
from blog.utils.form import LoginForm
from blog.utils.form import LoginModelForm
from django.http import HttpResponse,HttpResponseRedirect

# 用户登录
def login(req):

    # form
    if req.method == "GET":
        form = LoginForm()
        context = {
            "form": form,
        }

        return render(req, "login.html", context)

    form = LoginForm(data=req.POST)
    if form.is_valid():
        # 数据提交验证成功,获取到输入的用户名和密码
        # print(form.cleaned_data)  # {'username': 'tang', 'password': 'Tang@1234'}
        # form 里进行 clean_password 数据处理后返回密码存储的md5值去数据库进行比对
        # print(form.cleaned_data) # {'username': 'tang', 'password': '469bb5f078060211a9e6a2a5459bda16'}
        # 数据库数据查询比对
        # admin_object = Admin.objects.filter(username=form.cleaned_data['username'],password=form.cleaned_data['password']).first()
        # 如果数据字段与数据库表字段名一致
        # admin_object = Admin.objects.filter(**form.cleaned_data).first()
        user_object = Admin.objects.filter(username=form.cleaned_data['username']).first()
        admin_object = Admin.objects.filter(username=form.cleaned_data['username'],password=form.cleaned_data['password']).first()

        # if not Admin_object:
        #     form.add_error("username", "用户名或者密码错误!")
        #     form.add_error("password", "用户名或者密码错误!")
        #     context = {
        #         "form": form,
        #         }
        #     return render(req, "login.html", context)

        if not user_object:
            form.add_error("username", "用户名错误!")
            context = {
                "form": form,
                }
            return render(req, "login.html", context)
        elif not admin_object:
            form.add_error("password", "密码错误!")
            context = {
                "form": form,
                }
            return render(req, "login.html", context)
        else:
            # 用户名与密码正确
            # 网站生成随机字符串;写到用户浏览器的cookie中;再写入到session中
            """ COOKIES
            # 例子1:不使用模板
            response = HttpResponse("hello world")
            response.set_cookie(key,value,max_age)
            return response

            # 例子2: 使用模板
            response = render(request,'xxx.html', context)
            response.set_cookie(key,value,max_age)
            return response

            # 例子3: 重定向
            response = HttpResponseRedirect('/login/')
            response.set_cookie(key,value,max_age)
            return response
            # 删除
            response.delete_cookie('username')

            # 获取 COOKIES值
            # 方法一
            request.COOKIES['username']
            # 方法二
            request.COOKIES.get('username','')
            # 判断是否存在
            request.COOKIES.has_key('cookie_name')
            """
            """ session
            # 设置session的值
            request.session['key'] = value
            request.session.set_expiry(time): 设置过期时间,0表示浏览器关闭则失效

            # 获取session的值
            request.session.get('key',None)

            # 删除session的值, 如果key不存在会报错
            del request.session['key']

            # 判断一个key是否在session里
            'fav_color' in request.session

            # 获取所有session的key和value
            request.session.keys()
            request.session.values()
            request.session.items()
            # 清空
            request.session.flush()
            """

            response = HttpResponseRedirect('/admin_list/')
            # 将username写入浏览器cookie,有效时间为360秒
            response.set_cookie('id', admin_object.id, 3600)
            response.set_cookie('name', admin_object.username, 3600)
            req.session["info"] = {'id': admin_object.id, 'name': admin_object.username}
            req.session.set_expiry(1209600) # 默认 1209600 14天
            return response

    context = {
        "form": form,
        }

    return render(req, "login.html", context)

中间件实现登录验证

中间件会在视图函数下的每个方法执行前调用,不用在每个方法下面进行判断,不然函数太多,过于繁琐。

def xxxxxx(req):
    # 检查用户是否已登录,已登录可继续访问,未登录,跳转登录页面
    # 用户发来请求,获取cookie随机字符串
    # req.session["info"]
    info = req.session.get("info")
    if not info:
        return redirect("/login/")
# 中间件示例
# 前端\django\myweb\myweb\settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'blog.middleware.auth.M1',
    'blog.middleware.auth.M2',
]

# 前端\django\myweb\blog\middleware\auth.py
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse, redirect

# 登录验证中间件
class AuthMiddleware(MiddlewareMixin):
    """中间件AuthMiddleware"""

    def process_request(self, request):

        # 排除不需要中间件验证的页面
        if request.path_info == "/login/":
            return

        # 读取当彰访问的用户session的信息,如果能读到,说明已登陆过,就可以向后继续走
        info_dict = request.session.get('info')
        if info_dict:
            print(info_dict)
            return

        # 如果没有登录过,重定向到登录页面
        return redirect("/login/")

# 中间件测试示例
class M1(MiddlewareMixin):
    """中间件M1"""

    def process_request(self, request):

        # 如果方法中没有返回值(返回None)
        # 如果有返回值 HttpResponse、render、redirect,则不再继续向后执行,把返回值返回给用户
        print("M1 进来了")
        return HttpResponse("无权访问")

    def process_response(self, request, response):
        print("M1 出去了")
        return response

class M2(MiddlewareMixin):
    """中间件M2"""

    def process_request(self, request):
        print("M2 进来了")

    def process_response(self, request, response):
        print("M2 出去了")
        return response

""" 访问任何页面都会通过中间件
M1 进来了
M2 进来了
M2 出去了
M1 出去了
[18/Apr/2024 11:18:30] "GET /login/ HTTP/1.1" 200 3666
"""

用户登录注销

# 前端\django\myweb\blog\views\account.py
def  loginout(request):
    """ 注销 """

    # 清除当前session
    request.session.clear()

    return redirect("/login/")
layout.hmtl 可获取session数据  {{ request.session.info.name }}

图片验证码获取函数

pip3 install pillow

# 验证码获取函数 前端\django\myweb\blog\utils\check_code.py

import random, string
from PIL import Image, ImageDraw, ImageFont, ImageFilter

def check_code(width=120, height=30, char_length=5, font_file='D:\\git-python\\前端\\django\\myweb\\blog\\utils\\ttf\\kumo.ttf', font_size=28):
    code = []
    img = Image.new(mode='RGB', size=(width, height), color=(255, 255, 255))
    draw = ImageDraw.Draw(img, mode='RGB')

    def rndChar():
        """
        生成随机字母   
        :return:
        """
        # return chr(random.randint(65, 90))    # 随机 ascii 码大写字母范围
        return random.choice(string.ascii_letters)   # 随机大小写字母
        # return str(random.randint(0, 9))    # 随机0-9的数字字符串

    def rndColor():
        """
        生成随机颜色
        :return:
        """
        return (random.randint(0, 255), random.randint(10, 255), random.randint(64, 255))

    # 写文字
    font = ImageFont.truetype(font_file, font_size)
    for i in range(char_length):
        char = rndChar()
        code.append(char)
        h = random.randint(0, 4)
        draw.text([i * width / char_length, h], char, font=font, fill=rndColor())

    # 写干扰点
    for i in range(18):
        draw.point([random.randint(0, width), random.randint(0, height)], fill=rndColor())

    # 写干扰圆圈
    for i in range(18):
        draw.point([random.randint(0, width), random.randint(0, height)], fill=rndColor())
        x = random.randint(0, width)
        y = random.randint(0, height)
        draw.arc((x, y, x + 4, y + 4), 0, 90, fill=rndColor())

    # 画干扰线
    for i in range(5):
        x1 = random.randint(0, width)
        y1 = random.randint(0, height)
        x2 = random.randint(0, width)
        y2 = random.randint(0, height)

        draw.line((x1, y1, x2, y2), fill=rndColor())

    img = img.filter(ImageFilter.EDGE_ENHANCE_MORE)
    return img,''.join(code)

if __name__ == '__main__':
    # 1. 直接打开
    img,code = check_code()
    img.show()

    # 2. 写入文件
    # img,code = check_code()
    # with open('code.png','wb') as f:
    #     img.save(f,format='png')

    # 3. 写入内存(Python3)
    # from io import BytesIO
    # stream = BytesIO()
    # img.save(stream, 'png')
    # stream.getvalue()

    # 4. 写入内存(Python2)
    # import StringIO
    # stream = StringIO.StringIO()
    # img.save(stream, 'png')
    # stream.getvalue()

    pass

验证中间件加正则匹配验证码刷新

# 前端\django\myweb\blog\middleware\auth.py
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse, redirect
import re

# 登录验证中间件
class AuthMiddleware(MiddlewareMixin):
    """中间件AuthMiddleware"""

    def process_request(self, request):

        # 假设我们想匹配以'/img/check_code/'开头的路径
        path_info_regex = re.compile(r'^/img/check_code/')

        # 使用match方法进行匹配
        match = path_info_regex.match(request.path_info)

        print(request.path_info)

        if match:
            return
        # 排除不需要中间件验证的页面
        if request.path_info in ["/login/"]:
            return

        # 读取当彰访问的用户session的信息,如果能读到,说明已登陆过,就可以向后继续走
        info_dict = request.session.get('info')
        if info_dict:
            # print(info_dict)
            return

        # 如果没有登录过,重定向到登录页面
        return redirect("/login/")

验证码视图函数

# # 前端\django\myweb\blog\views\account.py
# 图片验证码
def check_code_img(request, random):
    """生成图片验证码"""

    # 调用pillow函数,生成图片
    # from blog.utils.check_code import check_code
    img, code_string = check_code(width=120, height=30, char_length=5, font_file='D:\\git-python\\前端\\django\\myweb\\blog\\utils\\ttf\\kumo.ttf', font_size=28)

    # img.show()
    # print(code_string)

    # code_string 写入到自己的session中,以便于后续获取验证码再进行校验
    request.session['image_code'] = code_string
    # 设置session 超时时间为 60s
    request.session.set_expiry(60)

    # 写入内存(Python3)
    # from io import BytesIO
    stream = BytesIO()
    img.save(stream, 'png')
    stream.getvalue()

    return HttpResponse(stream.getvalue())

改造登录页面

前端\django\myweb\blog\templates\login.html


<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>员工管理系统-登录</title>
  {% load static %}
  <link rel="stylesheet" href="{% static 'plugins/bootstrap-3.4.1/css/bootstrap.min.css' %}" />
  <style>
    .account {
        width: 400px;
        border: 1px solid #dddddd;
        border-radius: 5px;
        box-shadow: 5px 5px 20px #aaa;

        margin-left: auto;
        margin-right: auto;
        margin-top: 100px;
        padding: 20px 40px;
    }

    .account h2 {
        margin-top: 10px;
        text-align: center;
    }
</style>
</head>

<body>
    <div class="account">
        <h2>用户登录</h2>
        <div class="panel-body">
            <form method="post" novalidate>
                {% csrf_token %}

                <div class="form-group">
                    <label>用户名</label>
                    {{ form.username }}
                    <span style="color: red;">{{ form.errors.username.0 }}</span>

                </div>
                <div class="form-group">
                    <label>密码</label>
                    {{ form.password }}
                    <span style="color: red;">{{ form.errors.password.0 }}</span>
                </div>
                <div class="form-group">
                    <label for="id_code">图片验证码</label>
                    <div class="row">
                        <div class="col-xs-7">
                            {{ form.code }}
                            <span style="color: red;">{{ form.errors.code.0 }}</span>
                        </div>
                        <div class="col-xs-5">
                            <img id="image_code" src="/img/check_code/666" onclick="change_img()" title="点击换验证码">
                        </div>
                    </div>
                </div>
                <button type="submit" class="btn btn-primary center-block" style="width: 80px;">登录</button>
            </form>
        </div>
    </div>
    <script src="{% static 'js/jquery-3.7.1.min.js' %}"></script>
    <script src="{% static 'plugins/bootstrap-3.4.1/js/bootstrap.min.js' %}"></script>
    <script>
        function change_img() {
            var img = document.getElementById("image_code");
            img.src= '/img/check_code/'+(new Date()).getTime();
            }
    </script>
</body>

</html>

form 相关修改

# 前端\django\myweb\blog\utils\form.py
from django import forms

class BootStrap_check_code:
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 循环ModelForm中的所有字段,给每个字段的插件设置
        for _, field in self.fields.items():
            # 字段中有属性,保留原来的属性,没有属性,才增加
            if field.widget.attrs:
                field.widget.attrs["class"] = "form-control"
                # field.widget.attrs["style"] = "width: 300px;"
                if  "placeholder" in field.widget.attrs:
                    continue
                else:
                    field.widget.attrs["placeholder"] = field.label
            else:
                field.widget.attrs = {
                    "class": "form-control",
                    # "style": "width: 300px;",
                    "placeholder": field.label
                }

# 多继承
class BootStrapForm_check_code(BootStrap_check_code, forms.Form):
    pass
# 前端\django\myweb\blog\utils\form.py

# 用户认证
# form方式
# class LoginForm(forms.Form):
class LoginForm(BootStrapForm_check_code):
    username = forms.CharField(
        label="用户名",
        widget=forms.TextInput(attrs={"class": "form-control"}),
        required=True,
    )
    password = forms.CharField(
        label="密码",
        widget=forms.PasswordInput(attrs={"class": "form-control"}, render_value=True),
        required=True,
    )
    code = forms.CharField(
        label="验证码",
        widget=forms.TextInput(attrs={"class": "form-control"}),
        required=True,
    )

    # 获取用户提交的值
    def clean_password(self):
        pwd = self.cleaned_data.get("password")
        return md5(pwd)

完整 account.py

from django.shortcuts import render, redirect
from blog.models import Admin
from blog.utils.form import LoginForm
# from blog.utils.form import LoginModelForm
from django.http import HttpResponse,HttpResponseRedirect
from blog.utils.check_code import check_code
from io import BytesIO

# 用户登录
def login(request):

    # form
    if request.method == "GET":
        form = LoginForm()
        context = {
            "form": form,
        }

        return render(request, "login.html", context)

    form = LoginForm(data=request.POST)
    if form.is_valid():

        # 获取用户输入的验证码,使用pod获取,并剔除code,再进行下一步数据校验,数据库没有code数据。
        user_input_code = form.cleaned_data.pop('code')
        # 获取用户session内image_code值
        image_code = request.session.get('image_code', "")

        # 区分大小写
        # if user_input_code == image_code:
        # 不区分大小写
        if user_input_code.upper() != image_code.upper():
            form.add_error("code", "验证码错误!")
            context = {
                "form": form,
                }
            return render(request, "login.html", context)

        # 数据提交验证成功,获取到输入的用户名和密码
        # print(form.cleaned_data)  # {'username': 'tang', 'password': 'Tang@1234'}
        # form 里进行 clean_password 数据处理后返回密码存储的md5值去数据库进行比对
        # print(form.cleaned_data) # {'username': 'tang', 'password': '469bb5f078060211a9e6a2a5459bda16'}
        # 数据库数据查询比对
        # admin_object = Admin.objects.filter(username=form.cleaned_data['username'],password=form.cleaned_data['password']).first()
        # 如果数据字段与数据库表字段名一致
        # admin_object = Admin.objects.filter(**form.cleaned_data).first()
        user_object = Admin.objects.filter(username=form.cleaned_data['username']).first()
        admin_object = Admin.objects.filter(username=form.cleaned_data['username'],password=form.cleaned_data['password']).first()

        # if not Admin_object:
        #     form.add_error("username", "用户名或者密码错误!")
        #     form.add_error("password", "用户名或者密码错误!")
        #     context = {
        #         "form": form,
        #         }
        #     return render(request, "login.html", context)

        if not user_object:
            form.add_error("username", "用户名错误!")
            context = {
                "form": form,
                }
            return render(request, "login.html", context)
        elif not admin_object:
            form.add_error("password", "密码错误!")
            context = {
                "form": form,
                }
            return render(request, "login.html", context)
        else:
            # 用户名与密码正确
            # 网站生成随机字符串;写到用户浏览器的cookie中;再写入到session中
            """ COOKIES
            # 例子1:不使用模板
            response = HttpResponse("hello world")
            response.set_cookie(key,value,max_age)
            return response

            # 例子2: 使用模板
            response = render(request,'xxx.html', context)
            response.set_cookie(key,value,max_age)
            return response

            # 例子3: 重定向
            response = HttpResponseRedirect('/login/')
            response.set_cookie(key,value,max_age)
            return response
            # 删除
            response.delete_cookie('username')

            # 获取 COOKIES值
            # 方法一
            request.COOKIES['username']
            # 方法二
            request.COOKIES.get('username','')
            # 判断是否存在
            request.COOKIES.has_key('cookie_name')
            """
            """ session
            # 设置session的值
            request.session['key'] = value
            request.session.set_expiry(time): 设置过期时间, 0表示浏览器关闭则失效

            # 获取session的值
            request.session.get('key', None)

            # 判断一个key是否在session里
            'fav_color' in request.session

            # 获取所有session的key和value
            request.session.keys()
            request.session.values()
            request.session.items()

            # 清除所有session
            request.session.clear()  # 只删除session中值得部分

            # 删除所有session
            # request.session.flush()  # 删除session中的整条记录

            # 删除key为age的session
            del request.session["age"]
            """

            response = HttpResponseRedirect('/admin_list/')
            # 将username写入浏览器cookie,有效时间为360秒
            response.set_cookie('id', admin_object.id, 3600)
            response.set_cookie('name', admin_object.username, 3600)
            request.session["info"] = {'id': admin_object.id, 'name': admin_object.username}
            # request.session.set_expiry(1209600) # 默认 1209600 14天
            request.session.set_expiry(60 * 60 * 24 * 7) # 7天
            # 删除key为image_code的session,防止验证码复用
            del request.session["image_code"]

            return response

    context = {
        "form": form,
        }

    return render(request, "login.html", context)

# 用户登录注销
def  loginout(request):
    """ 注销 """

    # 清除当前session
    request.session.clear()

    return redirect("/login/")

# 图片验证码
def check_code_img(request, random):
    """生成图片验证码"""

    # 调用pillow函数,生成图片
    # from blog.utils.check_code import check_code
    img, code_string = check_code(width=120, height=30, char_length=5, font_file='D:\\git-python\\前端\\django\\myweb\\blog\\utils\\ttf\\kumo.ttf', font_size=28)

    # img.show()
    # print(code_string)

    # code_string 写入到自己的session中,以便于后续获取验证码再进行校验
    request.session['image_code'] = code_string
    # 设置session 超时时间为 60s
    request.session.set_expiry(60)

    # 写入内存(Python3)
    # from io import BytesIO
    stream = BytesIO()
    img.save(stream, 'png')
    # stream.getvalue()

    return HttpResponse(stream.getvalue())