16 Django

Django - 登录(含随机生成图片验证码)、注册示例

一、登录 - 随机生成图片验证码

1、随机生成验证码

  Python随机生成图片验证码,需要使用PIL模块,安装方式如下:

  pip3 install pillow

  1)创建图片

from PIL import Image
img = Image.new(mode='RGB', size=(120, 30), color=(255, 255, 255))
with open('code.png', 'wb') as f:    # 保存在本地(即写入硬盘)
    img.save(f, format='png')

  参数说明:

  mode='RGB'  表示以RGB来表示颜色

  size=(120,30)  表示坐标

  color=(255, 255, 255)  表示白色

  此时,打开启动文件所在目录,里面就有了一个宽120,高30的白色code.png图片。

  2)创建画笔(用于在图片上画任意内容)

from PIL import Image, ImageDraw
img = Image.new(mode='RGB', size=(120, 30), color=(255, 255, 255)) draw = ImageDraw.Draw(img, mode='RGB') # 创建画笔对象draw img.show() # 在图片查看器中打开,这句会调用系统默认的图片管理工具

  3)画点 - point()方法

 
from PIL import Image, ImageDraw
img = Image.new(mode='RGB', size=(120, 30), color=(255, 255, 255)) draw = ImageDraw.Draw(img, mode='RGB') # point()第一个参数:表示坐标, 第二个参数:表示颜色 draw.point([100, 20], fill='red') draw.point([60, 10], fill=(0, 255, 0)) # 保存在本地 with open('code.png', 'wb') as f: img.save(f, format='png')
 

效果如下图:

  4)画线 - line()方法

 
from PIL import Image, ImageDraw
img = Image.new(mode='RGB', size=(540, 150), color=(255, 255, 255)) draw = ImageDraw.Draw(img, mode='RGB') # line()第一个参数:表示起始坐标和结束坐标,第二个参数:表示颜色 draw.line((50, 50, 50, 150), fill='red') # 上面一句表示画一条坐标(x=100,y=100)到(x=100,y=300)的直线 draw.line((50, 100, 150, 50), fill=(120, 120, 120)) draw.line((50, 50, 150, 50), fill=(0, 255, 255)) with open('code.png', 'wb') as f: img.save(f, format='png')
 

效果如下:

  5)画圆 - arc()方法

 
from PIL import Image, ImageDraw
img = Image.new(mode='RGB', size=(500, 140), color=(255, 255, 255)) draw = ImageDraw.Draw(img, mode='RGB') # 第一个参数:表示起始坐标和结束坐标(圆要画在其中间,两点确定的矩形的内切圆) # 第二个参数:表示开始角度 # 第三个参数:表示结束角度 # 第四个参数:表示颜色 draw.arc((200, 20, 300, 120), 0, 360, fill='red') with open('code.png', 'wb') as f: img.save(f, format='png')
 

效果如下:

  6)写文本 - text()方法

 
from PIL import Image, ImageDraw
img = Image.new(mode='RGB', size=(80, 20), color=(255, 255, 255)) draw = ImageDraw.Draw(img, mode='RGB') # 第一个参数:表示起始坐标,第二个参数:表示写入的文本,第三个参数:表示颜色 draw.text([0, 0], 'python', 'red') with open('code.png', 'wb') as f: img.save(f, format='png')
 

效果如下:

  7)特殊字体文字(下载好引用的字体文件)

 
from PIL import Image, ImageDraw, ImageFont
img = Image.new(mode='RGB', size=(120, 40), color=(255, 255, 255)) draw = ImageDraw.Draw(img, mode='RGB') font = ImageFont.truetype('kumo.ttf', 28) # 第一个参数:表示字体文件路径 # 第二个参数:表示字体大小 draw.text((0, 0), 'python', 'red', font=font) # 第一个参数:表示起始坐标 # 第二个参数:表示写入内容 # 第三个参数:表示颜色 # 第四个参数:表示字体 with open('code.png', 'wb') as f: img.save(f, format='png')
 

效果如下:

  8)随机生成图片验证码

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

def check_code(width=120, height=30, char_length=5, font_file='../static/font/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:
        """
        ranNum = str(random.randint(0, 9))
        ranLower = chr(random.randint(65, 90))
        ranUpper = chr(random.randint(97, 120))
        return random.choice([ranNum, ranLower, ranUpper])

    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 = ( height - font_size ) / 2
        draw.text([i * width / char_length, h], char, font=font, fill=rndColor())

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

    # 写干扰圆圈
    for i in range(40):
        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:   # f是写入磁盘的文件句柄
    #     img.save(f, format='png')
    # data = f.read()   # data是读取图片的字节

    # 3. 写入内存(Python3)
    # img,code = check_code()
    # from io import BytesIO    # 内存管理的模块
    # stream = BytesIO()         # stream是写入内存的文件句柄
    # img.save(stream, 'png')
    # data = stream.getvalue()

    # 4. 写入内存(Python2)
    # img,code = check_code()
    # import StringIO
    # stream = StringIO.StringIO()  # stream是写入内存的文件句柄
    # img.save(stream, 'png')
    # data = stream.getvalue()
 

效果如下:

2、基于ajax实现登录的示例代码

  1)urls.py中关于登录代码:

path('login/', views.login,),  # 获取登录页面url
path('get_identifyCode/', views.get_identifyCode,),  # 获取验证码对应url

  2)login.html核心代码:

 
<body>
<div id="particles-js">
    <div class="login">
        <p class="login-top">登录</p>
        {% csrf_token %}
        <div class="login-center clearfix">
            <label class="" for="user">用户名</label>
            <input type="text" id="user" placeholder="用户名" />
        </div>

        <div class="login-center clearfix">
            <label class="iconfont labelFS" for="pwd">密码</label>
            <input type="password" id="pwd" placeholder="密码" />
        </div>

        <div class="login-center clearfix">
            <label class="iconfont labelFS" for="validcode">&#xe615;</label>
            <input type="text" id="validcode" placeholder="验证码" />
            <img src="/get_identifyCode/" alt="验证码" title="换一张" class="validImg" id="img" width="88" height="30" >
        </div>
        <a href="javascript:void(0);" class="login_btn">登录</a>
        <p class="error"></p>
    </div>
</div>

<script src="jquery.min.js"></script>

<script>
    // ajax 登录
    $(".login_btn").click(function () {
        $.ajax({
            url:"",
            type:"post",
            // data发送urlencoded格式就行,数据没那么深,没必要发json格式
            data:{
                 user:$("#user").val(),
                 pwd:$("#pwd").val(),
                 validcode:$("#validcode").val(),
                 csrfmiddlewaretoken:$("[name='csrfmiddlewaretoken']").val()
            },
            success:function (response) {
                console.log(response);
                if(response.user){
                    // 登录成功
                    location.href="/index/"
                }
                else{
                    // 登录失败
                    $(".error").html(response.err_msg)
                }
            }
        })
    });

    //  验证码刷新:img标签有一个天然的发请求的模式,即src的路径后边拼接一个问号就会发一次请求,利用这一原理可以实现验证码刷新
    $("#img").click(function () {
        this.src += "?"
    });

</script>
</body>
 

  3)views.py中获取随机验证码的视图函数代码(验证码保存利用session)

 
def get_identifyCode(request):
      img,code = check_code()  # 利用上面的模块得到img对象和验证码code
  
    f = BytesIO()  # 得到写入内存的文件句柄
    img.save(f, "png")   # 写入内存
    data = f.getvalue()   # 从内存中读出

    # 将验证码存在各自的session中,这样做的好处是每个人都有自己的验证码,不会相互混淆(一定不能设为全局变量)
    request.session['keep_str'] = code

    return HttpResponse(data)
 

  4)views.py中login视图函数代码

 
from django.contrib import auth
def login(request):
    # if request.method == "POST":
    if request.is_ajax():    # 判断是否ajax请求
        user = request.POST.get("user")
        pwd = request.POST.get("pwd")
        validcode = request.POST.get("validcode")
        # Ajax请求通常返回一个自己构建的字典
        response={"user": None, "err_msg": ""}
          # request.session.get("keep_str")取出session中验证码与用户输入作判断
        if validcode.upper() == request.session.get("keep_str").upper():  
            user_obj = auth.authenticate(username=user, password=pwd)
            print("user_obj", user_obj, bool(user_obj))
            if user_obj:
                response["user"] = user
                auth.login(request, user_obj)  # 保存用户状态
            else:
                response['err_msg'] = "用户名或者密码错误!"
        else:
            response["err_msg"] = "验证码错误!"
        return JsonResponse(response)
    else:
        return render(request, "login.html")
 

二、基于ajax和forms组件实现注册示例

1)model.py

from django.contrib.auth.models import AbstractUser

class UserInfo(AbstractUser):  # 将原生auth_user表扩展一个tel手机号字段
    tel=models.CharField(max_length=32)

2)forms.py(其实代码放在哪里没关系,最重要的是程序能找到,为了解耦,我们可以定义一个form.py)

 
from django import forms
# exceptions中存着django的所有错误,错误在核心组件中
from django.core.exceptions import ValidationError
from django.forms import widgets
from app01.models import UserInfo

class UserForm(forms.Form):   # UserForm中定义需要校验的字段
    username=forms.CharField(min_length=5,
                  label="用户名")
    password=forms.CharField(min_length=5,
                 widget=widgets.PasswordInput(),
                 label="密码")
    r_pwd=forms.CharField(min_length=5,
                   widget=widgets.PasswordInput(),
                   label="确认密码")
    email=forms.EmailField(min_length=5,
                label="邮箱")

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs.update({'class': 'form-control'}) # 统一加class

    def clean_user(self):
        val=self.cleaned_data.get("username")
        user=UserInfo.objects.filter(username=val).first()
        if user:
            raise ValidationError("用户已存在!")
        else:
            return val

    def clean_pwd(self):
        val=self.cleaned_data.get("password")
        if val.isdigit():
            raise ValidationError("密码不能是纯数字!")
        else:
            return val

    def clean_email(self):
        val = self.cleaned_data.get("email")
        if re.search("w+@163.com$", val):
            return val
        else:
            raise ValidationError("邮箱必须是163邮箱!")
        
    def clean(self):
        pwd=self.cleaned_data.get("password")
        r_pwd=self.cleaned_data.get("r_pwd")

        if pwd and r_pwd and r_pwd!=pwd:
            self.add_error("r_pwd", ValidationError("两次密码不一致!"))
        else:
            return self.cleaned_data
 

3)reg.html核心代码:

 
<body>
<h3>注册页面</h3>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <form action="" method="">
            {% csrf_token %}
            {% for field in form %}
                <div class="form-group">
                    <label for="">{{ field.label }}</label>
                    {{ field }}
                    <span class="error"></span>
                </div>
            {% endfor %}
            <input type="button" class="btn btn-primary reg_btn" value="注册">
            </form>
        </div>
    </div>
</div>
<script src="jquery.min.js"></script>
<script>
    $(".reg_btn").click(function () {
        $.ajax({
            url:"",
            type:"post",
            data:{
                username:$("#id_username").val(),
                password:$("#id_password").val(),
                r_pwd:$("#id_r_pwd").val(),
                email:$("#id_email").val(),
                csrfmiddlewaretoken:$("[name='csrfmiddlewaretoken']").val()

            },
            success:function (res) {
                if (res.user){
                    // 注册成功
                    location.href="/login/"
                }
                else{
                    // 清除错误
                    $(".error").html("");
                    //  展示新的错误
                    $.each(res.err_msg,function (i,j) {
                       $("#id_"+i).next().html(j[0]);
                    })
                }
            }
        })
    })
</script>
</body>
 

4)views.py中注册的视图函数reg

 
def reg(request):
    if request.method == "POST":
        form = UserInfo(request.POST)
        res = {"user": None, "err_msg": ""}
        if form.is_valid():
            res["user"] = form.cleaned_data.get("username")
            del form.cleaned_data["r_pwd"]   # 因表中无此字段,只需校验,不插入
            UserInfo.objects.create_user(**form.cleaned_data)
        else:
            res["err_msg"] =form.errors
        return JsonResponse(res)
    else:   # get请求
        form = UserInfoModelForm()
        return  render(request,"reg.html",{"form": form})
 

三、补充知识点

1、对原生auth_user表扩展字段(使用AbstractUser)

  我们之前学习用户认证组件时,用的是django提供的auth_user表,即通过引入User对象(from django.contrib.auth.models import User)去操作它,我们又发现源码中User类继承了AbstractUser类,所以AbstractUser和User其实就是一张表,所以当我们想要有用户认证功能,又想要一些auth_user表中没有的字段时,可以按照如下这样做:

  在models.py中,引入AbstractUser,并且自己定义一个用户类(表),这时类中只定义django的auth_user表中没有而你又想使用的字段即可,并且让你定义的类继承AbstractUser,如下:

from django.contrib.auth.models import AbstractUser
class UserInfo(AbstractUser): tel=models.CharField(max_length=32) # 扩展了一个手机号字段

  注意:写完以上代码就直接去迁移数据库会报错“HINT: Add or change a related_name argument to the definition for ......”,我们需要在settings.py中加上下面这句话,来告诉Django我们要使用自己定义的表作为用户认证表(因此登录的使用方法不变,认证时django会自己去找这张表):

AUTH_USER_MODEL="app01.UserInfo"

  这时再去进行数据库迁移,我们发现,数据库中没有auth_user表了,而我们自己定义的表中除了有自己定义的那些字段外,还有之前auth_user表中的所有字段,这就代表已经达到了我们的目的。

  补充:通过命令创建超级用户的方式:

Tools -- > Run manage.py Task    # 运行起来manage.py,再输入如下命令
manage.py@myproject > createsuperuser   # 执行后根据提示输入用户名,密码,邮箱
# 注意:输入的密码会进行加密处理,再存入表中,并且命令输入密码要求最少8位

2、JsonResponse的使用

我们发现,一般浏览器发送Ajax请求给服务器时,都会返回一个字典,我们需要先将字典序列化,浏览器接收到后再进行反序列化,你会不会觉得这样做有点繁琐?其实,django为我们提供了一个JsonResponse类,它为我们做好了json的序列化,并且浏览器接收到之后,ajax也会自动为我们反序列化,即ajax中success函数接收到的response就是反序列化之后的数据,直接使用即可,如上面登录示例部分代码:

from django.http import JsonResponse      # 引入JsonResponse
def login(request):
        if request.is_ajax():   
            ......            
            response={"user":None, "err_msg": ""}
            ......
            return JsonResponse(response)
 

  分析原因:JsonResponse本质也继承了HttpResponse,而且既为我们做了序列化的操作,还将数据格式设置为json,ajax收到设置了json格式的数据也会为我们自动反序列化,也说明了不仅仅请求头中有content-type,响应头中也有,JsonResponse源码如下:

 
class JsonResponse(HttpResponse):
    def __init__(self, data, encoder=DjangoJSONEncoder, safe=True,
                 json_dumps_params=None, **kwargs):
        if safe and not isinstance(data, dict):
            raise TypeError(
                'In order to allow non-dict objects to be serialized set the '
                'safe parameter to False.'
            )
        if json_dumps_params is None:
            json_dumps_params = {}
        kwargs.setdefault('content_type', 'application/json')
        data = json.dumps(data, cls=encoder, **json_dumps_params)
        super().__init__(content=data, **kwargs)
 

3、forms组件中对渲染出来的input输入框统一增加一个类名

  我们在学习forms组件时,可以分别给每个字段设置一个类名,如class="form-control",但发现像之前那样写的有代码冗余的问题,按照如下方式写可以解决此问题:

 
from django import forms
from django.forms import widgets
class UserForm(forms.Form):
    user=forms.CharField(min_length=5,
                 label="用户名")
    pwd=forms.CharField(min_length=5,
                 widget=widgets.PasswordInput(),
                 label="密码")
    r_pwd=forms.CharField(min_length=5,
                 widget=widgets.PasswordInput(),
                 label="确认密码")
    email=forms.EmailField(min_length=5,
                 label="邮箱")

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for filed in self.fields.values():
            filed.widget.attrs.update({'class': 'form-control'})
 

4、关于全局钩子__all__的问题

我们知道全局钩子的错误信息都在__all__中,源码中是这样写的:

所以知道了这些,我们可以自己设置全局钩子的字段,避免跟其他字段规律不一致造成单独判断的问题,如下方式:

 
  # 全局钩子:校验两次密码不一致
  def clean(self):
      pwd=self.cleaned_data.get("pwd")
      r_pwd=self.cleaned_data.get("r_pwd")

      if pwd and r_pwd and r_pwd!=pwd:
          self.add_error("r_pwd", ValidationError("两次密码不一致!"))
        # 自己定义错误信息对应的字段是r_pwd
      else:
          return self.cleaned_data
 

5、关于具有提交功能的按钮问题

我们知道form表单是浏览器向服务器发请求的一种方式,提交按钮也有多种,但是要注意,具有提交功能的按钮有两种:<input type="submit" value="提交" />和<button>提交</button>,也就是说,当你想用form表单发请求时,可以用以上两种的任一种,但是当你想基于ajax发送请求时,若有form标签,则一定不要用以上两种提交按钮,否则当你点击按钮发送ajax时会自动以form表单的方式再发一次请求,使用<input type="button" value="提交" />是可以的,因为它没有提交form表单功能。

原文地址:https://www.cnblogs.com/xintiao-/p/10078954.html