mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-21 22:53:56 +08:00
update:移动端登录页面加密
This commit is contained in:
@@ -100,6 +100,7 @@
|
|||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"pinia": "2.0.36",
|
"pinia": "2.0.36",
|
||||||
"pinia-plugin-persistedstate": "3.2.1",
|
"pinia-plugin-persistedstate": "3.2.1",
|
||||||
|
"sm-crypto": "^0.3.13",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"wot-design-uni": "^1.9.1",
|
"wot-design-uni": "^1.9.1",
|
||||||
"z-paging": "2.8.7"
|
"z-paging": "2.8.7"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { http } from '@/http/request/alova'
|
|||||||
export interface LoginData {
|
export interface LoginData {
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
captcha: string
|
|
||||||
captchaId: string
|
captchaId: string
|
||||||
areaCode?: string
|
areaCode?: string
|
||||||
mobile?: string
|
mobile?: string
|
||||||
|
|||||||
@@ -186,6 +186,15 @@ export default {
|
|||||||
'common.unknownError': 'Unknown error',
|
'common.unknownError': 'Unknown error',
|
||||||
'common.networkError': 'Network 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 page
|
||||||
'voiceprint.noSelectedAgent': 'No agent selected',
|
'voiceprint.noSelectedAgent': 'No agent selected',
|
||||||
'voiceprint.pleaseSelectAgent': 'Please select an agent first',
|
'voiceprint.pleaseSelectAgent': 'Please select an agent first',
|
||||||
|
|||||||
@@ -186,6 +186,15 @@ export default {
|
|||||||
'common.unknownError': '未知错误',
|
'common.unknownError': '未知错误',
|
||||||
'common.networkError': '网络错误',
|
'common.networkError': '网络错误',
|
||||||
|
|
||||||
|
// SM2加密相关错误消息
|
||||||
|
'sm2.publicKeyNotConfigured': 'SM2公钥未配置,请联系管理员',
|
||||||
|
'sm2.encryptionFailed': '密码加密失败',
|
||||||
|
'sm2.keyGenerationFailed': '密钥对生成失败',
|
||||||
|
'sm2.invalidPublicKey': '无效的公钥格式',
|
||||||
|
'sm2.encryptionError': '加密过程中发生错误',
|
||||||
|
'sm2.publicKeyRetry': '正在重试获取公钥...',
|
||||||
|
'sm2.publicKeyRetryFailed': '公钥获取重试失败',
|
||||||
|
|
||||||
// Voiceprint page
|
// Voiceprint page
|
||||||
'voiceprint.noSelectedAgent': '没有选中的智能体',
|
'voiceprint.noSelectedAgent': '没有选中的智能体',
|
||||||
'voiceprint.pleaseSelectAgent': '请先选择智能体',
|
'voiceprint.pleaseSelectAgent': '请先选择智能体',
|
||||||
|
|||||||
@@ -186,6 +186,15 @@ export default {
|
|||||||
'common.unknownError': '未知錯誤',
|
'common.unknownError': '未知錯誤',
|
||||||
'common.networkError': '網路錯誤',
|
'common.networkError': '網路錯誤',
|
||||||
|
|
||||||
|
// SM2加密相關錯誤消息
|
||||||
|
'sm2.publicKeyNotConfigured': 'SM2公鑰未配置,請聯繫管理員',
|
||||||
|
'sm2.encryptionFailed': '密碼加密失敗',
|
||||||
|
'sm2.keyGenerationFailed': '金鑰對生成失敗',
|
||||||
|
'sm2.invalidPublicKey': '無效的公鑰格式',
|
||||||
|
'sm2.encryptionError': '加密過程中發生錯誤',
|
||||||
|
'sm2.publicKeyRetry': '正在重試獲取公鑰...',
|
||||||
|
'sm2.publicKeyRetryFailed': '公鑰獲取重試失敗',
|
||||||
|
|
||||||
// Voiceprint page
|
// Voiceprint page
|
||||||
'voiceprint.noSelectedAgent': '沒有選中的智能體',
|
'voiceprint.noSelectedAgent': '沒有選中的智能體',
|
||||||
'voiceprint.pleaseSelectAgent': '請先選擇智能體',
|
'voiceprint.pleaseSelectAgent': '請先選擇智能體',
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { toast } from '@/utils/toast'
|
|||||||
// 导入国际化相关功能
|
// 导入国际化相关功能
|
||||||
import { t, changeLanguage, getSupportedLanguages, initI18n } from '@/i18n'
|
import { t, changeLanguage, getSupportedLanguages, initI18n } from '@/i18n'
|
||||||
import type { Language } from '@/store/lang'
|
import type { Language } from '@/store/lang'
|
||||||
|
// 导入SM2加密工具
|
||||||
|
import { sm2Encrypt } from '@/utils'
|
||||||
|
|
||||||
// 获取屏幕边界到安全区域距离
|
// 获取屏幕边界到安全区域距离
|
||||||
let safeAreaInsets
|
let safeAreaInsets
|
||||||
@@ -42,7 +44,7 @@ systemInfo = uni.getSystemInfoSync()
|
|||||||
safeAreaInsets = systemInfo.safeAreaInsets
|
safeAreaInsets = systemInfo.safeAreaInsets
|
||||||
// #endif
|
// #endif
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const formData = ref<LoginData>({
|
const formData = ref({
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
captcha: '',
|
captcha: '',
|
||||||
@@ -76,6 +78,11 @@ const areaCodeList = computed(() => {
|
|||||||
return configStore.config.mobileAreaList || [{ name: '中国大陆', key: '+86' }]
|
return configStore.config.mobileAreaList || [{ name: '中国大陆', key: '+86' }]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// SM2公钥
|
||||||
|
const sm2PublicKey = computed(() => {
|
||||||
|
return configStore.config.sm2PublicKey
|
||||||
|
})
|
||||||
|
|
||||||
// 切换登录方式
|
// 切换登录方式
|
||||||
function toggleLoginType() {
|
function toggleLoginType() {
|
||||||
loginType.value = loginType.value === 'username' ? 'mobile' : 'username'
|
loginType.value = loginType.value === 'username' ? 'mobile' : 'username'
|
||||||
@@ -164,15 +171,39 @@ async function handleLogin() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查SM2公钥是否配置
|
||||||
|
if (!sm2PublicKey.value) {
|
||||||
|
toast.warning(t('sm2.publicKeyNotConfigured'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
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字段
|
// 如果是手机号登录,将区号+手机号拼接到username字段
|
||||||
if (loginType.value === 'mobile') {
|
if (loginType.value === 'mobile') {
|
||||||
loginData.username = `${selectedAreaCode.value}${formData.value.mobile}`
|
loginData.username = `${selectedAreaCode.value}${formData.value.mobile}`
|
||||||
|
} else {
|
||||||
|
loginData.username = formData.value.username
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await login(loginData)
|
const response = await login(loginData)
|
||||||
|
|||||||
@@ -402,7 +402,7 @@ onMounted(async () => {
|
|||||||
</view>
|
</view>
|
||||||
<view class="flex items-center">
|
<view class="flex items-center">
|
||||||
<text class="text-[32rpx] text-[#9d9ea3] font-semibold mr-[16rpx]">
|
<text class="text-[32rpx] text-[#9d9ea3] font-semibold mr-[16rpx]">
|
||||||
{{ supportedLanguages.find(lang => lang.code === currentLanguage.value)?.name }}
|
{{ supportedLanguages.find(lang => lang.code === currentLanguage)?.name }}
|
||||||
</text>
|
</text>
|
||||||
<wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" />
|
<wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" />
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -196,3 +196,60 @@ export function getEnvBaseUploadUrl() {
|
|||||||
|
|
||||||
return baseUploadUrl
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user