diff --git a/apps/users/serializers/user_serializers.py b/apps/users/serializers/user_serializers.py index 96a4bb390..cea2919f3 100644 --- a/apps/users/serializers/user_serializers.py +++ b/apps/users/serializers/user_serializers.py @@ -8,6 +8,7 @@ """ import base64 import datetime +import json import os import random import re @@ -37,6 +38,7 @@ from common.response.result import get_api_response from common.util.common import valid_license, get_random_chars from common.util.field_message import ErrMessage from common.util.lock import lock +from common.util.rsa_util import decrypt, get_key_pair_by_sql from dataset.models import DataSet, Document, Paragraph, Problem, ProblemParagraphMapping from embedding.task import delete_embedding_by_dataset_id_list from function_lib.models.function import FunctionLib @@ -75,7 +77,8 @@ class SystemSerializer(ApiMixin, serializers.Serializer): xpack_cache = DBModelManage.get_model('xpack_cache') return {'version': version, 'IS_XPACK': hasattr(settings, 'IS_XPACK'), 'XPACK_LICENSE_IS_VALID': False if xpack_cache is None else xpack_cache.get('XPACK_LICENSE_IS_VALID', - False)} + False), + 'ras': get_key_pair_by_sql().get('key')} @staticmethod def get_response_body_api(): @@ -96,35 +99,13 @@ 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"))) + encryptedData = serializers.CharField(required=False, label=_('encryptedData'), allow_null=True, + allow_blank=True) - def is_valid(self, *, raise_exception=False): + def get_user_token(self, user): """ - 校验参数 - :param raise_exception: Whether to throw an exception can only be True - :return: User information - """ - super().is_valid(raise_exception=True) - captcha = self.data.get('captcha') - captcha_value = captcha_cache.get(f"LOGIN:{captcha.lower()}") - 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, - password=password) | Q(email=username, - password=password)).first() - if user is None: - raise ExceptionCodeConstants.INCORRECT_USERNAME_AND_PASSWORD.value.to_app_api_exception() - if not user.is_active: - raise AppApiException(1005, _("The user has been disabled, please contact the administrator!")) - return user - - def get_user_token(self): - """ - Get user token :return: User Token (authentication information) """ - user = self.is_valid() token = signing.dumps({'username': user.username, 'id': str(user.id), 'email': user.email, 'type': AuthenticationType.USER.value}) return token @@ -136,11 +117,13 @@ class LoginSerializer(ApiMixin, serializers.Serializer): def get_request_body_api(self): return openapi.Schema( type=openapi.TYPE_OBJECT, - required=['username', 'password'], + required=['username', 'encryptedData'], properties={ 'username': openapi.Schema(type=openapi.TYPE_STRING, title=_("Username"), description=_("Username")), 'password': openapi.Schema(type=openapi.TYPE_STRING, title=_("Password"), description=_("Password")), - 'captcha': openapi.Schema(type=openapi.TYPE_STRING, title=_("captcha"), description=_("captcha")) + 'captcha': openapi.Schema(type=openapi.TYPE_STRING, title=_("captcha"), description=_("captcha")), + 'encryptedData': openapi.Schema(type=openapi.TYPE_STRING, title=_("encryptedData"), + description=_("encryptedData")) } ) @@ -152,6 +135,29 @@ class LoginSerializer(ApiMixin, serializers.Serializer): description="认证token" )) + @staticmethod + def login(instance): + username = instance.get("username", "") + encryptedData = instance.get("encryptedData", "") + if encryptedData: + json_data = json.loads(decrypt(encryptedData)) + instance.update(json_data) + LoginSerializer(data=instance).is_valid(raise_exception=True) + password = instance.get("password") + captcha = instance.get("captcha", "") + captcha_value = captcha_cache.get(f"LOGIN:{captcha.lower()}") + if captcha_value is None: + raise AppApiException(1005, _("Captcha code error or expiration")) + user = QuerySet(User).filter(Q(username=username, + password=password_encrypt(password)) | Q(email=username, + password=password_encrypt( + password))).first() + if user is None: + raise ExceptionCodeConstants.INCORRECT_USERNAME_AND_PASSWORD.value.to_app_api_exception() + if not user.is_active: + raise AppApiException(1005, _("The user has been disabled, please contact the administrator!")) + return user + class RegisterSerializer(ApiMixin, serializers.Serializer): """ diff --git a/apps/users/views/user.py b/apps/users/views/user.py index 3ca8b395f..c77dce5bb 100644 --- a/apps/users/views/user.py +++ b/apps/users/views/user.py @@ -84,7 +84,7 @@ class SwitchUserLanguageView(APIView): description=_("language")), } ), - responses=RePasswordSerializer().get_response_body_api(), + responses=result.get_default_response(), tags=[_("User management")]) @log(menu='User management', operate='Switch Language', get_operation_object=lambda r, k: {'name': r.user.username}) @@ -111,7 +111,7 @@ class ResetCurrentUserPasswordView(APIView): description=_("Password")) } ), - responses=RePasswordSerializer().get_response_body_api(), + responses=result.get_default_response(), tags=[_("User management")]) @log(menu='User management', operate='Modify current user password', get_operation_object=lambda r, k: {'name': r.user.username}, @@ -195,10 +195,8 @@ class Login(APIView): get_details=_get_details, get_operation_object=lambda r, k: {'name': r.data.get('username')}) def post(self, request: Request): - login_request = LoginSerializer(data=request.data) - # 校验请求参数 - user = login_request.is_valid(raise_exception=True) - token = login_request.get_user_token() + user = LoginSerializer().login(request.data) + token = LoginSerializer().get_user_token(user) token_cache.set(token, user, timeout=CONFIG.get_session_timeout()) return result.success(token) diff --git a/ui/package.json b/ui/package.json index d8708911a..a2e9a4ad7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -39,6 +39,7 @@ "mitt": "^3.0.0", "moment": "^2.30.1", "nanoid": "^5.1.5", + "node-forge": "^1.3.1", "npm": "^10.2.4", "nprogress": "^0.2.0", "pinia": "^2.1.6", @@ -62,6 +63,7 @@ "@types/file-saver": "^2.0.7", "@types/jsdom": "^21.1.1", "@types/node": "^18.17.5", + "@types/node-forge": "^1.3.14", "@types/nprogress": "^0.2.0", "@vitejs/plugin-vue": "^4.3.1", "@vue/eslint-config-prettier": "^8.0.0", diff --git a/ui/src/api/type/user.ts b/ui/src/api/type/user.ts index ef22b55d9..197dba888 100644 --- a/ui/src/api/type/user.ts +++ b/ui/src/api/type/user.ts @@ -41,6 +41,7 @@ interface LoginRequest { * 验证码 */ captcha: string + encryptedData?: string } interface RegisterRequest { diff --git a/ui/src/api/user.ts b/ui/src/api/user.ts index d14a65e2f..1133f2e39 100644 --- a/ui/src/api/user.ts +++ b/ui/src/api/user.ts @@ -10,23 +10,20 @@ import type { } from '@/api/type/user' import type { Ref } from 'vue' -/** - * 登录 - * @param auth_type - * @param request 登录接口请求表单 - * @param loading 接口加载器 - * @returns 认证数据 - */ -const login: ( - auth_type: string, - request: LoginRequest, - loading?: Ref -) => Promise> = (auth_type, request, loading) => { - if (auth_type !== '') { - return post(`/${auth_type}/login`, request, undefined, loading) - } + +const login: (request: LoginRequest, loading?: Ref) => Promise> = ( + request, + loading +) => { return post('/user/login', request, undefined, loading) } + +const ldapLogin: (request: LoginRequest, loading?: Ref) => Promise> = ( + request, + loading +) => { + return post('/ldap/login', request, undefined, loading) +} /** * 获取图形验证码 * @returns @@ -234,5 +231,6 @@ export default { getDingOauth2Callback, getlarkCallback, getQrSource, - getCaptcha + getCaptcha, + ldapLogin } diff --git a/ui/src/stores/modules/user.ts b/ui/src/stores/modules/user.ts index a5f0eae3a..c805715a6 100644 --- a/ui/src/stores/modules/user.ts +++ b/ui/src/stores/modules/user.ts @@ -8,6 +8,7 @@ import { useElementPlusTheme } from 'use-element-plus-theme' import { defaultPlatformSetting } from '@/utils/theme' import { useLocalStorage } from '@vueuse/core' import { localeConfigKey, getBrowserLang } from '@/locales/index' + export interface userStateTypes { userType: number // 1 系统操作者 2 对话用户 userInfo: User | null @@ -17,6 +18,7 @@ export interface userStateTypes { XPACK_LICENSE_IS_VALID: false isXPack: false themeInfo: any + rasKey: string } const useUserStore = defineStore({ @@ -29,7 +31,8 @@ const useUserStore = defineStore({ userAccessToken: '', XPACK_LICENSE_IS_VALID: false, isXPack: false, - themeInfo: null + themeInfo: null, + rasKey: '' }), actions: { getLanguage() { @@ -100,6 +103,7 @@ const useUserStore = defineStore({ this.version = ok.data?.version || '-' this.isXPack = ok.data?.IS_XPACK this.XPACK_LICENSE_IS_VALID = ok.data?.XPACK_LICENSE_IS_VALID + this.rasKey = ok.data?.ras || '' if (this.isEnterprise()) { await this.theme() @@ -135,8 +139,15 @@ const useUserStore = defineStore({ }) }, - async login(auth_type: string, username: string, password: string, captcha: string) { - return UserApi.login(auth_type, { username, password, captcha }).then((ok) => { + async login(data: any, loading?: Ref) { + return UserApi.login(data).then((ok) => { + this.token = ok.data + localStorage.setItem('token', ok.data) + return this.profile() + }) + }, + async asyncLdapLogin(data: any, loading?: Ref) { + return UserApi.ldapLogin(data).then((ok) => { this.token = ok.data localStorage.setItem('token', ok.data) return this.profile() diff --git a/ui/src/views/login/index.vue b/ui/src/views/login/index.vue index 83de02680..99b6bc87b 100644 --- a/ui/src/views/login/index.vue +++ b/ui/src/views/login/index.vue @@ -135,21 +135,27 @@ import QrCodeTab from '@/views/login/components/QrCodeTab.vue' import { useI18n } from 'vue-i18n' import * as dd from 'dingtalk-jsapi' import { loadScript } from '@/utils/utils' + const { locale } = useI18n({ useScope: 'global' }) const loading = ref(false) const { user } = useStore() const router = useRouter() +import forge from 'node-forge' + const loginForm = ref({ username: '', password: '', - captcha: '' + captcha: '', + encryptedData: '' }) const identifyCode = ref('') + function makeCode() { useApi.getCaptcha().then((res: any) => { identifyCode.value = res.data }) } + const rules = ref>({ username: [ { @@ -270,18 +276,28 @@ const login = () => { loginFormRef.value?.validate((valid) => { if (valid) { loading.value = true - user - .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' }) - }) - .finally(() => (loading.value = false)) + if (loginMode.value === 'LDAP') { + user + .asyncLdapLogin(loginForm.value) + .then(() => { + locale.value = localStorage.getItem('MaxKB-locale') || getBrowserLang() || 'en-US' + router.push({ name: 'home' }) + }) + .catch(() => { + loading.value = false + }) + } else { + const publicKey = forge.pki.publicKeyFromPem(user.rasKey) + const encrypted = publicKey.encrypt(JSON.stringify(loginForm.value), 'RSAES-PKCS1-V1_5') + const encryptedBase64 = forge.util.encode64(encrypted) + user + .login({ encryptedData: encryptedBase64, username: loginForm.value.username }) + .then(() => { + locale.value = localStorage.getItem('MaxKB-locale') || getBrowserLang() || 'en-US' + router.push({ name: 'home' }) + }) + .finally(() => (loading.value = false)) + } } }) }