返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
Django

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

曦远 (DealiAxy)约 26 分钟
目录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/xxx
  • audio/xxx
  • refresh

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:

challengeresponsehashkeyexpirationid
LOKJlokj286f34637808d669f4fd55ebb1877f72d4ab7fa92022-04-08 15:32:41.32875431
JDNAjdnafb1e57277df26cbd7c20f6a7887f0bed18972e5b2022-04-08 15:32:45.79525932

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

_EOF_

Exploring Captcha Implementation in Django - Encapsulation in the DjangoStarter Project Template

本文作者:
程序设计实验室
版权声明:
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章

DjangoRestFramework框架三种分页功能的实现 - 在DjangoStarter项目模板中封装

前言 继续Django后端开发系列文章。刚好遇到一个分页的需求,就记录一下。 Django作为一个“全家桶”型的框架,本身啥都有,分页组件也是有的,但默认的分页组件没有对API开发做优化,所以DjangoRestFramework这个专门写API的框架又把Django的分页组件包装了一层,集成在viewsets里的时候会更方便。 不过我们不可能一直用viewsets,有一部分API还是要用自由度更

2022-08-26 · Django · 约 1 分钟

Django项目笔记:sessions处理以及复杂对象序列化

前言:一点题外话 我发现,不更新博客的时候,不是非常忙,就是效率非常低。最近没怎么更新博客,原因是第二种= =。惭愧惭愧。 今天效率出奇的高,一天时间把PassNote后端的接口全部写完了,Django很好用,不过实际项目中还是有些框架不能实现的功能需要解决的,我比较喜欢造轮子,不过为了保证效率,还是尽量用现成的工具,减少不必要的折腾。 关于PassNote 之前应该在博客有说过,这是一个密码管理

2022-04-02 · Django · 约 1 分钟

轻量级消息队列Django-Q轻度体验

前言 最近做的这个项目(基于Django),需要做个功能,实现定时采集车辆定位。 这让我想起来几年前那个OneCat项目,当时我用的是Celery这个很重的组件 Celery实在是太重了,后来我做公众号采集平台的时候,又接触了Django-RQ和Django-Q这俩,前者是对RQ的封装,让RQ和Django更好的结合在一起;后者是一个全新的「多进程任务队列」组件,相比起celery很轻量,当时使用

2022-08-31 · Django · 约 1 分钟

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…