update:前端SM2非对称加密登录功能

This commit is contained in:
3030332422
2025-09-23 20:22:12 +08:00
parent ffba2f4fa3
commit 33bad7ed40
11 changed files with 340 additions and 144 deletions
+19
View File
@@ -192,5 +192,24 @@ export default {
this.retrievePassword(passwordData, callback, failCallback);
});
}).send()
},
// 获取SM2公钥
getSM2PublicKey(callback, failCallback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/user/sm2-public-key`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
RequestService.clearRequestTime();
failCallback(err);
})
.networkFail(() => {
RequestService.reAjaxFun(() => {
this.getSM2PublicKey(callback, failCallback);
});
}).send()
}
}
+9 -1
View File
@@ -1045,5 +1045,13 @@ export default {
'templateQuickConfig.templateNotFound': 'Template not found',
'warning': 'Warning',
'info': 'Info',
'common.networkError': 'Network request failed'
'common.networkError': 'Network request failed',
'sm2.publicKeyNotConfigured': 'SM2 public key not configured, please contact administrator',
'sm2.failedToGetPublicKey': 'Failed to get SM2 public key',
'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'
}
+10 -1
View File
@@ -1044,5 +1044,14 @@ export default {
'templateQuickConfig.resetSuccess': '重置成功',
'warning': '警告',
'info': '提示',
'common.networkError': '网络请求失败'
'common.networkError': '网络请求失败',
// SM2加密相关错误消息
'sm2.publicKeyNotConfigured': 'SM2公钥未配置,请联系管理员',
'sm2.failedToGetPublicKey': '获取SM2公钥失败',
'sm2.encryptionFailed': '密码加密失败',
'sm2.keyGenerationFailed': '密钥对生成失败',
'sm2.invalidPublicKey': '无效的公钥格式',
'sm2.encryptionError': '加密过程中发生错误',
'sm2.publicKeyRetry': '正在重试获取公钥...',
'sm2.publicKeyRetryFailed': '公钥获取重试失败'
}
+11 -1
View File
@@ -1045,5 +1045,15 @@ export default {
'error': '錯誤',
'warning': '警告',
'info': '提示',
'common.networkError': '網路請求失敗'
'common.networkError': '網路請求失敗',
// SM2加密相關錯誤消息
'sm2.publicKeyNotConfigured': 'SM2公鑰未配置,請聯繫管理員',
'sm2.failedToGetPublicKey': '獲取SM2公鑰失敗',
'sm2.encryptionFailed': '密碼加密失敗',
'sm2.keyGenerationFailed': '金鑰對生成失敗',
'sm2.invalidPublicKey': '無效的公鑰格式',
'sm2.encryptionError': '加密過程中發生錯誤',
'sm2.publicKeyRetry': '正在重試獲取公鑰...',
'sm2.publicKeyRetryFailed': '公鑰獲取重試失敗'
}
+72
View File
@@ -200,3 +200,75 @@ export function validateMobile(mobile, areaCode) {
}
}
/**
* 生成SM2密钥对(十六进制格式)
* @returns {Object} 包含公钥和私钥的对象
*/
export function generateSm2KeyPairHex() {
// 使用sm-crypto库生成SM2密钥对
const sm2 = require('sm-crypto').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, plainText) {
if (!publicKey) {
throw new Error('公钥不能为null或undefined');
}
if (!plainText) {
throw new Error('明文不能为空');
}
const sm2 = require('sm-crypto').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, cipherText) {
const sm2 = require('sm-crypto').sm2;
// 移除04前缀(与后端保持一致)
const dataWithoutPrefix = cipherText.startsWith("04") ? cipherText.substring(2) : cipherText;
// SM2解密
return sm2.doDecrypt(dataWithoutPrefix, privateKey, 1);
}
/**
* 判断字符串是否为Base64编码
* @param {string} str 待判断的字符串
* @returns {boolean} 是否为Base64编码
*/
export function isBase64(str) {
if (typeof str !== 'string' || str.trim() === '') {
return false;
}
try {
return btoa(atob(str)) === str;
} catch (e) {
return false;
}
}
+118 -2
View File
@@ -148,8 +148,9 @@
import Api from "@/apis/api";
import VersionFooter from "@/components/VersionFooter.vue";
import i18n, { changeLanguage } from "@/i18n";
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from "@/utils";
import { getUUID, goToPage, showDanger, showSuccess, validateMobile, generateSm2KeyPairHex, sm2Encrypt, isBase64 } from "@/utils";
import { mapState } from "vuex";
import Constant from "@/utils/constant";
export default {
name: "login",
@@ -196,6 +197,8 @@ export default {
captchaUrl: "",
isMobileLogin: false,
languageDropdownVisible: false,
serverPublicKey: "", // 服务器公钥
clientKeyPair: null, // 客户端密钥对
};
},
mounted() {
@@ -204,8 +207,49 @@ export default {
// 根据配置决定默认登录方式
this.isMobileLogin = this.enableMobileRegister;
});
// 获取服务器公钥
this.getServerPublicKey();
// 生成客户端密钥对
this.generateClientKeyPair();
},
methods: {
// 获取服务器公钥
getServerPublicKey() {
console.log('开始获取服务器公钥...');
// 先从本地存储获取
const storedPublicKey = localStorage.getItem(Constant.STORAGE_KEY.PUBLIC_KEY);
if (storedPublicKey) {
console.log('从本地存储获取到公钥,长度:', storedPublicKey.length);
this.serverPublicKey = storedPublicKey;
return;
}
console.log('本地存储无公钥,从服务器获取...');
// 从服务器获取公钥
Api.user.getSM2PublicKey(
(res) => {
if (res.data && res.data.data) {
console.log('获取到服务器公钥,长度:', res.data.data.length);
this.serverPublicKey = res.data.data;
// 存储到本地
localStorage.setItem(Constant.STORAGE_KEY.PUBLIC_KEY, this.serverPublicKey);
console.log('公钥已存储到本地');
} else {
console.error('服务器返回数据格式异常:', res);
}
},
(err) => {
console.error("获取SM2公钥失败:", err);
showDanger(this.$t('sm2.failedToGetPublicKey'));
}
);
},
// 生成客户端密钥对
generateClientKeyPair() {
this.clientKeyPair = generateSm2KeyPairHex();
},
fetchCaptcha() {
if (this.$store.getters.getToken) {
if (this.$route.path !== "/home") {
@@ -285,9 +329,68 @@ export default {
return;
}
// 检查服务器公钥是否已获取,如果未获取则重新获取
if (!this.serverPublicKey) {
try {
// 等待公钥获取完成
await new Promise((resolve, reject) => {
this.getServerPublicKey();
// 设置超时检查,最多等待3秒
const checkInterval = setInterval(() => {
if (this.serverPublicKey) {
clearInterval(checkInterval);
resolve();
}
}, 100);
setTimeout(() => {
clearInterval(checkInterval);
if (!this.serverPublicKey) {
reject(new Error('获取公钥超时'));
}
}, 3000);
});
} catch (error) {
showDanger(this.$t('sm2.failedToGetPublicKey'));
return;
}
}
// 加密密码
let encryptedPassword = this.form.password;
if (!this.isSM2Encrypted(this.form.password)) {
try {
encryptedPassword = sm2Encrypt(this.serverPublicKey, this.form.password);
} catch (error) {
console.error("密码加密失败:", error);
showDanger(this.$t('sm2.encryptionFailed'));
return;
}
}
// 加密用户名
let encryptedUsername = this.form.username;
if (!this.isSM2Encrypted(this.form.username)) {
try {
encryptedUsername = sm2Encrypt(this.serverPublicKey, this.form.username);
} catch (error) {
console.error("用户名加密失败:", error);
showDanger(this.$t('sm2.encryptionFailed'));
return;
}
}
this.form.captchaId = this.captchaUuid;
// 使用加密后的用户名和密码
const loginData = {
...this.form,
username: encryptedUsername,
password: encryptedPassword
};
Api.user.login(
this.form,
loginData,
({ data }) => {
showSuccess(this.$t('login.loginSuccess'));
this.$store.commit("setToken", JSON.stringify(data.data));
@@ -320,6 +423,19 @@ export default {
goToForgetPassword() {
goToPage("/retrieve-password");
},
/**
* 判断字符串是否为SM2加密格式(十六进制格式)
* @param {string} str 待判断的字符串
* @returns {boolean} 是否为SM2加密格式
*/
isSM2Encrypted(str) {
if (typeof str !== 'string' || str.trim() === '') {
return false;
}
// 长度大于100且只包含0-9,a-f,A-F字符
return str.length > 100 && /^[0-9a-fA-F]+$/.test(str);
}
},
};
</script>