1. django-simple-captcha 模塊
- 安裝
django-simple-captcha
pip install django-simple-captcha
pip install Pillow
- 注冊
和注冊 app 一樣,captcha 也需要注冊到 settings
中。同時它也會創建自己的數據表,因此還需要數據同步。
# settings.py
INSTALLED_APPS = [
...
'captcha',
]
# 執行命令進行數據遷徙,會發現數據庫中多了一個 captcha_captchastore 的數據表
python manage.py migrate
- 添加路由
在項目根目錄下的 urls.py
中添加 captcha
對應的路由:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('captcha', include('captcha.urls')), # 驗證碼
]
- 修改 Form 表單
Django 中通常都是由 Form 生成表單,而驗證碼一般也伴隨注冊登錄表單,因此需要在 forms.py
中添加驗證碼的字段。
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>
2. 手動生成驗證碼
主要利用的是畫圖模塊 PIL
以及隨機模塊 random
在后台生成一個圖片和一串隨機數,然后保存在內存中(也可以直接保存在 Django 項目中)。
在前端指定一個 img
標簽,其 src 屬性路徑為:生成驗證碼的路徑 <img src='/accounts/check_code/'
。
- 畫圖程序
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
Tips
這里需要指定 Monaco.ttf
字體:
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 請求,再次從后台生成一個驗證碼並返回。
- 路由
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. 極驗科技之滑動驗證碼
除上述兩種圖片驗證碼以外,還有一種滑動驗證碼,用的比較多有 極驗科技。
- 官方下載源碼包,並安裝
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'),
]
總結
- 極驗滑動驗證碼除了支持 Django,還支持 flask、tornado 等
- 上述以 Ajax 形式發送的 post 請求,因此注意查看是否設置了 csrf_token,並且提交按鈕
button
的提交類型應該為button
而非submit
(踩坑) - 同時它還有嵌入式,移動端等,更多示例請參考下載的官方源碼。