feat: Login and add graphic captcha (#3117)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run

This commit is contained in:
shaohuzhang1 2025-05-20 20:16:36 +08:00 committed by GitHub
parent 1ba8077e95
commit c1ddec1a61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 161 additions and 17 deletions

View File

@ -11,6 +11,7 @@ import importlib
import io
import mimetypes
import pickle
import random
import re
import shutil
from functools import reduce
@ -297,3 +298,10 @@ def markdown_to_plain_text(md: str) -> str:
# 去除首尾空格
text = text.strip()
return text
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)])

View File

@ -7491,3 +7491,12 @@ msgstr ""
msgid "Generate related"
msgstr ""
msgid "Obtain graphical captcha"
msgstr ""
msgid "Captcha code error or expiration"
msgstr ""
msgid "captcha"
msgstr ""

View File

@ -7654,3 +7654,12 @@ msgstr "字段: {name} 未设置值"
msgid "Generate related"
msgstr "生成问题"
msgid "Obtain graphical captcha"
msgstr "获取图形验证码"
msgid "Captcha code error or expiration"
msgstr "验证码错误或过期"
msgid "captcha"
msgstr "验证码"

View File

@ -7664,3 +7664,12 @@ msgstr "欄位: {name} 未設定值"
msgid "Generate related"
msgstr "生成問題"
msgid "Obtain graphical captcha"
msgstr "獲取圖形驗證碼"
msgid "Captcha code error or expiration"
msgstr "驗證碼錯誤或過期"
msgid "captcha"
msgstr "驗證碼"

View File

@ -126,6 +126,10 @@ CACHES = {
"token_cache": {
'BACKEND': 'common.cache.file_cache.FileCache',
'LOCATION': os.path.join(PROJECT_DIR, 'data', 'cache', "token_cache") # 文件夹路径
},
'captcha_cache': {
'BACKEND': 'common.cache.file_cache.FileCache',
'LOCATION': os.path.join(PROJECT_DIR, 'data', 'cache', "captcha_cache") # 文件夹路径
}
}

View File

@ -6,18 +6,22 @@
@date2023/9/5 16:32
@desc:
"""
import base64
import datetime
import os
import random
import re
import uuid
from captcha.image import ImageCaptcha
from django.conf import settings
from django.core import validators, signing, cache
from django.core.mail import send_mail
from django.core.mail.backends.smtp import EmailBackend
from django.db import transaction
from django.db.models import Q, QuerySet, Prefetch
from django.utils.translation import get_language
from django.utils.translation import gettext_lazy as _, to_locale
from drf_yasg import openapi
from rest_framework import serializers
@ -30,7 +34,7 @@ from common.exception.app_exception import AppApiException
from common.mixins.api_mixin import ApiMixin
from common.models.db_model_manage import DBModelManage
from common.response.result import get_api_response
from common.util.common import valid_license
from common.util.common import valid_license, get_random_chars
from common.util.field_message import ErrMessage
from common.util.lock import lock
from dataset.models import DataSet, Document, Paragraph, Problem, ProblemParagraphMapping
@ -39,9 +43,29 @@ from function_lib.models.function import FunctionLib
from setting.models import Team, SystemSetting, SettingType, Model, TeamMember, TeamMemberPermission
from smartdoc.conf import PROJECT_DIR
from users.models.user import User, password_encrypt, get_user_dynamics_permission
from django.utils.translation import gettext_lazy as _, gettext, to_locale
from django.utils.translation import get_language
user_cache = cache.caches['user_cache']
captcha_cache = cache.caches['captcha_cache']
class CaptchaSerializer(ApiMixin, serializers.Serializer):
@staticmethod
def get_response_body_api():
return get_api_response(openapi.Schema(
type=openapi.TYPE_STRING,
title="captcha",
default="xxxx",
description="captcha"
))
@staticmethod
def generate():
chars = get_random_chars()
image = ImageCaptcha()
data = image.generate(chars)
captcha = base64.b64encode(data.getbuffer())
captcha_cache.set(f"LOGIN:{chars}", chars, timeout=5 * 60)
return 'data:image/png;base64,' + captcha.decode()
class SystemSerializer(ApiMixin, serializers.Serializer):
@ -71,6 +95,8 @@ class LoginSerializer(ApiMixin, serializers.Serializer):
password = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Password")))
captcha = serializers.CharField(required=True, error_messages=ErrMessage.char(_("captcha")))
def is_valid(self, *, raise_exception=False):
"""
校验参数
@ -78,6 +104,10 @@ class LoginSerializer(ApiMixin, serializers.Serializer):
:return: User information
"""
super().is_valid(raise_exception=True)
captcha = self.data.get('captcha')
captcha_value = captcha_cache.get(f"LOGIN:{captcha}")
if captcha_value is None:
raise AppApiException(1005, _("Captcha code error or expiration"))
username = self.data.get("username")
password = password_encrypt(self.data.get("password"))
user = QuerySet(User).filter(Q(username=username,
@ -109,7 +139,8 @@ class LoginSerializer(ApiMixin, serializers.Serializer):
required=['username', 'password'],
properties={
'username': openapi.Schema(type=openapi.TYPE_STRING, title=_("Username"), description=_("Username")),
'password': openapi.Schema(type=openapi.TYPE_STRING, title=_("Password"), description=_("Password"))
'password': openapi.Schema(type=openapi.TYPE_STRING, title=_("Password"), description=_("Password")),
'captcha': openapi.Schema(type=openapi.TYPE_STRING, title=_("captcha"), description=_("captcha"))
}
)

View File

@ -6,6 +6,7 @@ app_name = "user"
urlpatterns = [
path('profile', views.Profile.as_view()),
path('user', views.User.as_view(), name="profile"),
path('user/captcha', views.CaptchaView.as_view(), name='captcha'),
path('user/language', views.SwitchUserLanguageView.as_view(), name='language'),
path('user/list', views.User.Query.as_view()),
path('user/login', views.Login.as_view(), name='login'),

View File

@ -26,7 +26,7 @@ from smartdoc.settings import JWT_AUTH
from users.serializers.user_serializers import RegisterSerializer, LoginSerializer, CheckCodeSerializer, \
RePasswordSerializer, \
SendEmailSerializer, UserProfile, UserSerializer, UserManageSerializer, UserInstanceSerializer, SystemSerializer, \
SwitchLanguageSerializer
SwitchLanguageSerializer, CaptchaSerializer
from users.views.common import get_user_operation_object, get_re_password_details
user_cache = cache.caches['user_cache']
@ -170,6 +170,18 @@ def _get_details(request):
}
class CaptchaView(APIView):
@action(methods=['GET'], detail=False)
@swagger_auto_schema(operation_summary=_("Obtain graphical captcha"),
operation_id=_("Obtain graphical captcha"),
responses=CaptchaSerializer().get_response_body_api(),
security=[],
tags=[_("User management")])
def get(self, request: Request):
return result.success(CaptchaSerializer().generate())
class Login(APIView):
@action(methods=['POST'], detail=False)

View File

@ -68,6 +68,7 @@ django-db-connection-pool = "1.2.5"
opencv-python-headless = "4.11.0.86"
pymysql = "1.1.1"
accelerate = "1.6.0"
captcha = "0.7.1"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@ -37,6 +37,10 @@ interface LoginRequest {
*
*/
password: string
/**
*
*/
captcha: string
}
interface RegisterRequest {

View File

@ -27,6 +27,13 @@ const login: (
}
return post('/user/login', request, undefined, loading)
}
/**
*
* @returns
*/
const getCaptcha: () => Promise<Result<string>> = () => {
return get('user/captcha')
}
/**
*
* @param loading
@ -226,5 +233,6 @@ export default {
postLanguage,
getDingOauth2Callback,
getlarkCallback,
getQrSource
getQrSource,
getCaptcha
}

View File

@ -28,6 +28,10 @@ export default {
requiredMessage: 'Please enter username',
lengthMessage: 'Length must be between 6 and 20 words'
},
captcha: {
label: 'captcha',
placeholder: 'Please enter the captcha'
},
nick_name: {
label: 'Name',
placeholder: 'Please enter name'

View File

@ -25,6 +25,10 @@ export default {
requiredMessage: '请输入用户名',
lengthMessage: '长度在 6 到 20 个字符'
},
captcha: {
label: '验证码',
placeholder: '请输入验证码'
},
nick_name: {
label: '姓名',
placeholder: '请输入姓名'
@ -33,7 +37,7 @@ export default {
label: '邮箱',
placeholder: '请输入邮箱',
requiredMessage: '请输入邮箱',
validatorEmail: '请输入有效邮箱格式!',
validatorEmail: '请输入有效邮箱格式!'
},
phone: {
label: '手机号',
@ -48,13 +52,13 @@ export default {
new_password: {
label: '新密码',
placeholder: '请输入新密码',
requiredMessage: '请输入新密码',
requiredMessage: '请输入新密码'
},
re_password: {
label: '确认密码',
placeholder: '请输入确认密码',
requiredMessage: '请输入确认密码',
validatorMessage: '密码不一致',
validatorMessage: '密码不一致'
}
}
},

View File

@ -26,6 +26,10 @@ export default {
requiredMessage: '請輸入使用者名稱',
lengthMessage: '長度須介於 6 到 20 個字元之間'
},
captcha: {
label: '驗證碼',
placeholder: '請輸入驗證碼'
},
nick_name: {
label: '姓名',
placeholder: '請輸入姓名'

View File

@ -135,8 +135,8 @@ const useUserStore = defineStore({
})
},
async login(auth_type: string, username: string, password: string) {
return UserApi.login(auth_type, { username, password }).then((ok) => {
async login(auth_type: string, username: string, password: string, captcha: string) {
return UserApi.login(auth_type, { username, password, captcha }).then((ok) => {
this.token = ok.data
localStorage.setItem('token', ok.data)
return this.profile()

View File

@ -34,6 +34,21 @@
</el-input>
</el-form-item>
</div>
<div class="mb-24">
<el-form-item prop="captcha">
<div class="flex-between w-full">
<el-input
size="large"
class="input-item"
v-model="loginForm.captcha"
:placeholder="$t('views.user.userForm.form.captcha.placeholder')"
>
</el-input>
<img :src="identifyCode" alt="" height="40" class="ml-8 cursor" @click="makeCode" />
</div>
</el-form-item>
</div>
</el-form>
<el-button size="large" type="primary" class="w-full" @click="login"
@ -107,6 +122,7 @@ import { useRoute, useRouter } from 'vue-router'
import type { FormInstance, FormRules } from 'element-plus'
import useStore from '@/stores'
import authApi from '@/api/auth-setting'
import useApi from '@/api/user'
import { MsgConfirm, MsgError, MsgSuccess } from '@/utils/message'
import { t, getBrowserLang } from '@/locales'
@ -120,9 +136,15 @@ const { user } = useStore()
const router = useRouter()
const loginForm = ref<LoginRequest>({
username: '',
password: ''
password: '',
captcha: ''
})
const identifyCode = ref<string>('')
function makeCode() {
useApi.getCaptcha().then((res: any) => {
identifyCode.value = res.data
})
}
const rules = ref<FormRules<LoginRequest>>({
username: [
{
@ -137,6 +159,13 @@ const rules = ref<FormRules<LoginRequest>>({
message: t('views.user.userForm.form.password.requiredMessage'),
trigger: 'blur'
}
],
captcha: [
{
required: true,
message: t('views.user.userForm.form.captcha.placeholder'),
trigger: 'blur'
}
]
})
const loginFormRef = ref<FormInstance>()
@ -222,7 +251,8 @@ function changeMode(val: string) {
showQrCodeTab.value = false
loginForm.value = {
username: '',
password: ''
password: '',
captcha: ''
}
redirectAuth(val)
loginFormRef.value?.clearValidate()
@ -232,7 +262,12 @@ const login = () => {
loginFormRef.value?.validate().then(() => {
loading.value = true
user
.login(loginMode.value, loginForm.value.username, loginForm.value.password)
.login(
loginMode.value,
loginForm.value.username,
loginForm.value.password,
loginForm.value.captcha
)
.then(() => {
locale.value = localStorage.getItem('MaxKB-locale') || getBrowserLang() || 'en-US'
router.push({ name: 'home' })
@ -285,6 +320,7 @@ onBeforeMount(() => {
declare const window: any
onMounted(() => {
makeCode()
const route = useRoute()
const currentUrl = ref(route.fullPath)
const params = new URLSearchParams(currentUrl.value.split('?')[1])