diff --git a/main/manager-mobile/package.json b/main/manager-mobile/package.json index 62fae5f0..9c4ed55f 100644 --- a/main/manager-mobile/package.json +++ b/main/manager-mobile/package.json @@ -100,6 +100,7 @@ "js-cookie": "^3.0.5", "pinia": "2.0.36", "pinia-plugin-persistedstate": "3.2.1", + "sm-crypto": "^0.3.13", "vue": "^3.4.21", "wot-design-uni": "^1.9.1", "z-paging": "2.8.7" diff --git a/main/manager-mobile/src/api/auth.ts b/main/manager-mobile/src/api/auth.ts index 83c0ecab..dc0b5159 100644 --- a/main/manager-mobile/src/api/auth.ts +++ b/main/manager-mobile/src/api/auth.ts @@ -4,7 +4,6 @@ import { http } from '@/http/request/alova' export interface LoginData { username: string password: string - captcha: string captchaId: string areaCode?: string mobile?: string diff --git a/main/manager-mobile/src/i18n/en.ts b/main/manager-mobile/src/i18n/en.ts index f87035c1..47921775 100644 --- a/main/manager-mobile/src/i18n/en.ts +++ b/main/manager-mobile/src/i18n/en.ts @@ -186,6 +186,15 @@ export default { 'common.unknownError': 'Unknown error', 'common.networkError': 'Network error', + // SM2 encryption related error messages + 'sm2.publicKeyNotConfigured': 'SM2 public key not configured, please contact administrator', + 'sm2.encryptionFailed': 'Password encryption failed', + 'sm2.keyGenerationFailed': 'Key pair generation failed', + 'sm2.invalidPublicKey': 'Invalid public key format', + 'sm2.encryptionError': 'Error occurred during encryption', + 'sm2.publicKeyRetry': 'Retrying to get public key...', + 'sm2.publicKeyRetryFailed': 'Public key retrieval retry failed', + // Voiceprint page 'voiceprint.noSelectedAgent': 'No agent selected', 'voiceprint.pleaseSelectAgent': 'Please select an agent first', diff --git a/main/manager-mobile/src/i18n/zh_CN.ts b/main/manager-mobile/src/i18n/zh_CN.ts index c6f7b090..c5d7c695 100644 --- a/main/manager-mobile/src/i18n/zh_CN.ts +++ b/main/manager-mobile/src/i18n/zh_CN.ts @@ -186,6 +186,15 @@ export default { 'common.unknownError': '未知错误', 'common.networkError': '网络错误', + // SM2加密相关错误消息 + 'sm2.publicKeyNotConfigured': 'SM2公钥未配置,请联系管理员', + 'sm2.encryptionFailed': '密码加密失败', + 'sm2.keyGenerationFailed': '密钥对生成失败', + 'sm2.invalidPublicKey': '无效的公钥格式', + 'sm2.encryptionError': '加密过程中发生错误', + 'sm2.publicKeyRetry': '正在重试获取公钥...', + 'sm2.publicKeyRetryFailed': '公钥获取重试失败', + // Voiceprint page 'voiceprint.noSelectedAgent': '没有选中的智能体', 'voiceprint.pleaseSelectAgent': '请先选择智能体', diff --git a/main/manager-mobile/src/i18n/zh_TW.ts b/main/manager-mobile/src/i18n/zh_TW.ts index d9dc64e3..7aa6183f 100644 --- a/main/manager-mobile/src/i18n/zh_TW.ts +++ b/main/manager-mobile/src/i18n/zh_TW.ts @@ -186,6 +186,15 @@ export default { 'common.unknownError': '未知錯誤', 'common.networkError': '網路錯誤', + // SM2加密相關錯誤消息 + 'sm2.publicKeyNotConfigured': 'SM2公鑰未配置,請聯繫管理員', + 'sm2.encryptionFailed': '密碼加密失敗', + 'sm2.keyGenerationFailed': '金鑰對生成失敗', + 'sm2.invalidPublicKey': '無效的公鑰格式', + 'sm2.encryptionError': '加密過程中發生錯誤', + 'sm2.publicKeyRetry': '正在重試獲取公鑰...', + 'sm2.publicKeyRetryFailed': '公鑰獲取重試失敗', + // Voiceprint page 'voiceprint.noSelectedAgent': '沒有選中的智能體', 'voiceprint.pleaseSelectAgent': '請先選擇智能體', diff --git a/main/manager-mobile/src/pages/login/index.vue b/main/manager-mobile/src/pages/login/index.vue index e97ffa6c..d133ebe3 100644 --- a/main/manager-mobile/src/pages/login/index.vue +++ b/main/manager-mobile/src/pages/login/index.vue @@ -18,6 +18,8 @@ import { toast } from '@/utils/toast' // 导入国际化相关功能 import { t, changeLanguage, getSupportedLanguages, initI18n } from '@/i18n' import type { Language } from '@/store/lang' +// 导入SM2加密工具 +import { sm2Encrypt } from '@/utils' // 获取屏幕边界到安全区域距离 let safeAreaInsets @@ -42,7 +44,7 @@ systemInfo = uni.getSystemInfoSync() safeAreaInsets = systemInfo.safeAreaInsets // #endif // 表单数据 -const formData = ref({ +const formData = ref({ username: '', password: '', captcha: '', @@ -76,6 +78,11 @@ const areaCodeList = computed(() => { return configStore.config.mobileAreaList || [{ name: '中国大陆', key: '+86' }] }) +// SM2公钥 +const sm2PublicKey = computed(() => { + return configStore.config.sm2PublicKey +}) + // 切换登录方式 function toggleLoginType() { loginType.value = loginType.value === 'username' ? 'mobile' : 'username' @@ -164,15 +171,39 @@ async function handleLogin() { return } + // 检查SM2公钥是否配置 + if (!sm2PublicKey.value) { + toast.warning(t('sm2.publicKeyNotConfigured')) + return + } + try { loading.value = true + // 加密密码 + let encryptedPassword + try { + // 拼接验证码和密码 + const captchaAndPassword = formData.value.captcha + formData.value.password + encryptedPassword = sm2Encrypt(sm2PublicKey.value, captchaAndPassword) + } catch (error) { + console.error('密码加密失败:', error) + toast.warning(t('sm2.encryptionFailed')) + return + } + // 构建登录数据 - const loginData = { ...formData.value } + const loginData: LoginData = { + username: '', + password: encryptedPassword, + captchaId: formData.value.captchaId + } // 如果是手机号登录,将区号+手机号拼接到username字段 if (loginType.value === 'mobile') { loginData.username = `${selectedAreaCode.value}${formData.value.mobile}` + } else { + loginData.username = formData.value.username } const response = await login(loginData) diff --git a/main/manager-mobile/src/pages/settings/index.vue b/main/manager-mobile/src/pages/settings/index.vue index 5a099bf9..b5ad110a 100644 --- a/main/manager-mobile/src/pages/settings/index.vue +++ b/main/manager-mobile/src/pages/settings/index.vue @@ -402,7 +402,7 @@ onMounted(async () => { - {{ supportedLanguages.find(lang => lang.code === currentLanguage.value)?.name }} + {{ supportedLanguages.find(lang => lang.code === currentLanguage)?.name }} diff --git a/main/manager-mobile/src/utils/index.ts b/main/manager-mobile/src/utils/index.ts index 4d173825..0d9070b4 100644 --- a/main/manager-mobile/src/utils/index.ts +++ b/main/manager-mobile/src/utils/index.ts @@ -196,3 +196,60 @@ export function getEnvBaseUploadUrl() { return baseUploadUrl } + +import smCrypto from 'sm-crypto' + +/** + * 生成SM2密钥对(十六进制格式) + * @returns {Object} 包含公钥和私钥的对象 + */ +export function generateSm2KeyPairHex() { + // 使用sm-crypto库生成SM2密钥对 + const sm2 = smCrypto.sm2; + const keypair = sm2.generateKeyPairHex(); + + return { + publicKey: keypair.publicKey, + privateKey: keypair.privateKey, + clientPublicKey: keypair.publicKey, // 客户端公钥 + clientPrivateKey: keypair.privateKey // 客户端私钥 + }; +} + +/** + * SM2公钥加密 + * @param {string} publicKey 公钥(十六进制格式) + * @param {string} plainText 明文 + * @returns {string} 加密后的密文(十六进制格式) + */ +export function sm2Encrypt(publicKey: string, plainText: string): string { + if (!publicKey) { + throw new Error('公钥不能为null或undefined'); + } + + if (!plainText) { + throw new Error('明文不能为空'); + } + + const sm2 = smCrypto.sm2; + // SM2加密,添加04前缀表示未压缩公钥 + const encrypted = sm2.doEncrypt(plainText, publicKey, 1); + // 转换为十六进制格式(与后端保持一致,添加04前缀) + const result = "04" + encrypted; + + return result; +} + +/** + * SM2私钥解密 + * @param {string} privateKey 私钥(十六进制格式) + * @param {string} cipherText 密文(十六进制格式) + * @returns {string} 解密后的明文 + */ +export function sm2Decrypt(privateKey: string, cipherText: string): string { + const sm2 = smCrypto.sm2; + // 移除04前缀(与后端保持一致) + const dataWithoutPrefix = cipherText.startsWith("04") ? cipherText.substring(2) : cipherText; + // SM2解密 + return sm2.doDecrypt(dataWithoutPrefix, privateKey, 1); +}