feat: implement RSA encryption for login data and add LDAP login support

This commit is contained in:
wxg0103 2025-09-22 14:35:02 +08:00
parent a5595bd840
commit d405b06016
7 changed files with 98 additions and 66 deletions

View File

@ -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):
"""

View File

@ -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)

View File

@ -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",

View File

@ -41,6 +41,7 @@ interface LoginRequest {
*
*/
captcha: string
encryptedData?: string
}
interface RegisterRequest {

View File

@ -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<boolean>
) => Promise<Result<string>> = (auth_type, request, loading) => {
if (auth_type !== '') {
return post(`/${auth_type}/login`, request, undefined, loading)
}
const login: (request: LoginRequest, loading?: Ref<boolean>) => Promise<Result<any>> = (
request,
loading
) => {
return post('/user/login', request, undefined, loading)
}
const ldapLogin: (request: LoginRequest, loading?: Ref<boolean>) => Promise<Result<any>> = (
request,
loading
) => {
return post('/ldap/login', request, undefined, loading)
}
/**
*
* @returns
@ -234,5 +231,6 @@ export default {
getDingOauth2Callback,
getlarkCallback,
getQrSource,
getCaptcha
getCaptcha,
ldapLogin
}

View File

@ -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<boolean>) {
return UserApi.login(data).then((ok) => {
this.token = ok.data
localStorage.setItem('token', ok.data)
return this.profile()
})
},
async asyncLdapLogin(data: any, loading?: Ref<boolean>) {
return UserApi.ldapLogin(data).then((ok) => {
this.token = ok.data
localStorage.setItem('token', ok.data)
return this.profile()

View File

@ -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<boolean>(false)
const { user } = useStore()
const router = useRouter()
import forge from 'node-forge'
const loginForm = ref<LoginRequest>({
username: '',
password: '',
captcha: ''
captcha: '',
encryptedData: ''
})
const identifyCode = ref<string>('')
function makeCode() {
useApi.getCaptcha().then((res: any) => {
identifyCode.value = res.data
})
}
const rules = ref<FormRules<LoginRequest>>({
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))
}
}
})
}