django-simple-captcha
pip install django-simple-captcha pip install Pillow
和註冊 app 同樣,captcha 也須要註冊到 settings
中。同時它也會建立本身的數據表,所以還須要數據同步。javascript
# settings.py INSTALLED_APPS = [ ... 'captcha', ] # 執行命令進行數據遷徙,會發現數據庫中多了一個 captcha_captchastore 的數據表 python manage.py migrate
在項目根目錄下的 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')), # 驗證碼 ]
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': '驗證碼不能爲空' } )
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})
接下來就是在如何前端渲染出來:前端
<html> <head></head> <body> <form action='#' method='post'> {{ register_form.captcha.label_tag }} {{ register_form.captcha }} {{ </form> </body> </html>
主要利用的是畫圖模塊 PIL
以及隨機模塊 random
在後臺生成一個圖片和一串隨機數,而後保存在內存中(也能夠直接保存在 Django 項目中)。java
在前端指定一個 img
標籤,其 src 屬性路徑爲:生成驗證碼的路徑 <img src='/accounts/check_code/'
。python
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
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')
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
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
中的字體引入路徑。除上述兩種圖片驗證碼之外,還有一種滑動驗證碼,用的比較多有 極驗科技。
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>
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 請求,以驗證用戶名及密碼是否正確,如有錯誤將錯誤信息顯示出來。第二部分向後臺獲取驗證碼所需相關參數。
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)
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'), ]
總結
button
的提交類型應該爲 button
而非 submit
(踩坑)