探索Django驗(yàn)證碼功能的實(shí)現(xiàn) - DjangoStarter項(xiàng)目模板里的封裝
前言
依然是最近在做的這個(gè)項(xiàng)目,用Django做后端,App上提交信息的時(shí)候需要一個(gè)驗(yàn)證碼來(lái)防止用戶亂提交,正好我的「DjangoStarter」項(xiàng)目腳手架也有封裝了驗(yàn)證碼功能,不過(guò)我發(fā)現(xiàn)好像里面只是把驗(yàn)證碼作為admin后臺(tái)登錄的校驗(yàn)手段,并沒(méi)有給出前后端分離項(xiàng)目的驗(yàn)證碼相關(guān)接口。
所以本文介紹驗(yàn)證碼功能實(shí)現(xiàn)的同時(shí),也對(duì)「DjangoStarter」的驗(yàn)證碼模塊做一層封裝,使其更方便使用~
用哪個(gè)庫(kù)好呢
Python之禪:人生苦短,不造輪子
——魯迅:我說(shuō)的
我在「DjangoStarter」里選擇的是django-simple-captcha和django-multi-captcha-admin這倆庫(kù),前者提供生成、存儲(chǔ)驗(yàn)證碼的功能;后者可以將驗(yàn)證碼集成到Django Admin的登錄頁(yè)面里。
開(kāi)始
所以我們現(xiàn)在具備了實(shí)現(xiàn)驗(yàn)證碼功能的基礎(chǔ),那么該如何在前端獲取驗(yàn)證碼呢?
首先django-simple-captcha這個(gè)庫(kù)既然是要提供驗(yàn)證碼功能,那肯定有相關(guān)接口吧,來(lái)看看官網(wǎng)文檔?no,這文檔也太簡(jiǎn)陋了
算了,直接看源碼吧
在添加這個(gè)庫(kù)到項(xiàng)目里的時(shí)候,需要配置這個(gè)路由:
path('captcha/',?include('captcha.urls'))
那我們就從路由(captcha.urls)開(kāi)始看
它的路由配置代碼是這樣的
urlpatterns?=?[
????url(
????????r"image/(?P\w+)/$" ,
????????views.captcha_image,
????????name="captcha-image",
????????kwargs={"scale":?1},
????),
????url(
????????r"image/(?P\w+)@2/$" ,
????????views.captcha_image,
????????name="captcha-image-2x",
????????kwargs={"scale":?2},
????),
????url(r"audio/(?P\w+).wav$" ,?views.captcha_audio,?name="captcha-audio"),
????url(r"refresh/$",?views.captcha_refresh,?name="captcha-refresh"),
]
可以看到有三種鏈接形式,分別是
image/xxxaudio/xxxrefresh
嘗試
那很顯然,刷新驗(yàn)證碼的就是最后這個(gè)refresh
然后我試著在Postman里訪問(wèn)captcha/refresh/,發(fā)現(xiàn)直接報(bào)404
What?這個(gè)鏈接明明存在的,咋回事
只能繼續(xù)看看源碼了
直接看這個(gè) views.captcha_refresh() 方法的源碼!
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")
然后在源碼里面看到了這個(gè):
if?not?request.headers.get('x-requested-with')?==?'XMLHttpRequest':?
????raise?Http404
坑爹??!
什么年代了,還搞jQuery的Ajax那一套是吧?
果斷棄坑!
哦不,棄坑是不可能的,有現(xiàn)成的東西為啥不用,我直接自己重新封裝一個(gè)不就好了?
重新封裝一個(gè)模塊
在contrib目錄下創(chuàng)建一個(gè)新的Python Package,名字就叫captcha好了
然后編輯contrib/captcha/__init__.py文件
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:
????"""
????獲取新的驗(yàn)證碼
????: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:
????"""
????檢查輸入的驗(yàn)證碼是否正確
????:param?key:
????:param?code:
????:return:
????"""
????#?清理過(guò)期的驗(yàn)證碼記錄
????CaptchaStore.remove_expired()
????try:
????????CaptchaStore.objects.get(response=code,?hashkey=key).delete()
????????return?True
????except?CaptchaStore.DoesNotExist:
????????return?False
代碼里面注釋很清楚了,我可以不用解釋了,哈哈
寫(xiě)個(gè)新的驗(yàn)證碼接口
眾所周知,「DjangoStarter」有一個(gè)默認(rèn)的應(yīng)用apps.core,那我們就把驗(yàn)證碼的接口寫(xiě)在這個(gè)app里面就好了
在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='刷新驗(yàn)證碼')
@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,
????})
然后編輯apps/core/urls.py,添加一下路由配置
from?.?import?views
urlpatterns?=?[
????...
????path('refresh_captcha',?views.refresh_captcha),
]
OK搞定啦~!
測(cè)試一下看看,在Swagger或者Postman里請(qǐng)求一下這個(gè)接口:core/refresh_captcha,得到結(jié)果
{
??"message":?"請(qǐng)求成功",
??"code":?200,
??"data":?{
????"key":?"f5275573b0715d2fa9613a73f80a4161ed759061",
????"image_url":?"/captcha/image/f5275573b0715d2fa9613a73f80a4161ed759061/",
????"audio_url":?null
??}
}
結(jié)果里除了我們期待的驗(yàn)證碼圖片地址,還有一個(gè)key,客戶端在提交用戶輸入的驗(yàn)證碼時(shí),要把key一并提交,服務(wù)端才能驗(yàn)證這個(gè)提交是否有效。
檢查驗(yàn)證碼是否匹配
獲取驗(yàn)證碼有了,接下來(lái)要做的是檢查用戶輸入的驗(yàn)證碼是否正確
在前面的封裝里,我們已經(jīng)寫(xiě)了verify函數(shù),只需要傳入驗(yàn)證碼的key和用戶輸入的code就好~
正確的話會(huì)返回True,并且把這條驗(yàn)證碼的記錄刪除,不存在或者錯(cuò)誤的話返回False。
來(lái)一個(gè)例子吧,這個(gè)接口使用的是POST方法,參數(shù)在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='檢查驗(yàn)證碼',
????manual_parameters=[
????????openapi.Parameter('code',?openapi.IN_FORM,?type=openapi.TYPE_STRING,?description='驗(yàn)證碼'),
????????openapi.Parameter('key',?openapi.IN_FORM,?type=openapi.TYPE_STRING,?description='驗(yàn)證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':?'請(qǐng)輸入驗(yàn)證碼'},?status=status.HTTP_400_BAD_REQUEST)
????if?captcha.verify(key,?code):
????????return?Response({'message':?'驗(yàn)證碼輸入正確'})
????else:
????????return?Response({'message':?'驗(yàn)證碼錯(cuò)誤'},?status=status.HTTP_403_FORBIDDEN)
高級(jí)用法
前面介紹的只是最基礎(chǔ)的用法,可以根據(jù)實(shí)際需求來(lái)自定義生成驗(yàn)證碼的行為,比如手動(dòng)指定驗(yàn)證碼有效期之類的
要自定義的話,首先得了解驗(yàn)證碼生成的過(guò)程
先來(lái)看看數(shù)據(jù)庫(kù)表是什么樣的:
| 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 |
可以看到有五個(gè)字段,其中expiration字段就是指定過(guò)期時(shí)間了
之前封裝生成驗(yàn)證碼方法的時(shí)候,可以看到生成的時(shí)候是調(diào)用CaptchaStore.pick()這個(gè)方法
其實(shí)這個(gè)CaptchaStore是django-simple-captcha這個(gè)庫(kù)定義的一個(gè)Django Model,作者在這個(gè)model里定義了pick這個(gè)類方法(class method)來(lái)生成驗(yàn)證碼,我們來(lái)看看源碼實(shí)現(xiàn)
@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()
注意minimum_expiration = timezone.now() + datetime.timedelta這行代碼,它的作用是從配置中讀取過(guò)期時(shí)間
所以我們其實(shí)也不用折騰,直接在設(shè)置里配置一下就好了
不過(guò)注意這里面captcha_settings的引入方式是:from captcha.conf import settings as captcha_settings
它是對(duì)Django的settings包裝了一層
具體源碼就不展開(kāi)了
反正我們?cè)贒jango的settings里面配置CAPTCHA_GET_FROM_POOL_TIMEOUT=10就好了,注意時(shí)間單位是分鐘
參考資料
Django Simple Captcha項(xiàng)目地址:https://github.com/mbi/django-simple-captcha Django Simple Captcha文檔:http://django-simple-captcha.readthedocs.org/en/latest/
