mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
feat: implement RSA encryption for login data and add LDAP login support
This commit is contained in:
parent
a5595bd840
commit
d405b06016
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ interface LoginRequest {
|
|||
* 验证码
|
||||
*/
|
||||
captcha: string
|
||||
encryptedData?: string
|
||||
}
|
||||
|
||||
interface RegisterRequest {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue