1. django-simple-captcha 模块

安装 django-simple-captcha

  1. pip install django-simple-captcha
  2. pip install Pillow

注册

和注册 app 一样,captcha 也需要注册到 settings 中。同时它也会创建自己的数据表,因此还需要数据同步。

  1. # settings.py
  2. INSTALLED_APPS = [
  3. ...
  4. 'captcha',
  5. ]
  6. # 执行命令进行数据迁徙,会发现数据库中多了一个 captcha_captchastore 的数据表
  7. python manage.py migrate

添加路由

在项目根目录下的 urls.py中添加 captcha对应的路由:

  1. from django.contrib import admin
  2. from django.urls import path, include
  3. urlpatterns = [
  4. path('admin/', admin.site.urls),
  5. path('captcha', include('captcha.urls')), # 验证码
  6. ]

修改 Form 表单

Django 中通常都是由 Form 生成表单,而验证码一般也伴随注册登录表单,因此需要在 forms.py 中添加验证码的字段。

  1. from django import forms
  2. from captcha.fields import CaptchaField # 一定要导入这行
  3. class UserForm(forms.Form):
  4. username = forms.CharField(
  5. label='用户名', # 在表单里表现为 label 标签
  6. max_length=128,
  7. widget=forms.TextInput(attrs={'class': 'form-control'}) # 添加 css 属性
  8. )
  9. captcha = CaptchaField(
  10. label='验证码',
  11. required=True,
  12. error_messages={
  13. 'required': '验证码不能为空'
  14. }
  15. )

视图函数:

  1. from django.shortcuts import render
  2. from app.forms import UserForm
  3. def home(request):
  4. register_form = UserForm(request.POST)
  5. if register_form.is_valid():
  6. pass
  7. register_form = UserForm()
  8. return render(request, 'index.html', {'register_form': register_form})

前端渲染

  1. <html>
  2. <head></head>
  3. <body>
  4. <form action='#' method='post'>
  5. {{ register_form.captcha.label_tag }}
  6. {{ register_form.captcha }} {{
  7. </form>
  8. </body>
  9. </html>

2. 手动生成验证码

主要利用的是画图模块 PIL 以及随机模块 random 在后台生成一个图片和一串随机数,然后保存在内存中(也可以直接保存在 Django 项目中)。
在前端指定一个 img 标签,其 src 属性路径为:生成验证码的路径 Django之验证码实现 - 图1

画图程序 check_code.py

  1. #!/usr/bin/env python
  2. # -*- coding:utf-8 -*-
  3. import random
  4. from PIL import Image, ImageDraw, ImageFont, ImageFilter
  5. _letter_cases = "abcdefghjkmnpqrstuvwxy" # 小写字母,去除可能干扰的i,l,o,z
  6. _upper_cases = _letter_cases.upper() # 大写字母
  7. _numbers = ''.join(map(str, range(3, 10))) # 数字
  8. init_chars = ''.join((_letter_cases, _upper_cases, _numbers))
  9. # PIL
  10. def create_validate_code(size=(120, 30),
  11. chars=init_chars,
  12. img_type="GIF",
  13. mode="RGB",
  14. bg_color=(255, 255, 255),
  15. fg_color=(0, 0, 255),
  16. font_size=18,
  17. font_type="static/font/Monaco.ttf",
  18. length=4,
  19. draw_lines=True,
  20. n_line=(1, 2),
  21. draw_points=True,
  22. point_chance=2):
  23. """
  24. @todo: 生成验证码图片
  25. @param size: 图片的大小,格式(宽,高),默认为(120, 30)
  26. @param chars: 允许的字符集合,格式字符串
  27. @param img_type: 图片保存的格式,默认为GIF,可选的为GIF,JPEG,TIFF,PNG
  28. @param mode: 图片模式,默认为RGB
  29. @param bg_color: 背景颜色,默认为白色
  30. @param fg_color: 前景色,验证码字符颜色,默认为蓝色#0000FF
  31. @param font_size: 验证码字体大小
  32. @param font_type: 验证码字体,默认为 ae_AlArabiya.ttf
  33. @param length: 验证码字符个数
  34. @param draw_lines: 是否划干扰线
  35. @param n_lines: 干扰线的条数范围,格式元组,默认为(1, 2),只有draw_lines为True时有效
  36. @param draw_points: 是否画干扰点
  37. @param point_chance: 干扰点出现的概率,大小范围[0, 100]
  38. @return: [0]: PIL Image实例
  39. @return: [1]: 验证码图片中的字符串
  40. """
  41. width, height = size # 宽高
  42. # 创建图形
  43. img = Image.new(mode, size, bg_color)
  44. draw = ImageDraw.Draw(img) # 创建画笔
  45. def get_chars():
  46. """生成给定长度的字符串,返回列表格式"""
  47. return random.sample(chars, length)
  48. def create_lines():
  49. """绘制干扰线"""
  50. line_num = random.randint(*n_line) # 干扰线条数
  51. for i in range(line_num):
  52. # 起始点
  53. begin = (random.randint(0, size[0]), random.randint(0, size[1]))
  54. # 结束点
  55. end = (random.randint(0, size[0]), random.randint(0, size[1]))
  56. draw.line([begin, end], fill=(0, 0, 0))
  57. def create_points():
  58. """绘制干扰点"""
  59. chance = min(100, max(0, int(point_chance))) # 大小限制在[0, 100]
  60. for w in range(width):
  61. for h in range(height):
  62. tmp = random.randint(0, 100)
  63. if tmp > 100 - chance:
  64. draw.point((w, h), fill=(0, 0, 0))
  65. def create_strs():
  66. """绘制验证码字符"""
  67. c_chars = get_chars()
  68. strs = ' %s ' % ' '.join(c_chars) # 每个字符前后以空格隔开
  69. font = ImageFont.truetype(font_type, font_size)
  70. font_width, font_height = font.getsize(strs)
  71. draw.text(((width - font_width) / 3, (height - font_height) / 3),
  72. strs, font=font, fill=fg_color)
  73. return ''.join(c_chars)
  74. if draw_lines:
  75. create_lines()
  76. if draw_points:
  77. create_points()
  78. strs = create_strs()
  79. # 图形扭曲参数
  80. params = [1 - float(random.randint(1, 2)) / 100,
  81. 0,
  82. 0,
  83. 0,
  84. 1 - float(random.randint(1, 10)) / 100,
  85. float(random.randint(1, 2)) / 500,
  86. 0.001,
  87. float(random.randint(1, 2)) / 500
  88. ]
  89. img = img.transform(size, Image.PERSPECTIVE, params) # 创建扭曲
  90. img = img.filter(ImageFilter.EDGE_ENHANCE_MORE) # 滤镜,边界加强(阈值更大)
  91. return img, strs

这里需要指定 Monaco.ttf 字体:

  1. font_type="static/font/Monaco.ttf",
  2. # https://pan.baidu.com/s/1XwyaFC_MROFA4fXujVwH3A 提取码:17f8

视图函数 views.py

  1. from django.shortcuts import render, redirect, HttpResponse
  2. from blog.check_code import create_validate_code
  3. from io import BytesIO
  4. from django.contrib import auth
  5. from django.http import JsonResponse
  6. def check_code(request):
  7. """
  8. 获取验证码
  9. :param request:
  10. :return:
  11. """
  12. stream = BytesIO()
  13. # 生成图片 img、数字代码 code,保存在内存中,而不是 Django 项目中
  14. img, code = create_validate_code()
  15. img.save(stream, 'PNG')
  16. # 写入 session
  17. request.session['valid_code'] = code
  18. print(code)
  19. return HttpResponse(stream.getvalue())
  20. def login(request):
  21. """
  22. 登录视图
  23. :param request:
  24. :return:
  25. """
  26. if request.method == 'POST':
  27. ret = {'status': False, 'message': None}
  28. username = request.POST.get('username')
  29. password = request.POST.get('password')
  30. # 获取用户输入的验证码
  31. code = request.POST.get('check_code')
  32. p = request.POST.get('p')
  33. # 用户输入的验证码与 session 中取出的验证码比较
  34. if code.upper() == request.session.get('valid_code').upper():
  35. # 验证码正确,验证用户名密码是否正确
  36. user_obj = auth.authenticate(username=username, password=password)
  37. if user_obj:
  38. # 验证通过,则进行登录操作
  39. # 封装到 request.user 中
  40. auth.login(request, user_obj)
  41. return redirect('accounts:home')
  42. else:
  43. ret['status'] = True
  44. ret['message'] = '用户名或密码错误'
  45. return render(request, 'accounts/login.html', ret)
  46. else:
  47. ret['status'] = True
  48. ret['message'] = '验证码错误'
  49. return render(request, 'accounts/login.html', ret)
  50. return render(request, 'accounts/login.html')

登录页面 login.html

  1. {% load static %}
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="UTF-8">
  6. <title>登录</title>
  7. <link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.css' %}">
  8. <style>
  9. .login-col {
  10. margin-top: 100px;
  11. }
  12. </style>
  13. </head>
  14. <body>
  15. <div class="container">
  16. <div class="row">
  17. <div class="well col-md-6 col-md-offset-3 login-col">
  18. <h3 class="text-center">登录</h3>
  19. <!--错误信息-->
  20. {% if status %}
  21. <div class="alert alert-danger" role="alert">
  22. <p id="login-error">{{ message }}</p>
  23. <p id="login-error"></p>
  24. </div>
  25. {% endif %}
  26. <form action="{% url 'accounts:login' %}" method="post" novalidate>
  27. {% csrf_token %}
  28. <div class="form-group">
  29. <label for="exampleInputUsername">用户名:</label>
  30. <input type="text" class="form-control" id="exampleInputUsername" placeholder="用户名" name="username">
  31. </div>
  32. <div class="form-group">
  33. <label for="exampleInputPassword1">密码:</label>
  34. <input type="password" class="form-control" id="exampleInputPassword" placeholder="密码"
  35. name="password">
  36. </div>
  37. <!--验证码-->
  38. <div class="form-group">
  39. <label for="id_code">验证码:</label>
  40. <div class="row">
  41. <div class="col-md-7 col-xs-7">
  42. <input type="text" class="form-control" id="id_code" placeholder="请输入验证码" name="check_code">
  43. </div>
  44. <div class="col-md-5 col-xs-5">
  45. <img src="/accounts/check_code" onclick="changeImg(this)" class="img">
  46. </div>
  47. </div>
  48. </div>
  49. <div class="checkbox">
  50. <label>
  51. <input type="checkbox"> 记住我
  52. </label>
  53. </div>
  54. <button type="submit" class="btn btn-primary btn-block" id="login-button">提交</button>
  55. </form>
  56. </div>
  57. </div>
  58. </div>
  59. <script src="{% static 'js/jquery-3.1.1.js' %}"></script>
  60. <script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.js' %}"></script>
  61. <script>
  62. function changeImg(ths) {
  63. // 硬编码
  64. ths.src = '/accounts/check_code/?temp=' + Math.random();
  65. // 使用命名空间,发送请求
  66. // ths.src = '{% url 'accounts:check_code' %}' + '?temp=' + Math.random();
  67. }
  68. </script>
  69. </body>
  70. </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

  1. from django.urls import path
  2. from accounts import views
  3. app_name = 'accounts'
  4. urlpatterns = [
  5. # 登录
  6. path('login/', views.login, name='login'),
  7. # 获取验证码
  8. path('check_code/', views.check_code, name='check_code'),
  9. # 首页
  10. path('home/', views.home, name='home'),
  11. # 注销
  12. path('logout/', views.logout, name='logout'),
  13. ]
  • 画图程序 check_code.py 保存在项目任意位置即可,只需在视图函数中导入即可。
  • Monaco.ttf 字体不可或缺,放置在静态文件中即可,但是需要修改 check_code.py 中的字体引入路径。
  • 验证用户输入的验证码是否正确,只需从 session 中取出生成的验证码与其比较即可。
  • 验证码刷新,只需让其再发送一次 get 请求即可。

    3. 极验科技之滑动验证码

    除上述两种图片验证码以外,还有一种滑动验证码,用的比较多有 极验科技

    官方下载源码包,并安装 geetest 模块

    官网
    访问官网,选择:技术文档 —— 行为验证 —— 选择服务端部署为 Python —— 使用 git 或直接下载
    gt3-python-sdk 文件。 ```python pip install geetest pip install requests # 有可能还需要 requests 模块
  1. <a name="jMb9e"></a>
  2. ## 登录页面 login2.html
  3. **html 部分**
  4. ```python
  5. {% load static %}
  6. <!DOCTYPE html>
  7. <html lang="en">
  8. <head>
  9. <meta charset="UTF-8">
  10. <title>登录</title>
  11. <link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.css' %}">
  12. <style>
  13. .login-col {
  14. margin-top: 100px;
  15. }
  16. </style>
  17. </head>
  18. <body>
  19. <div class="container">
  20. <div class="row">
  21. <div class="well col-md-6 col-md-offset-3 login-col">
  22. <h3 class="text-center">登录</h3>
  23. <form>
  24. {% csrf_token %}
  25. <div class="form-group">
  26. <label for="username">用户名:</label>
  27. <input type="text" class="form-control" id="username" placeholder="用户名" name="username">
  28. </div>
  29. <div class="form-group">
  30. <label for="password">密码:</label>
  31. <input type="password" class="form-control" id="password" placeholder="密码" name="password">
  32. </div>
  33. <!--极验科技滑动验证码-->
  34. <div class="form-group">
  35. <!-- 放置极验的滑动验证码 -->
  36. <div id="popup-captcha"></div>
  37. </div>
  38. <!--记住我-->
  39. <div class="checkbox">
  40. <label>
  41. <input type="checkbox"> 记住我
  42. </label>
  43. </div>
  44. <!--登录按钮-->
  45. <button type="button" class="btn btn-primary btn-block" id="login-button">提交</button>
  46. <!--错误信息-->
  47. <span class="login-error"></span>
  48. </form>
  49. </div>
  50. </div>
  51. </div>
  52. </body>
  53. </html>
  1. <script src="{% static 'js/jquery-3.3.1.js' %}"></script>
  2. <script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.js' %}"></script>
  3. <!-- 引入封装了failback的接口--initGeetest -->
  4. <script src="http://static.geetest.com/static/tools/gt.js"></script>
  5. <script>
  6. var handlerPopup = function (captchaObj) {
  7. // 成功的回调
  8. captchaObj.onSuccess(function () {
  9. var validate = captchaObj.getValidate();
  10. var username = $('#username').val();
  11. var password = $('#password').val();
  12. console.log(username, password);
  13. $.ajax({
  14. url: "/accounts/login2/", // 进行二次验证
  15. type: "post",
  16. dataType: 'json',
  17. data: {
  18. username: username,
  19. password: password,
  20. csrfmiddlewaretoken: $("[name='csrfmiddlewaretoken']").val(),
  21. geetest_challenge: validate.geetest_challenge,
  22. geetest_validate: validate.geetest_validate,
  23. geetest_seccode: validate.geetest_seccode
  24. },
  25. success: function (data) {
  26. console.log(data);
  27. if (data.status) {
  28. // 有错误,在页面上显示
  29. $('.login-error').text(data.msg);
  30. } else {
  31. // 登录成功
  32. location.href = data.msg;
  33. }
  34. }
  35. });
  36. });
  37. // 当点击登录按钮时,弹出滑动验证码窗口
  38. $("#login-button").click(function () {
  39. captchaObj.show();
  40. });
  41. // 将验证码加到id为captcha的元素里
  42. captchaObj.appendTo("#popup-captcha");
  43. // 更多接口参考:http://www.geetest.com/install/sections/idx-client-sdk.html
  44. };
  45. $('#username, #password').focus(function () {
  46. // 将之前的错误清空
  47. $('.login-error').text('');
  48. });
  49. // 验证开始需要向网站主后台获取id,challenge,success(是否启用failback)
  50. $.ajax({
  51. url: "/accounts/pc-geetest/register?t=" + (new Date()).getTime(), // 加随机数防止缓存
  52. type: "get",
  53. dataType: "json",
  54. success: function (data) {
  55. // 使用initGeetest接口
  56. // 参数1:配置参数
  57. // 参数2:回调,回调的第一个参数验证码对象,之后可以使用它做appendTo之类的事件
  58. initGeetest({
  59. gt: data.gt,
  60. challenge: data.challenge,
  61. product: "popup", // 产品形式,包括:float,embed,popup。注意只对PC版验证码有效
  62. offline: !data.success // 表示用户后台检测极验服务器是否宕机,一般不需要关注
  63. // 更多配置参数请参见:http://www.geetest.com/install/sections/idx-client-sdk.html#config
  64. }, handlerPopup);
  65. }
  66. });
  67. </script>

JS 代码主要分为两部分,第一部分是获取表单的 value 值,向后台发送 Ajax 请求,以验证用户名及密码是否正确,若有错误将错误信息显示出来。第二部分向后台获取验证码所需相关参数。

视图函数 views.py

  1. from django.shortcuts import render, redirect, HttpResponse
  2. from django.http import JsonResponse
  3. from geetest import GeetestLib
  4. def login2(request):
  5. if request.method == 'POST':
  6. ret = {'status': False, 'msg': None}
  7. username = request.POST.get('username')
  8. password = request.POST.get('password')
  9. print(username, password)
  10. # 获取极验,滑动验证码相关参数
  11. gt = GeetestLib(pc_geetest_id, pc_geetest_key)
  12. challenge = request.POST.get(gt.FN_CHALLENGE, '')
  13. validate = request.POST.get(gt.FN_VALIDATE, '')
  14. seccode = request.POST.get(gt.FN_SECCODE, '')
  15. status = request.session[gt.GT_STATUS_SESSION_KEY]
  16. user_id = request.session["user_id"]
  17. print(gt, challenge, validate, seccode, status)
  18. if status:
  19. result = gt.success_validate(challenge, validate, seccode, user_id)
  20. else:
  21. result = gt.failback_validate(challenge, validate, seccode)
  22. if result:
  23. # 验证码正确
  24. # 利用auth模块做用户名和密码的校验
  25. user_obj = auth.authenticate(username=username, password=password)
  26. if user_obj:
  27. # 用户名密码正确
  28. # 给用户做登录
  29. auth.login(request, user_obj)
  30. ret["msg"] = "/accounts/home/"
  31. # return redirect('accounts:home')
  32. else:
  33. # 用户名密码错误
  34. ret["status"] = True
  35. ret["msg"] = "用户名或密码错误!"
  36. else:
  37. ret["status"] = True
  38. ret["msg"] = "验证码错误"
  39. return JsonResponse(ret)
  40. return render(request, "accounts/login2.html")
  41. # 请在官网申请ID使用,示例ID不可使用
  42. pc_geetest_id = "b46d1900d0a894591916ea94ea91bd2c"
  43. pc_geetest_key = "36fc3fe98530eea08dfc6ce76e3d24c4"
  44. # 处理极验 获取验证码的视图
  45. def get_geetest(request):
  46. user_id = 'test'
  47. gt = GeetestLib(pc_geetest_id, pc_geetest_key)
  48. status = gt.pre_process(user_id)
  49. request.session[gt.GT_STATUS_SESSION_KEY] = status
  50. request.session["user_id"] = user_id
  51. response_str = gt.get_response_str()
  52. return HttpResponse(response_str)

路由 accounts/urls.py

  1. from django.urls import path
  2. from accounts import views
  3. app_name = 'accounts'
  4. urlpatterns = [
  5. path('home/', views.home, name='home'),
  6. # 极验滑动验证码 获取验证码的url
  7. path('pc-geetest/register/', views.get_geetest, name='get_geetest'),
  8. path('login2/', views.login2, name='login2'),
  9. ]

总结

  • 极验滑动验证码除了支持 Django,还支持 flask、tornado 等
  • 上述以 Ajax 形式发送的 post 请求,因此注意查看是否设置了 csrf_token,并且提交按钮 button 的提交类型应该为 button 而非 submit (踩坑)
  • 同时它还有嵌入式,移动端等,更多示例请参考下载的官方源码。