Skip to content

Commit dcb77bb

Browse files
authored
feat: Captcha (#2913)
1 parent 063393d commit dcb77bb

File tree

10 files changed

+90
-4
lines changed

10 files changed

+90
-4
lines changed

apps/common/constants/cache_version.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class Cache_Version(Enum):
2121
# 当前用户所有权限
2222
PERMISSION_LIST = "PERMISSION:LIST", lambda user_id: user_id
2323

24+
CAPTCHA = "CAPTCHA", lambda captcha: captcha
25+
2426
def get_version(self):
2527
return self.value[0]
2628

apps/common/utils/common.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
@desc:
88
"""
99
import hashlib
10+
11+
import random
1012
import io
1113
import mimetypes
1214
import re
@@ -48,6 +50,13 @@ def group_by(list_source: List, key):
4850
return result
4951

5052

53+
54+
CHAR_SET = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
55+
56+
57+
def get_random_chars(number=6):
58+
return "".join([CHAR_SET[random.randint(0, len(CHAR_SET) - 1)] for index in range(number)])
59+
5160
def encryption(message: str):
5261
"""
5362
加密敏感字段数据 加密方式是 如果密码是 1234567890 那么给前端则是 123******890

apps/locales/en_US/LC_MESSAGES/django.po

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,12 @@ msgstr ""
102102
#: .\apps\users\views\user.py:24 .\apps\users\views\user.py:25
103103
msgid "Get current user information"
104104
msgstr ""
105+
106+
msgid "Get captcha"
107+
msgstr ""
108+
109+
msgid "captcha"
110+
msgstr ""
111+
112+
msgid "Captcha code error or expiration"
113+
msgstr ""

apps/locales/zh_CN/LC_MESSAGES/django.po

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,11 @@ msgstr "用户管理"
103103
msgid "Get current user information"
104104
msgstr "获取当前用户信息"
105105

106+
msgid "Get captcha"
107+
msgstr "获取验证码"
106108

109+
msgid "captcha"
110+
msgstr "验证码"
111+
112+
msgid "Captcha code error or expiration"
113+
msgstr "验证码错误或过期"

apps/locales/zh_Hant/LC_MESSAGES/django.po

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,12 @@ msgstr "用戶管理"
102102
#: .\apps\users\views\user.py:24 .\apps\users\views\user.py:25
103103
msgid "Get current user information"
104104
msgstr "獲取當前用戶資訊"
105+
106+
msgid "Get captcha"
107+
msgstr "獲取驗證碼"
108+
109+
msgid "captcha"
110+
msgstr "驗證碼"
111+
112+
msgid "Captcha code error or expiration"
113+
msgstr "驗證碼錯誤或過期"

apps/users/api/login.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from common.mixins.api_mixin import APIMixin
1111
from common.result import ResultSerializer
12-
from users.serializers.login import LoginResponse, LoginRequest
12+
from users.serializers.login import LoginResponse, LoginRequest, CaptchaResponse
1313

1414

1515
class ApiLoginResponse(ResultSerializer):
@@ -40,3 +40,14 @@ def get_request():
4040
@staticmethod
4141
def get_response():
4242
return ApiLoginResponse
43+
44+
45+
class ApiCaptchaResponse(ResultSerializer):
46+
def get_data(self):
47+
return CaptchaResponse()
48+
49+
50+
class CaptchaAPI(APIMixin):
51+
@staticmethod
52+
def get_response():
53+
return ApiCaptchaResponse

apps/users/serializers/login.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
@date:2025/4/14 11:08
77
@desc:
88
"""
9+
import base64
910
import datetime
1011

12+
from captcha.image import ImageCaptcha
1113
from django.core import signing
1214
from django.core.cache import cache
1315
from django.db.models import QuerySet
@@ -17,13 +19,14 @@
1719
from common.constants.authentication_type import AuthenticationType
1820
from common.constants.cache_version import Cache_Version
1921
from common.exception.app_exception import AppApiException
20-
from common.utils.common import password_encrypt
22+
from common.utils.common import password_encrypt, get_random_chars
2123
from users.models import User
2224

2325

2426
class LoginRequest(serializers.Serializer):
2527
username = serializers.CharField(required=True, max_length=64, help_text=_("Username"), label=_("Username"))
2628
password = serializers.CharField(required=True, max_length=128, label=_("Password"))
29+
captcha = serializers.CharField(required=True, max_length=64, label=_('captcha'))
2730

2831

2932
class LoginResponse(serializers.Serializer):
@@ -40,6 +43,11 @@ def login(instance):
4043
LoginRequest(data=instance).is_valid(raise_exception=True)
4144
username = instance.get('username')
4245
password = instance.get('password')
46+
captcha = instance.get('captcha')
47+
captcha_cache = cache.get(Cache_Version.CAPTCHA.get_key(captcha=captcha),
48+
version=Cache_Version.CAPTCHA.get_version())
49+
if captcha_cache is None:
50+
raise AppApiException(1005, _("Captcha code error or expiration"))
4351
user = QuerySet(User).filter(username=username, password=password_encrypt(password)).first()
4452
if user is None:
4553
raise AppApiException(500, _('The username or password is incorrect'))
@@ -52,3 +60,22 @@ def login(instance):
5260
version, get_key = Cache_Version.TOKEN.value
5361
cache.set(get_key(token), user, timeout=datetime.timedelta(seconds=60 * 60 * 2).seconds, version=version)
5462
return {'token': token}
63+
64+
65+
class CaptchaResponse(serializers.Serializer):
66+
"""
67+
登录响应对象
68+
"""
69+
captcha = serializers.CharField(required=True, label=_("captcha"))
70+
71+
72+
class CaptchaSerializer(serializers.Serializer):
73+
@staticmethod
74+
def generate():
75+
chars = get_random_chars()
76+
image = ImageCaptcha()
77+
data = image.generate(chars)
78+
captcha = base64.b64encode(data.getbuffer())
79+
cache.set(Cache_Version.CAPTCHA.get_key(captcha=chars), chars,
80+
timeout=60, version=Cache_Version.CAPTCHA.get_version())
81+
return {'captcha': 'data:image/png;base64,' + captcha.decode()}

apps/users/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
urlpatterns = [
77
path('user/login', views.LoginView.as_view(), name='login'),
88
path('user/profile', views.UserProfileView.as_view(), name="user_profile"),
9+
path('user/captcha', views.CaptchaView.as_view(), name='captcha'),
910
path('user/test', views.TestPermissionsUserView.as_view(), name="test"),
1011
path('workspace/<str:workspace_id>/user/profile', views.TestWorkspacePermissionUserView.as_view(),
1112
name="test_workspace_id_permission")

apps/users/views/login.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
from rest_framework.views import APIView
1313

1414
from common import result
15-
from users.api.login import LoginAPI
16-
from users.serializers.login import LoginSerializer
15+
from users.api.login import LoginAPI, CaptchaAPI
16+
from users.serializers.login import LoginSerializer, CaptchaSerializer
1717

1818

1919
class LoginView(APIView):
@@ -25,3 +25,13 @@ class LoginView(APIView):
2525
responses=LoginAPI.get_response())
2626
def post(self, request: Request):
2727
return result.success(LoginSerializer().login(request.data))
28+
29+
30+
class CaptchaView(APIView):
31+
@extend_schema(methods=['GET'],
32+
description=_("Get captcha"),
33+
operation_id=_("Get captcha"),
34+
tags=[_("User management")],
35+
responses=CaptchaAPI.get_response())
36+
def get(self, request: Request):
37+
return result.success(CaptchaSerializer().generate())

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ psycopg = { extras = ["binary"], version = "3.2.6" }
1515
python-dotenv = "1.1.0"
1616
uuid-utils = "0.10.0"
1717
diskcache2 = "0.1.2"
18+
captcha = "0.7.1"
1819
langchain-openai = "^0.3.0"
1920
langchain-anthropic = "^0.3.0"
2021
langchain-community = "^0.3.0"

0 commit comments

Comments
 (0)