Django 之驗證碼實現

1. django-simple-captcha 模塊

  1. 安裝 django-simple-captcha
pip install django-simple-captcha
pip install Pillow
  1. 註冊

和註冊 app 同樣,captcha 也須要註冊到 settings 中。同時它也會建立本身的數據表,所以還須要數據同步。javascript

# settings.py
INSTALLED_APPS = [
    ...
    'captcha',
]

# 執行命令進行數據遷徙,會發現數據庫中多了一個 captcha_captchastore 的數據表
python manage.py migrate
  1. 添加路由

在項目根目錄下的 urls.py中添加 captcha對應的路由:css

from django.contrib import admin
from django.urls import path, include


urlpatterns = [
    path('admin/', admin.site.urls),
    path('captcha', include('captcha.urls')),        # 驗證碼
]
  1. 修改 Form 表單

Django 中一般都是由 Form 生成表單,而驗證碼通常也伴隨註冊登陸表單,所以須要在 forms.py 中添加驗證碼的字段。html

from django import forms
from captcha.fields import CaptchaField     # 必定要導入這行


class UserForm(forms.Form):
    username = forms.CharField(
        label='用戶名',                # 在表單裏表現爲 label 標籤
        max_length=128,
        widget=forms.TextInput(attrs={'class': 'form-control'})   # 添加 css 屬性
    )

    captcha = CaptchaField(
        label='驗證碼',
        required=True,
        error_messages={
            'required': '驗證碼不能爲空'
        }
    )
  1. 視圖函數:
from django.shortcuts import render
from app.forms import UserForm


def home(request):
    register_form = UserForm(request.POST)
    if register_form.is_valid():
        pass
    register_form = UserForm()
    return render(request, 'index.html', {'register_form': register_form})
  1. 前端渲染

接下來就是在如何前端渲染出來:前端

<html>
    <head></head>
    <body>
        <form action='#' method='post'>
            {{ register_form.captcha.label_tag }}
            {{ register_form.captcha }} {{ 
        </form>
    </body>
</html>

長這樣

2. 手動生成驗證碼

主要利用的是畫圖模塊 PIL 以及隨機模塊 random 在後臺生成一個圖片和一串隨機數,而後保存在內存中(也能夠直接保存在 Django 項目中)。java

在前端指定一個 img 標籤,其 src 屬性路徑爲:生成驗證碼的路徑 <img src='/accounts/check_code/'python

  1. 畫圖程序 check_code.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-

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

_letter_cases = "abcdefghjkmnpqrstuvwxy"  # 小寫字母,去除可能干擾的i,l,o,z
_upper_cases = _letter_cases.upper()  # 大寫字母
_numbers = ''.join(map(str, range(3, 10)))  # 數字
init_chars = ''.join((_letter_cases, _upper_cases, _numbers))

# PIL

def create_validate_code(size=(120, 30),
                         chars=init_chars,
                         img_type="GIF",
                         mode="RGB",
                         bg_color=(255, 255, 255),
                         fg_color=(0, 0, 255),
                         font_size=18,
                         font_type="static/font/Monaco.ttf",
                         length=4,
                         draw_lines=True,
                         n_line=(1, 2),
                         draw_points=True,
                         point_chance=2):
    """
    @todo: 生成驗證碼圖片
    @param size: 圖片的大小,格式(寬,高),默認爲(120, 30)
    @param chars: 容許的字符集合,格式字符串
    @param img_type: 圖片保存的格式,默認爲GIF,可選的爲GIF,JPEG,TIFF,PNG
    @param mode: 圖片模式,默認爲RGB
    @param bg_color: 背景顏色,默認爲白色
    @param fg_color: 前景色,驗證碼字符顏色,默認爲藍色#0000FF
    @param font_size: 驗證碼字體大小
    @param font_type: 驗證碼字體,默認爲 ae_AlArabiya.ttf
    @param length: 驗證碼字符個數
    @param draw_lines: 是否劃干擾線
    @param n_lines: 干擾線的條數範圍,格式元組,默認爲(1, 2),只有draw_lines爲True時有效
    @param draw_points: 是否畫干擾點
    @param point_chance: 干擾點出現的機率,大小範圍[0, 100]
    @return: [0]: PIL Image實例
    @return: [1]: 驗證碼圖片中的字符串
    """

    width, height = size  # 寬高
    # 建立圖形
    img = Image.new(mode, size, bg_color)
    draw = ImageDraw.Draw(img)  # 建立畫筆

    def get_chars():
        """生成給定長度的字符串,返回列表格式"""
        return random.sample(chars, length)

    def create_lines():
        """繪製干擾線"""
        line_num = random.randint(*n_line)  # 干擾線條數

        for i in range(line_num):
            # 起始點
            begin = (random.randint(0, size[0]), random.randint(0, size[1]))
            # 結束點
            end = (random.randint(0, size[0]), random.randint(0, size[1]))
            draw.line([begin, end], fill=(0, 0, 0))

    def create_points():
        """繪製干擾點"""
        chance = min(100, max(0, int(point_chance)))  # 大小限制在[0, 100]

        for w in range(width):
            for h in range(height):
                tmp = random.randint(0, 100)
                if tmp > 100 - chance:
                    draw.point((w, h), fill=(0, 0, 0))

    def create_strs():
        """繪製驗證碼字符"""
        c_chars = get_chars()
        strs = ' %s ' % ' '.join(c_chars)  # 每一個字符先後以空格隔開

        font = ImageFont.truetype(font_type, font_size)
        font_width, font_height = font.getsize(strs)

        draw.text(((width - font_width) / 3, (height - font_height) / 3),
                  strs, font=font, fill=fg_color)

        return ''.join(c_chars)

    if draw_lines:
        create_lines()
    if draw_points:
        create_points()
    strs = create_strs()

    # 圖形扭曲參數
    params = [1 - float(random.randint(1, 2)) / 100,
              0,
              0,
              0,
              1 - float(random.randint(1, 10)) / 100,
              float(random.randint(1, 2)) / 500,
              0.001,
              float(random.randint(1, 2)) / 500
              ]
    img = img.transform(size, Image.PERSPECTIVE, params)  # 建立扭曲

    img = img.filter(ImageFilter.EDGE_ENHANCE_MORE)  # 濾鏡,邊界增強(閾值更大)

    return img, strs

Tipsjquery

這裏須要指定 Monaco.ttf 字體:git

font_type="static/font/Monaco.ttf",

# https://pan.baidu.com/s/1XwyaFC_MROFA4fXujVwH3A 提取碼:17f8 
  1. 視圖函數 views.py
from django.shortcuts import render, redirect, HttpResponse
from blog.check_code import create_validate_code
from io import BytesIO
from django.contrib import auth
from django.http import JsonResponse


def check_code(request):
    """
    獲取驗證碼
    :param request:
    :return:
    """
    stream = BytesIO()
    # 生成圖片 img、數字代碼 code,保存在內存中,而不是 Django 項目中
    img, code = create_validate_code()
    img.save(stream, 'PNG')

    # 寫入 session
    request.session['valid_code'] = code
    print(code)
    return HttpResponse(stream.getvalue())


def login(request):
    """
    登陸視圖
    :param request:
    :return:
    """
    if request.method == 'POST':
        ret = {'status': False, 'message': None}
        username = request.POST.get('username')
        password = request.POST.get('password')

        # 獲取用戶輸入的驗證碼
        code = request.POST.get('check_code')
        p = request.POST.get('p')


        # 用戶輸入的驗證碼與 session 中取出的驗證碼比較
        if code.upper() == request.session.get('valid_code').upper():
            # 驗證碼正確,驗證用戶名密碼是否正確
            user_obj = auth.authenticate(username=username, password=password)
            if user_obj:
                # 驗證經過,則進行登陸操做
                # 封裝到 request.user 中
                auth.login(request, user_obj)
                return redirect('accounts:home')
            else:
                ret['status'] = True
                ret['message'] = '用戶名或密碼錯誤'
                return render(request, 'accounts/login.html', ret)
        else:
            ret['status'] = True
            ret['message'] = '驗證碼錯誤'
        return render(request, 'accounts/login.html', ret)

    return render(request, 'accounts/login.html')
  1. 登陸頁面 login.html
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陸</title>
    <link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.css' %}">
    <style>
        .login-col {
            margin-top: 100px;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="well col-md-6 col-md-offset-3 login-col">
            <h3 class="text-center">登陸</h3>

            <!--錯誤信息-->
            {% if status %}
                <div class="alert alert-danger" role="alert">
                    <p id="login-error">{{ message }}</p>
                    <p id="login-error"></p>
                </div>
            {% endif %}

            <form action="{% url 'accounts:login' %}" method="post" novalidate>
                {% csrf_token %}
                <div class="form-group">
                    <label for="exampleInputUsername">用戶名:</label>
                    <input type="text" class="form-control" id="exampleInputUsername" placeholder="用戶名" name="username">
                </div>

                <div class="form-group">
                    <label for="exampleInputPassword1">密碼:</label>
                    <input type="password" class="form-control" id="exampleInputPassword" placeholder="密碼"
                           name="password">
                </div>

                <!--驗證碼-->
                <div class="form-group">
                    <label for="id_code">驗證碼:</label>
                    <div class="row">
                        <div class="col-md-7 col-xs-7">
                            <input type="text" class="form-control" id="id_code" placeholder="請輸入驗證碼" name="check_code">
                        </div>

                        <div class="col-md-5 col-xs-5">
                            <img src="/accounts/check_code" onclick="changeImg(this)" class="img">
                        </div>
                    </div>
                </div>


                <div class="checkbox">
                    <label>
                        <input type="checkbox"> 記住我
                    </label>
                </div>
                <button type="submit" class="btn btn-primary btn-block" id="login-button">提交</button>
            </form>
        </div>
    </div>
</div>

<script src="{% static 'js/jquery-3.1.1.js' %}"></script>
<script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.js' %}"></script>

<script>
    function changeImg(ths) {
        // 硬編碼
        ths.src = '/accounts/check_code/?temp=' + Math.random();

        // 使用命名空間,發送請求
        // ths.src = '{% url 'accounts:check_code' %}' + '?temp=' + Math.random();

    }

</script>

</body>
</html>

給驗證碼圖片 img 標籤綁定 onclick 事件,當用戶點擊驗證碼時,至關於訪問 http://127.0.0.1:8000/accounts/check_code/?temp=一個隨機數,即向 http://127.0.0.1:8000/accounts/check_code/ 發送一個 get 請求,再次從後臺生成一個驗證碼並返回。ajax

  1. 路由 accounts/urls.py
from django.urls import path
from accounts import views

app_name = 'accounts'
urlpatterns = [
    # 登陸
    path('login/', views.login, name='login'),      

    # 獲取驗證碼
    path('check_code/', views.check_code, name='check_code'),

    # 首頁
    path('home/', views.home, name='home'),

    # 註銷
    path('logout/', views.logout, name='logout'),
]

Tips數據庫

  • 畫圖程序 check_code.py 保存在項目任意位置便可,只需在視圖函數中導入便可。
  • Monaco.ttf 字體不可或缺,放置在靜態文件中便可,可是須要修改 check_code.py 中的字體引入路徑。
  • 驗證用戶輸入的驗證碼是否正確,只需從 session 中取出生成的驗證碼與其比較便可。
  • 驗證碼刷新,只需讓其再發送一次 get 請求便可。

3. 極驗科技之滑動驗證碼

除上述兩種圖片驗證碼之外,還有一種滑動驗證碼,用的比較多有 極驗科技

  1. 官方下載源碼包,並安裝 geetest 模塊

官網

訪問官網,選擇:技術文檔 —— 行爲驗證 —— 選擇服務端部署爲 Python —— 使用 git 或直接下載 gt3-python-sdk 文件。

pip install geetest
pip install requests    # 有可能還須要 requests 模塊

<!-- 引入封裝了failback的接口--initGeetest -->
<script src="http://static.geetest.com/static/tools/gt.js"></script>
  1. 登陸頁面 login2.html

html 部分

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陸</title>
    <link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.css' %}">
    <style>
        .login-col {
            margin-top: 100px;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="well col-md-6 col-md-offset-3 login-col">
            <h3 class="text-center">登陸</h3>
            <form>
                {% csrf_token %}
                <div class="form-group">
                    <label for="username">用戶名:</label>
                    <input type="text" class="form-control" id="username" placeholder="用戶名" name="username">
                </div>

                <div class="form-group">
                    <label for="password">密碼:</label>
                    <input type="password" class="form-control" id="password" placeholder="密碼" name="password">
                </div>


                <!--極驗科技滑動驗證碼-->
                <div class="form-group">
                    <!-- 放置極驗的滑動驗證碼 -->
                    <div id="popup-captcha"></div>
                </div>

                <!--記住我-->
                <div class="checkbox">
                    <label>
                        <input type="checkbox"> 記住我
                    </label>
                </div>

                <!--登陸按鈕-->
                <button type="button" class="btn btn-primary btn-block" id="login-button">提交</button>

                <!--錯誤信息-->
                <span class="login-error"></span>
            </form>
        </div>
    </div>
</div>
</body>
</html>

JavaScript 部分

<script src="{% static 'js/jquery-3.3.1.js' %}"></script>
<script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.js' %}"></script>
<!-- 引入封裝了failback的接口--initGeetest -->
<script src="http://static.geetest.com/static/tools/gt.js"></script>


<script>
    var handlerPopup = function (captchaObj) {
        // 成功的回調
        captchaObj.onSuccess(function () {
            var validate = captchaObj.getValidate();
            var username = $('#username').val();
            var password = $('#password').val();
            console.log(username, password);
            $.ajax({
                url: "/accounts/login2/", // 進行二次驗證
                type: "post",
                dataType: 'json',
                data: {
                    username: username,
                    password: password,
                    csrfmiddlewaretoken: $("[name='csrfmiddlewaretoken']").val(),
                    geetest_challenge: validate.geetest_challenge,
                    geetest_validate: validate.geetest_validate,
                    geetest_seccode: validate.geetest_seccode
                },
                success: function (data) {
                    console.log(data);
                    if (data.status) {
                        // 有錯誤,在頁面上顯示
                        $('.login-error').text(data.msg);
                    } else {
                        // 登陸成功
                        location.href = data.msg;
                    }
                }
            });
        });

        // 當點擊登陸按鈕時,彈出滑動驗證碼窗口
        $("#login-button").click(function () {
            captchaObj.show();
        });

        // 將驗證碼加到id爲captcha的元素裏
        captchaObj.appendTo("#popup-captcha");
        // 更多接口參考:http://www.geetest.com/install/sections/idx-client-sdk.html
    };

    $('#username, #password').focus(function () {
        // 將以前的錯誤清空
        $('.login-error').text('');
    });

    // 驗證開始須要向網站主後臺獲取id,challenge,success(是否啓用failback)
    $.ajax({
        url: "/accounts/pc-geetest/register?t=" + (new Date()).getTime(), // 加隨機數防止緩存
        type: "get",
        dataType: "json",
        success: function (data) {
            // 使用initGeetest接口
            // 參數1:配置參數
            // 參數2:回調,回調的第一個參數驗證碼對象,以後可使用它作appendTo之類的事件
            initGeetest({
                gt: data.gt,
                challenge: data.challenge,
                product: "popup", // 產品形式,包括:float,embed,popup。注意只對PC版驗證碼有效
                offline: !data.success // 表示用戶後臺檢測極驗服務器是否宕機,通常不須要關注
                // 更多配置參數請參見:http://www.geetest.com/install/sections/idx-client-sdk.html#config
            }, handlerPopup);
        }
    });

</script>

JS 代碼主要分爲兩部分,第一部分是獲取表單的 value 值,向後臺發送 Ajax 請求,以驗證用戶名及密碼是否正確,如有錯誤將錯誤信息顯示出來。第二部分向後臺獲取驗證碼所需相關參數。

  1. 視圖函數 views.py
from django.shortcuts import render, redirect, HttpResponse
from django.http import JsonResponse
from geetest import GeetestLib


def login2(request):
    if request.method == 'POST':
        ret = {'status': False, 'msg': None}
        username = request.POST.get('username')
        password = request.POST.get('password')

        print(username, password)

        # 獲取極驗,滑動驗證碼相關參數
        gt = GeetestLib(pc_geetest_id, pc_geetest_key)
        challenge = request.POST.get(gt.FN_CHALLENGE, '')
        validate = request.POST.get(gt.FN_VALIDATE, '')
        seccode = request.POST.get(gt.FN_SECCODE, '')
        status = request.session[gt.GT_STATUS_SESSION_KEY]
        user_id = request.session["user_id"]

        print(gt, challenge, validate, seccode, status)

        if status:
            result = gt.success_validate(challenge, validate, seccode, user_id)
        else:
            result = gt.failback_validate(challenge, validate, seccode)
        if result:
            # 驗證碼正確
            # 利用auth模塊作用戶名和密碼的校驗
            user_obj = auth.authenticate(username=username, password=password)
            if user_obj:
                # 用戶名密碼正確
                # 給用戶作登陸
                auth.login(request, user_obj)
                ret["msg"] = "/accounts/home/"
                # return redirect('accounts:home')
            else:
                # 用戶名密碼錯誤
                ret["status"] = True
                ret["msg"] = "用戶名或密碼錯誤!"
        else:
            ret["status"] = True
            ret["msg"] = "驗證碼錯誤"

        return JsonResponse(ret)
    return render(request, "accounts/login2.html")

# 請在官網申請ID使用,示例ID不可以使用
pc_geetest_id = "b46d1900d0a894591916ea94ea91bd2c"
pc_geetest_key = "36fc3fe98530eea08dfc6ce76e3d24c4"

# 處理極驗 獲取驗證碼的視圖
def get_geetest(request):
    user_id = 'test'
    gt = GeetestLib(pc_geetest_id, pc_geetest_key)
    status = gt.pre_process(user_id)
    request.session[gt.GT_STATUS_SESSION_KEY] = status
    request.session["user_id"] = user_id
    response_str = gt.get_response_str()
    return HttpResponse(response_str)
  1. 路由 accounts/urls.py
from django.urls import path
from accounts import views

app_name = 'accounts'
urlpatterns = [
    path('home/', views.home, name='home'),
 
    # 極驗滑動驗證碼 獲取驗證碼的url
    path('pc-geetest/register/', views.get_geetest, name='get_geetest'),

    path('login2/', views.login2, name='login2'),
]

總結

  • 極驗滑動驗證碼除了支持 Django,還支持 flask、tornado 等
  • 上述以 Ajax 形式發送的 post 請求,所以注意查看是否設置了 csrf_token,而且提交按鈕 button 的提交類型應該爲 button 而非 submit (踩坑)
  • 同時它還有嵌入式,移動端等,更多示例請參考下載的官方源碼。
相關文章
相關標籤/搜索