Exploring Captcha Implementation in Django - Encapsulation in the DjangoStarter Project Template
Introduction This is still about the project I've been working on recently. Using Django as the backend, when submitting information from the App, a captcha is needed to prevent users from submitting indiscriminately. My DjangoStarter project scaffold happens to have captcha functionality encapsulated as well, but I found that it seems to only use captcha as a verification method for admin backend login, and doesn't provide captcha-related APIs for frontend-backend separated projects. So while this article introduces captcha implementation, it also wraps the DjangoStarter captcha module to make it more convenient to use~ Which
目录9 个章节
Introduction
This is still about the project I've been working on recently. Using Django as the backend, when submitting information from the App, a captcha is needed to prevent users from submitting indiscriminately. My DjangoStarter project scaffold happens to have captcha functionality encapsulated as well, but I found that it seems to only use captcha as a verification method for admin backend login, and doesn't provide captcha-related APIs for frontend-backend separated projects.
So while this article introduces captcha implementation, it also wraps the captcha module of DjangoStarter to make it more convenient to use~
Which library should we use?
The Zen of Python: Life is short, don't reinvent the wheel
——Lu Xun: I said that
In DjangoStarter I chose django-simple-captcha and django-multi-captcha-admin. The former provides captcha generation and storage; the latter can integrate captcha into the Django Admin login page.
Getting started
So now we have the foundation for implementing captcha functionality. How do we get the captcha on the frontend?
First, since the django-simple-captcha library is supposed to provide captcha functionality, it must have related APIs, right? Let's check the official docs? no, this documentation is way too sparse
Forget it, let's just look at the source code
When adding this library to the project, you need to configure this route:
path('captcha/', include('captcha.urls'))
So let's start by looking at the routes (captcha.urls)
Its route configuration code looks like this
urlpatterns = [
url(
r"image/(?P<key>\w+)/$",
views.captcha_image,
name="captcha-image",
kwargs={"scale": 1},
),
url(
r"image/(?P<key>\w+)@2/$",
views.captcha_image,
name="captcha-image-2x",
kwargs={"scale": 2},
),
url(r"audio/(?P<key>\w+).wav$", views.captcha_audio, name="captcha-audio"),
url(r"refresh/$", views.captcha_refresh, name="captcha-refresh"),
]
You can see there are three link formats:
image/xxxaudio/xxxrefresh
Trying it out
Obviously, the one for refreshing the captcha is this last refresh
Then I tried accessing captcha/refresh/ in Postman, and got a 404 right away
What? This URL clearly exists, what's going on
Have to keep looking at the source code
Let's look directly at the source of this views.captcha_refresh() method!
def captcha_refresh(request):
""" Return json with new captcha for ajax refresh request """
if not request.headers.get('x-requested-with') == 'XMLHttpRequest':
raise Http404
new_key = CaptchaStore.pick()
to_json_response = {
"key": new_key,
"image_url": captcha_image_url(new_key),
"audio_url": captcha_audio_url(new_key) if settings.CAPTCHA_FLITE_PATH else None,
}
return HttpResponse(json.dumps(to_json_response), content_type="application/json")
Then I saw this in the source:
if not request.headers.get('x-requested-with') == 'XMLHttpRequest':
raise Http404
What a trap!
What era is this, still pulling that jQuery Ajax stuff?
Definitely abandoning this!
Oh no, abandoning it is out of the question. Why not use what's already there? I'll just wrap it myself, right?
Wrapping a new module
Create a new Python Package under the contrib directory, let's just call it captcha
Then edit the contrib/captcha/__init__.py file
from captcha.conf import settings
from captcha.models import CaptchaStore
from captcha.helpers import captcha_audio_url, captcha_image_url
class CaptchaItem(object):
def __init__(self, key, image_url, audio_url):
self.key = key
self.image_url = image_url
self.audio_url = audio_url
def refresh() -> CaptchaItem:
"""
获取新的验证码
:return:
"""
key = CaptchaStore.pick()
return CaptchaItem(
key,
captcha_image_url(key),
captcha_audio_url(key) if settings.CAPTCHA_FLITE_PATH else None,
)
def verify(key: str, code: str) -> bool:
"""
检查输入的验证码是否正确
:param key:
:param code:
:return:
"""
# 清理过期的验证码记录
CaptchaStore.remove_expired()
try:
CaptchaStore.objects.get(response=code, hashkey=key).delete()
return True
except CaptchaStore.DoesNotExist:
return False
The comments in the code are clear enough, I don't need to explain, haha
Writing a new captcha API
As everyone knows, DjangoStarter has a default app apps.core, so we'll just write the captcha API in this app
Add code in apps/core/views.py
from drf_yasg2.utils import swagger_auto_schema
from rest_framework import permissions
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
@swagger_auto_schema(method='get', operation_summary='刷新验证码')
@permission_classes([permissions.AllowAny])
@api_view()
def refresh_captcha(request):
from contrib import captcha
captcha_item = captcha.refresh()
return Response({
"key": captcha_item.key,
"image_url": captcha_item.image_url,
"audio_url": captcha_item.audio_url,
})
Then edit apps/core/urls.py and add the route configuration
from . import views
urlpatterns = [
...
path('refresh_captcha', views.refresh_captcha),
]
OK, all done~!
Let's give it a test. Request this API in Swagger or Postman: core/refresh_captcha, and you get
{
"message": "请求成功",
"code": 200,
"data": {
"key": "f5275573b0715d2fa9613a73f80a4161ed759061",
"image_url": "/captcha/image/f5275573b0715d2fa9613a73f80a4161ed759061/",
"audio_url": null
}
}
Besides the captcha image URL we expected, the result also includes a key. When the client submits the captcha entered by the user, it needs to submit the key as well so the server can verify whether the submission is valid.
Checking whether the captcha matches
We've got fetching the captcha. Next we need to check whether the captcha entered by the user is correct
In the earlier wrapper, we already wrote the verify function. Just pass in the captcha key and the user-entered code~
If correct it returns True and deletes this captcha record; if it doesn't exist or is wrong, it returns False.
Here's an example. This API uses the POST method, with parameters in FormData
from rest_framework import status
from rest_framework.response import Response
from drf_yasg2.utils import swagger_auto_schema
from drf_yasg2 import openapi
from contrib import captcha
@swagger_auto_schema(
method='post',
operation_summary='检查验证码',
manual_parameters=[
openapi.Parameter('code', openapi.IN_FORM, type=openapi.TYPE_STRING, description='验证码'),
openapi.Parameter('key', openapi.IN_FORM, type=openapi.TYPE_STRING, description='验证key'),
]
)
@api_view()
def verify_captcha(request):
code = request.POST.get('code')
key = request.POST.get('key')
if not (code and key):
return Response({'message': '请输入验证码'}, status=status.HTTP_400_BAD_REQUEST)
if captcha.verify(key, code):
return Response({'message': '验证码输入正确'})
else:
return Response({'message': '验证码错误'}, status=status.HTTP_403_FORBIDDEN)
Advanced usage
What was introduced above is just the most basic usage. You can customize captcha generation behavior based on actual needs, such as manually specifying the captcha expiration period
To customize, you first need to understand the captcha generation process
Let's first look at what the database table looks like:
| challenge | response | hashkey | expiration | id |
|---|---|---|---|---|
| LOKJ | lokj | 286f34637808d669f4fd55ebb1877f72d4ab7fa9 | 2022-04-08 15:32:41.328754 | 31 |
| JDNA | jdna | fb1e57277df26cbd7c20f6a7887f0bed18972e5b | 2022-04-08 15:32:45.795259 | 32 |
You can see there are five fields, among which the expiration field specifies the expiration time
When wrapping the captcha generation method earlier, you can see that generation calls the CaptchaStore.pick() method
Actually this CaptchaStore is a Django Model defined by the django-simple-captcha library. The author defined the pick class method on this model to generate captchas. Let's look at the source implementation
@classmethod
def pick(cls):
if not captcha_settings.CAPTCHA_GET_FROM_POOL:
return cls.generate_key()
def fallback():
logger.error("Couldn't get a captcha from pool, generating")
return cls.generate_key()
# Pick up a random item from pool
minimum_expiration = timezone.now() + datetime.timedelta(
minutes=int(captcha_settings.CAPTCHA_GET_FROM_POOL_TIMEOUT)
)
store = (
cls.objects.filter(expiration__gt=minimum_expiration).order_by("?").first()
)
return (store and store.hashkey) or fallback()
Note the line minimum_expiration = timezone.now() + datetime.timedelta — its purpose is to read the expiration time from the configuration.
So we don't actually need to fuss over it; we can just configure it in the settings.
However, note that captcha_settings is imported as: from captcha.conf import settings as captcha_settings
It wraps Django's settings in an extra layer.
I won't go into the source code in detail.
Anyway, we just need to set CAPTCHA_GET_FROM_POOL_TIMEOUT=10 in Django's settings. Note that the time unit is minutes.
References
- Django Simple Captcha project URL: https://github.com/mbi/django-simple-captcha
- Django Simple Captcha documentation: http://django-simple-captcha.readthedocs.org/en/latest/
_EOF_
Exploring Captcha Implementation in Django - Encapsulation in the DjangoStarter Project Template
- 本文作者:
- 程序设计实验室
- 版权声明:
- 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
相关文章
聊聊Django应用的部署和性能的那些事儿
前言 随着工作的深入,我越来越发现Python Web开发中有很多坑,也一直在羡慕AspNetCore和Go等的可执行文件部署和高性能,以及Spring生态的丰富,不过因为工作用了Django,生活还是要继续的嘛,这Django好歹也是有很大份额的Web框架,也没那么不堪,至少开发速度上就吊打一众框架了~ 在之前的文章里我介绍过使用Docker部署Django应用的方法,不过那种部署方式只适合上线
2022-05-27 · Django · 约 1 分钟
Django+Taro项目实现企业微信登录
前言 还是最近在做的一个小项目,后端用的是Django搭配RestFramework做接口,前端第一次尝试用京东开源的Taro框架来做多端(目前需要做用于企业微信的H5端和微信小程序) 本文记录一下企业微信登录的流程,上周看文档看得头晕晕的,其实很简单,封装好了就几行代码的事~ 两种方式 一种是先拼接好登录要用的地址authorize_url,回调地址设置成h5应用的网页入口,然后把authori
2022-04-08 · Django · 约 1 分钟
实测 Django 6.0:模版片段、后台任务、CSP 安全,三大特性体验报告
呵,连WebSocket都没塞进去就敢叫大版本更新?不过模版片段和内置后台任务总算让Django像个现代框架了,至少写HTMX时不用再到处塞碎片文件。原生CSP?早该有了。至于抛弃Python 3.11——跟不上时代的版本留着也是累赘。
2026-01-20 · Django · 约 1 分钟
Django中「强行」使用MVVM设计模式
扫盲 首先带大家了解一下什么是MVVM模式: 什么是MVVM?MVVM是Model-View-ViewModel的缩写。 MVVM是MVC的增强版,实质上和MVC没有本质区别,只是代码的位置变动而已 从名字上看,MVVM比MVC架构中多了一个ViewModel,没错,就是这个ViewModel,他是MVVM相对于MVC改进的核心思想。在开发过程中,由于需求的变更或添加,项目的复杂度越来越高,代码量
2021-12-18 · Django · 约 1 分钟
讨论
共 0 条加载评论中…