feat: Captcha (#2913)

This commit is contained in:
shaohuzhang1 2025-04-17 19:16:54 +08:00 committed by GitHub
parent 063393d659
commit dcb77bbe16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 90 additions and 4 deletions

View File

@ -21,6 +21,8 @@ class Cache_Version(Enum):
# 当前用户所有权限 # 当前用户所有权限
PERMISSION_LIST = "PERMISSION:LIST", lambda user_id: user_id PERMISSION_LIST = "PERMISSION:LIST", lambda user_id: user_id
CAPTCHA = "CAPTCHA", lambda captcha: captcha
def get_version(self): def get_version(self):
return self.value[0] return self.value[0]

View File

@ -7,6 +7,8 @@
@desc: @desc:
""" """
import hashlib import hashlib
import random
import io import io
import mimetypes import mimetypes
import re import re
@ -48,6 +50,13 @@ def group_by(list_source: List, key):
return result return result
CHAR_SET = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
def get_random_chars(number=6):
return "".join([CHAR_SET[random.randint(0, len(CHAR_SET) - 1)] for index in range(number)])
def encryption(message: str): def encryption(message: str):
""" """
加密敏感字段数据 加密方式是 如果密码是 1234567890 那么给前端则是 123******890 加密敏感字段数据 加密方式是 如果密码是 1234567890 那么给前端则是 123******890

View File

@ -102,3 +102,12 @@ msgstr ""
#: .\apps\users\views\user.py:24 .\apps\users\views\user.py:25 #: .\apps\users\views\user.py:24 .\apps\users\views\user.py:25
msgid "Get current user information" msgid "Get current user information"
msgstr "" msgstr ""
msgid "Get captcha"
msgstr ""
msgid "captcha"
msgstr ""
msgid "Captcha code error or expiration"
msgstr ""

View File

@ -103,4 +103,11 @@ msgstr "用户管理"
msgid "Get current user information" msgid "Get current user information"
msgstr "获取当前用户信息" msgstr "获取当前用户信息"
msgid "Get captcha"
msgstr "获取验证码"
msgid "captcha"
msgstr "验证码"
msgid "Captcha code error or expiration"
msgstr "验证码错误或过期"

View File

@ -102,3 +102,12 @@ msgstr "用戶管理"
#: .\apps\users\views\user.py:24 .\apps\users\views\user.py:25 #: .\apps\users\views\user.py:24 .\apps\users\views\user.py:25
msgid "Get current user information" msgid "Get current user information"
msgstr "獲取當前用戶資訊" msgstr "獲取當前用戶資訊"
msgid "Get captcha"
msgstr "獲取驗證碼"
msgid "captcha"
msgstr "驗證碼"
msgid "Captcha code error or expiration"
msgstr "驗證碼錯誤或過期"

View File

@ -9,7 +9,7 @@
from common.mixins.api_mixin import APIMixin from common.mixins.api_mixin import APIMixin
from common.result import ResultSerializer from common.result import ResultSerializer
from users.serializers.login import LoginResponse, LoginRequest from users.serializers.login import LoginResponse, LoginRequest, CaptchaResponse
class ApiLoginResponse(ResultSerializer): class ApiLoginResponse(ResultSerializer):
@ -40,3 +40,14 @@ class LoginAPI(APIMixin):
@staticmethod @staticmethod
def get_response(): def get_response():
return ApiLoginResponse return ApiLoginResponse
class ApiCaptchaResponse(ResultSerializer):
def get_data(self):
return CaptchaResponse()
class CaptchaAPI(APIMixin):
@staticmethod
def get_response():
return ApiCaptchaResponse

View File

@ -6,8 +6,10 @@
@date2025/4/14 11:08 @date2025/4/14 11:08
@desc: @desc:
""" """
import base64
import datetime import datetime
from captcha.image import ImageCaptcha
from django.core import signing from django.core import signing
from django.core.cache import cache from django.core.cache import cache
from django.db.models import QuerySet from django.db.models import QuerySet
@ -17,13 +19,14 @@ from rest_framework import serializers
from common.constants.authentication_type import AuthenticationType from common.constants.authentication_type import AuthenticationType
from common.constants.cache_version import Cache_Version from common.constants.cache_version import Cache_Version
from common.exception.app_exception import AppApiException from common.exception.app_exception import AppApiException
from common.utils.common import password_encrypt from common.utils.common import password_encrypt, get_random_chars
from users.models import User from users.models import User
class LoginRequest(serializers.Serializer): class LoginRequest(serializers.Serializer):
username = serializers.CharField(required=True, max_length=64, help_text=_("Username"), label=_("Username")) username = serializers.CharField(required=True, max_length=64, help_text=_("Username"), label=_("Username"))
password = serializers.CharField(required=True, max_length=128, label=_("Password")) password = serializers.CharField(required=True, max_length=128, label=_("Password"))
captcha = serializers.CharField(required=True, max_length=64, label=_('captcha'))
class LoginResponse(serializers.Serializer): class LoginResponse(serializers.Serializer):
@ -40,6 +43,11 @@ class LoginSerializer(serializers.Serializer):
LoginRequest(data=instance).is_valid(raise_exception=True) LoginRequest(data=instance).is_valid(raise_exception=True)
username = instance.get('username') username = instance.get('username')
password = instance.get('password') password = instance.get('password')
captcha = instance.get('captcha')
captcha_cache = cache.get(Cache_Version.CAPTCHA.get_key(captcha=captcha),
version=Cache_Version.CAPTCHA.get_version())
if captcha_cache is None:
raise AppApiException(1005, _("Captcha code error or expiration"))
user = QuerySet(User).filter(username=username, password=password_encrypt(password)).first() user = QuerySet(User).filter(username=username, password=password_encrypt(password)).first()
if user is None: if user is None:
raise AppApiException(500, _('The username or password is incorrect')) raise AppApiException(500, _('The username or password is incorrect'))
@ -52,3 +60,22 @@ class LoginSerializer(serializers.Serializer):
version, get_key = Cache_Version.TOKEN.value version, get_key = Cache_Version.TOKEN.value
cache.set(get_key(token), user, timeout=datetime.timedelta(seconds=60 * 60 * 2).seconds, version=version) cache.set(get_key(token), user, timeout=datetime.timedelta(seconds=60 * 60 * 2).seconds, version=version)
return {'token': token} return {'token': token}
class CaptchaResponse(serializers.Serializer):
"""
登录响应对象
"""
captcha = serializers.CharField(required=True, label=_("captcha"))
class CaptchaSerializer(serializers.Serializer):
@staticmethod
def generate():
chars = get_random_chars()
image = ImageCaptcha()
data = image.generate(chars)
captcha = base64.b64encode(data.getbuffer())
cache.set(Cache_Version.CAPTCHA.get_key(captcha=chars), chars,
timeout=60, version=Cache_Version.CAPTCHA.get_version())
return {'captcha': 'data:image/png;base64,' + captcha.decode()}

View File

@ -6,6 +6,7 @@ app_name = "user"
urlpatterns = [ urlpatterns = [
path('user/login', views.LoginView.as_view(), name='login'), path('user/login', views.LoginView.as_view(), name='login'),
path('user/profile', views.UserProfileView.as_view(), name="user_profile"), path('user/profile', views.UserProfileView.as_view(), name="user_profile"),
path('user/captcha', views.CaptchaView.as_view(), name='captcha'),
path('user/test', views.TestPermissionsUserView.as_view(), name="test"), path('user/test', views.TestPermissionsUserView.as_view(), name="test"),
path('workspace/<str:workspace_id>/user/profile', views.TestWorkspacePermissionUserView.as_view(), path('workspace/<str:workspace_id>/user/profile', views.TestWorkspacePermissionUserView.as_view(),
name="test_workspace_id_permission") name="test_workspace_id_permission")

View File

@ -12,8 +12,8 @@ from rest_framework.request import Request
from rest_framework.views import APIView from rest_framework.views import APIView
from common import result from common import result
from users.api.login import LoginAPI from users.api.login import LoginAPI, CaptchaAPI
from users.serializers.login import LoginSerializer from users.serializers.login import LoginSerializer, CaptchaSerializer
class LoginView(APIView): class LoginView(APIView):
@ -25,3 +25,13 @@ class LoginView(APIView):
responses=LoginAPI.get_response()) responses=LoginAPI.get_response())
def post(self, request: Request): def post(self, request: Request):
return result.success(LoginSerializer().login(request.data)) return result.success(LoginSerializer().login(request.data))
class CaptchaView(APIView):
@extend_schema(methods=['GET'],
description=_("Get captcha"),
operation_id=_("Get captcha"),
tags=[_("User management")],
responses=CaptchaAPI.get_response())
def get(self, request: Request):
return result.success(CaptchaSerializer().generate())

View File

@ -15,6 +15,7 @@ psycopg = { extras = ["binary"], version = "3.2.6" }
python-dotenv = "1.1.0" python-dotenv = "1.1.0"
uuid-utils = "0.10.0" uuid-utils = "0.10.0"
diskcache2 = "0.1.2" diskcache2 = "0.1.2"
captcha = "0.7.1"
langchain-openai = "^0.3.0" langchain-openai = "^0.3.0"
langchain-anthropic = "^0.3.0" langchain-anthropic = "^0.3.0"
langchain-community = "^0.3.0" langchain-community = "^0.3.0"