Skip to content

Commit d405b06

Browse files
committed
feat: implement RSA encryption for login data and add LDAP login support
1 parent a5595bd commit d405b06

File tree

7 files changed

+98
-66
lines changed

7 files changed

+98
-66
lines changed

apps/users/serializers/user_serializers.py

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"""
99
import base64
1010
import datetime
11+
import json
1112
import os
1213
import random
1314
import re
@@ -37,6 +38,7 @@
3738
from common.util.common import valid_license, get_random_chars
3839
from common.util.field_message import ErrMessage
3940
from common.util.lock import lock
41+
from common.util.rsa_util import decrypt, get_key_pair_by_sql
4042
from dataset.models import DataSet, Document, Paragraph, Problem, ProblemParagraphMapping
4143
from embedding.task import delete_embedding_by_dataset_id_list
4244
from function_lib.models.function import FunctionLib
@@ -75,7 +77,8 @@ def get_profile():
7577
xpack_cache = DBModelManage.get_model('xpack_cache')
7678
return {'version': version, 'IS_XPACK': hasattr(settings, 'IS_XPACK'),
7779
'XPACK_LICENSE_IS_VALID': False if xpack_cache is None else xpack_cache.get('XPACK_LICENSE_IS_VALID',
78-
False)}
80+
False),
81+
'ras': get_key_pair_by_sql().get('key')}
7982

8083
@staticmethod
8184
def get_response_body_api():
@@ -96,35 +99,13 @@ class LoginSerializer(ApiMixin, serializers.Serializer):
9699
password = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Password")))
97100

98101
captcha = serializers.CharField(required=True, error_messages=ErrMessage.char(_("captcha")))
102+
encryptedData = serializers.CharField(required=False, label=_('encryptedData'), allow_null=True,
103+
allow_blank=True)
99104

100-
def is_valid(self, *, raise_exception=False):
101-
"""
102-
校验参数
103-
:param raise_exception: Whether to throw an exception can only be True
104-
:return: User information
105-
"""
106-
super().is_valid(raise_exception=True)
107-
captcha = self.data.get('captcha')
108-
captcha_value = captcha_cache.get(f"LOGIN:{captcha.lower()}")
109-
if captcha_value is None:
110-
raise AppApiException(1005, _("Captcha code error or expiration"))
111-
username = self.data.get("username")
112-
password = password_encrypt(self.data.get("password"))
113-
user = QuerySet(User).filter(Q(username=username,
114-
password=password) | Q(email=username,
115-
password=password)).first()
116-
if user is None:
117-
raise ExceptionCodeConstants.INCORRECT_USERNAME_AND_PASSWORD.value.to_app_api_exception()
118-
if not user.is_active:
119-
raise AppApiException(1005, _("The user has been disabled, please contact the administrator!"))
120-
return user
121-
122-
def get_user_token(self):
105+
def get_user_token(self, user):
123106
"""
124-
Get user token
125107
:return: User Token (authentication information)
126108
"""
127-
user = self.is_valid()
128109
token = signing.dumps({'username': user.username, 'id': str(user.id), 'email': user.email,
129110
'type': AuthenticationType.USER.value})
130111
return token
@@ -136,11 +117,13 @@ class Meta:
136117
def get_request_body_api(self):
137118
return openapi.Schema(
138119
type=openapi.TYPE_OBJECT,
139-
required=['username', 'password'],
120+
required=['username', 'encryptedData'],
140121
properties={
141122
'username': openapi.Schema(type=openapi.TYPE_STRING, title=_("Username"), description=_("Username")),
142123
'password': openapi.Schema(type=openapi.TYPE_STRING, title=_("Password"), description=_("Password")),
143-
'captcha': openapi.Schema(type=openapi.TYPE_STRING, title=_("captcha"), description=_("captcha"))
124+
'captcha': openapi.Schema(type=openapi.TYPE_STRING, title=_("captcha"), description=_("captcha")),
125+
'encryptedData': openapi.Schema(type=openapi.TYPE_STRING, title=_("encryptedData"),
126+
description=_("encryptedData"))
144127
}
145128
)
146129

@@ -152,6 +135,29 @@ def get_response_body_api(self):
152135
description="认证token"
153136
))
154137

138+
@staticmethod
139+
def login(instance):
140+
username = instance.get("username", "")
141+
encryptedData = instance.get("encryptedData", "")
142+
if encryptedData:
143+
json_data = json.loads(decrypt(encryptedData))
144+
instance.update(json_data)
145+
LoginSerializer(data=instance).is_valid(raise_exception=True)
146+
password = instance.get("password")
147+
captcha = instance.get("captcha", "")
148+
captcha_value = captcha_cache.get(f"LOGIN:{captcha.lower()}")
149+
if captcha_value is None:
150+
raise AppApiException(1005, _("Captcha code error or expiration"))
151+
user = QuerySet(User).filter(Q(username=username,
152+
password=password_encrypt(password)) | Q(email=username,
153+
password=password_encrypt(
154+
password))).first()
155+
if user is None:
156+
raise ExceptionCodeConstants.INCORRECT_USERNAME_AND_PASSWORD.value.to_app_api_exception()
157+
if not user.is_active:
158+
raise AppApiException(1005, _("The user has been disabled, please contact the administrator!"))
159+
return user
160+
155161

156162
class RegisterSerializer(ApiMixin, serializers.Serializer):
157163
"""

apps/users/views/user.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class SwitchUserLanguageView(APIView):
8484
description=_("language")),
8585
}
8686
),
87-
responses=RePasswordSerializer().get_response_body_api(),
87+
responses=result.get_default_response(),
8888
tags=[_("User management")])
8989
@log(menu='User management', operate='Switch Language',
9090
get_operation_object=lambda r, k: {'name': r.user.username})
@@ -111,7 +111,7 @@ class ResetCurrentUserPasswordView(APIView):
111111
description=_("Password"))
112112
}
113113
),
114-
responses=RePasswordSerializer().get_response_body_api(),
114+
responses=result.get_default_response(),
115115
tags=[_("User management")])
116116
@log(menu='User management', operate='Modify current user password',
117117
get_operation_object=lambda r, k: {'name': r.user.username},
@@ -195,10 +195,8 @@ class Login(APIView):
195195
get_details=_get_details,
196196
get_operation_object=lambda r, k: {'name': r.data.get('username')})
197197
def post(self, request: Request):
198-
login_request = LoginSerializer(data=request.data)
199-
# 校验请求参数
200-
user = login_request.is_valid(raise_exception=True)
201-
token = login_request.get_user_token()
198+
user = LoginSerializer().login(request.data)
199+
token = LoginSerializer().get_user_token(user)
202200
token_cache.set(token, user, timeout=CONFIG.get_session_timeout())
203201
return result.success(token)
204202

ui/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"mitt": "^3.0.0",
4040
"moment": "^2.30.1",
4141
"nanoid": "^5.1.5",
42+
"node-forge": "^1.3.1",
4243
"npm": "^10.2.4",
4344
"nprogress": "^0.2.0",
4445
"pinia": "^2.1.6",
@@ -62,6 +63,7 @@
6263
"@types/file-saver": "^2.0.7",
6364
"@types/jsdom": "^21.1.1",
6465
"@types/node": "^18.17.5",
66+
"@types/node-forge": "^1.3.14",
6567
"@types/nprogress": "^0.2.0",
6668
"@vitejs/plugin-vue": "^4.3.1",
6769
"@vue/eslint-config-prettier": "^8.0.0",

ui/src/api/type/user.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ interface LoginRequest {
4141
* 验证码
4242
*/
4343
captcha: string
44+
encryptedData?: string
4445
}
4546

4647
interface RegisterRequest {

ui/src/api/user.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,20 @@ import type {
1010
} from '@/api/type/user'
1111
import type { Ref } from 'vue'
1212

13-
/**
14-
* 登录
15-
* @param auth_type
16-
* @param request 登录接口请求表单
17-
* @param loading 接口加载器
18-
* @returns 认证数据
19-
*/
20-
const login: (
21-
auth_type: string,
22-
request: LoginRequest,
23-
loading?: Ref<boolean>
24-
) => Promise<Result<string>> = (auth_type, request, loading) => {
25-
if (auth_type !== '') {
26-
return post(`/${auth_type}/login`, request, undefined, loading)
27-
}
13+
14+
const login: (request: LoginRequest, loading?: Ref<boolean>) => Promise<Result<any>> = (
15+
request,
16+
loading
17+
) => {
2818
return post('/user/login', request, undefined, loading)
2919
}
20+
21+
const ldapLogin: (request: LoginRequest, loading?: Ref<boolean>) => Promise<Result<any>> = (
22+
request,
23+
loading
24+
) => {
25+
return post('/ldap/login', request, undefined, loading)
26+
}
3027
/**
3128
* 获取图形验证码
3229
* @returns
@@ -234,5 +231,6 @@ export default {
234231
getDingOauth2Callback,
235232
getlarkCallback,
236233
getQrSource,
237-
getCaptcha
234+
getCaptcha,
235+
ldapLogin
238236
}

ui/src/stores/modules/user.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useElementPlusTheme } from 'use-element-plus-theme'
88
import { defaultPlatformSetting } from '@/utils/theme'
99
import { useLocalStorage } from '@vueuse/core'
1010
import { localeConfigKey, getBrowserLang } from '@/locales/index'
11+
1112
export interface userStateTypes {
1213
userType: number // 1 系统操作者 2 对话用户
1314
userInfo: User | null
@@ -17,6 +18,7 @@ export interface userStateTypes {
1718
XPACK_LICENSE_IS_VALID: false
1819
isXPack: false
1920
themeInfo: any
21+
rasKey: string
2022
}
2123

2224
const useUserStore = defineStore({
@@ -29,7 +31,8 @@ const useUserStore = defineStore({
2931
userAccessToken: '',
3032
XPACK_LICENSE_IS_VALID: false,
3133
isXPack: false,
32-
themeInfo: null
34+
themeInfo: null,
35+
rasKey: ''
3336
}),
3437
actions: {
3538
getLanguage() {
@@ -100,6 +103,7 @@ const useUserStore = defineStore({
100103
this.version = ok.data?.version || '-'
101104
this.isXPack = ok.data?.IS_XPACK
102105
this.XPACK_LICENSE_IS_VALID = ok.data?.XPACK_LICENSE_IS_VALID
106+
this.rasKey = ok.data?.ras || ''
103107

104108
if (this.isEnterprise()) {
105109
await this.theme()
@@ -135,8 +139,15 @@ const useUserStore = defineStore({
135139
})
136140
},
137141

138-
async login(auth_type: string, username: string, password: string, captcha: string) {
139-
return UserApi.login(auth_type, { username, password, captcha }).then((ok) => {
142+
async login(data: any, loading?: Ref<boolean>) {
143+
return UserApi.login(data).then((ok) => {
144+
this.token = ok.data
145+
localStorage.setItem('token', ok.data)
146+
return this.profile()
147+
})
148+
},
149+
async asyncLdapLogin(data: any, loading?: Ref<boolean>) {
150+
return UserApi.ldapLogin(data).then((ok) => {
140151
this.token = ok.data
141152
localStorage.setItem('token', ok.data)
142153
return this.profile()

ui/src/views/login/index.vue

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -135,21 +135,27 @@ import QrCodeTab from '@/views/login/components/QrCodeTab.vue'
135135
import { useI18n } from 'vue-i18n'
136136
import * as dd from 'dingtalk-jsapi'
137137
import { loadScript } from '@/utils/utils'
138+
138139
const { locale } = useI18n({ useScope: 'global' })
139140
const loading = ref<boolean>(false)
140141
const { user } = useStore()
141142
const router = useRouter()
143+
import forge from 'node-forge'
144+
142145
const loginForm = ref<LoginRequest>({
143146
username: '',
144147
password: '',
145-
captcha: ''
148+
captcha: '',
149+
encryptedData: ''
146150
})
147151
const identifyCode = ref<string>('')
152+
148153
function makeCode() {
149154
useApi.getCaptcha().then((res: any) => {
150155
identifyCode.value = res.data
151156
})
152157
}
158+
153159
const rules = ref<FormRules<LoginRequest>>({
154160
username: [
155161
{
@@ -270,18 +276,28 @@ const login = () => {
270276
loginFormRef.value?.validate((valid) => {
271277
if (valid) {
272278
loading.value = true
273-
user
274-
.login(
275-
loginMode.value,
276-
loginForm.value.username,
277-
loginForm.value.password,
278-
loginForm.value.captcha
279-
)
280-
.then(() => {
281-
locale.value = localStorage.getItem('MaxKB-locale') || getBrowserLang() || 'en-US'
282-
router.push({ name: 'home' })
283-
})
284-
.finally(() => (loading.value = false))
279+
if (loginMode.value === 'LDAP') {
280+
user
281+
.asyncLdapLogin(loginForm.value)
282+
.then(() => {
283+
locale.value = localStorage.getItem('MaxKB-locale') || getBrowserLang() || 'en-US'
284+
router.push({ name: 'home' })
285+
})
286+
.catch(() => {
287+
loading.value = false
288+
})
289+
} else {
290+
const publicKey = forge.pki.publicKeyFromPem(user.rasKey)
291+
const encrypted = publicKey.encrypt(JSON.stringify(loginForm.value), 'RSAES-PKCS1-V1_5')
292+
const encryptedBase64 = forge.util.encode64(encrypted)
293+
user
294+
.login({ encryptedData: encryptedBase64, username: loginForm.value.username })
295+
.then(() => {
296+
locale.value = localStorage.getItem('MaxKB-locale') || getBrowserLang() || 'en-US'
297+
router.push({ name: 'home' })
298+
})
299+
.finally(() => (loading.value = false))
300+
}
285301
}
286302
})
287303
}

0 commit comments

Comments
 (0)