mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-25 17:22:55 +00:00
feat: Login and add graphic captcha (#3117)
This commit is contained in:
parent
1ba8077e95
commit
c1ddec1a61
|
|
@ -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)])
|
||||
|
|
|
|||
|
|
@ -7490,4 +7490,13 @@ msgid "Field: {name} No value set"
|
|||
msgstr ""
|
||||
|
||||
msgid "Generate related"
|
||||
msgstr ""
|
||||
|
||||
msgid "Obtain graphical captcha"
|
||||
msgstr ""
|
||||
|
||||
msgid "Captcha code error or expiration"
|
||||
msgstr ""
|
||||
|
||||
msgid "captcha"
|
||||
msgstr ""
|
||||
|
|
@ -7653,4 +7653,13 @@ msgid "Field: {name} No value set"
|
|||
msgstr "字段: {name} 未设置值"
|
||||
|
||||
msgid "Generate related"
|
||||
msgstr "生成问题"
|
||||
msgstr "生成问题"
|
||||
|
||||
msgid "Obtain graphical captcha"
|
||||
msgstr "获取图形验证码"
|
||||
|
||||
msgid "Captcha code error or expiration"
|
||||
msgstr "验证码错误或过期"
|
||||
|
||||
msgid "captcha"
|
||||
msgstr "验证码"
|
||||
|
|
@ -7663,4 +7663,13 @@ msgid "Field: {name} No value set"
|
|||
msgstr "欄位: {name} 未設定值"
|
||||
|
||||
msgid "Generate related"
|
||||
msgstr "生成問題"
|
||||
msgstr "生成問題"
|
||||
|
||||
msgid "Obtain graphical captcha"
|
||||
msgstr "獲取圖形驗證碼"
|
||||
|
||||
msgid "Captcha code error or expiration"
|
||||
msgstr "驗證碼錯誤或過期"
|
||||
|
||||
msgid "captcha"
|
||||
msgstr "驗證碼"
|
||||
|
|
@ -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") # 文件夹路径
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,18 +6,22 @@
|
|||
@date:2023/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"))
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ interface LoginRequest {
|
|||
* 密码
|
||||
*/
|
||||
password: string
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
captcha: string
|
||||
}
|
||||
|
||||
interface RegisterRequest {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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: '密码不一致'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ export default {
|
|||
requiredMessage: '請輸入使用者名稱',
|
||||
lengthMessage: '長度須介於 6 到 20 個字元之間'
|
||||
},
|
||||
captcha: {
|
||||
label: '驗證碼',
|
||||
placeholder: '請輸入驗證碼'
|
||||
},
|
||||
nick_name: {
|
||||
label: '姓名',
|
||||
placeholder: '請輸入姓名'
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
Loading…
Reference in New Issue