diff --git a/main/manager-api/src/main/java/xiaozhi/common/utils/SM2Utils.java b/main/manager-api/src/main/java/xiaozhi/common/utils/SM2Utils.java index 25670ce3..cf0ed9c5 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/utils/SM2Utils.java +++ b/main/manager-api/src/main/java/xiaozhi/common/utils/SM2Utils.java @@ -1,5 +1,6 @@ package xiaozhi.common.utils; +import org.bouncycastle.asn1.gm.GMNamedCurves; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.engines.SM2Engine; import org.bouncycastle.crypto.params.ECDomainParameters; @@ -8,184 +9,122 @@ import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.params.ParametersWithRandom; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; -import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.math.ec.ECPoint; -import org.bouncycastle.util.encoders.Base64; +import org.bouncycastle.util.encoders.Hex; import java.math.BigInteger; -import java.nio.charset.StandardCharsets; import java.security.*; import java.security.spec.ECGenParameterSpec; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; /** - * SM2加密工具类 + * SM2加密工具类(采用十六进制格式,与chancheng-archive-service项目保持一致) */ public class SM2Utils { + /** + * 公钥常量 + */ + public static final String KEY_PUBLIC_KEY = "publicKey"; + /** + * 私钥返回值常量 + */ + public static final String KEY_PRIVATE_KEY = "privateKey"; + static { Security.addProvider(new BouncyCastleProvider()); } /** - * 生成SM2密钥对 + * SM2加密算法 * - * @return 密钥对 + * @param publicKey 十六进制公钥 + * @param data 明文数据 + * @return 十六进制密文 */ - public static KeyPair generateKeyPair() { + public static String encrypt(String publicKey, String data) { try { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC", "BC"); - ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1"); - keyPairGenerator.initialize(sm2Spec, new SecureRandom()); - return keyPairGenerator.generateKeyPair(); - } catch (Exception e) { - throw new RuntimeException("生成SM2密钥对失败", e); - } - } + // 获取一条SM2曲线参数 + X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1"); + // 构造ECC算法参数,曲线方程、椭圆曲线G点、大整数N + ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN()); + //提取公钥点 + ECPoint pukPoint = sm2ECParameters.getCurve().decodePoint(Hex.decode(publicKey)); + // 公钥前面的02或者03表示是压缩公钥,04表示未压缩公钥, 04的时候,可以去掉前面的04 + ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, domainParameters); - /** - * 获取公钥字符串 - * - * @param publicKey 公钥 - * @return Base64编码的公钥字符串 - */ - public static String getPublicKeyStr(PublicKey publicKey) { - BCECPublicKey bcPublicKey = (BCECPublicKey) publicKey; - return Base64.toBase64String(bcPublicKey.getEncoded()); - } - - /** - * 获取私钥字符串 - * - * @param privateKey 私钥 - * @return Base64编码的私钥字符串 - */ - public static String getPrivateKeyStr(PrivateKey privateKey) { - BCECPrivateKey bcPrivateKey = (BCECPrivateKey) privateKey; - return Base64.toBase64String(bcPrivateKey.getEncoded()); - } - - /** - * 从字符串加载公钥 - * - * @param publicKeyStr Base64编码的公钥字符串 - * @return 公钥对象 - */ - public static PublicKey loadPublicKey(String publicKeyStr) { - try { - byte[] keyBytes = Base64.decode(publicKeyStr); - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); - KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC"); - return keyFactory.generatePublic(keySpec); - } catch (Exception e) { - throw new RuntimeException("加载公钥失败", e); - } - } - - /** - * 从字符串加载私钥 - * - * @param privateKeyStr Base64编码的私钥字符串 - * @return 私钥对象 - */ - public static PrivateKey loadPrivateKey(String privateKeyStr) { - try { - byte[] keyBytes = Base64.decode(privateKeyStr); - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); - KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC"); - return keyFactory.generatePrivate(keySpec); - } catch (Exception e) { - throw new RuntimeException("加载私钥失败", e); - } - } - - /** - * SM2加密 - * - * @param publicKey 公钥 - * @param plainText 明文 - * @return Base64编码的密文 - */ - public static String encrypt(PublicKey publicKey, String plainText) { - try { - BCECPublicKey bcPublicKey = (BCECPublicKey) publicKey; - ECPoint ecPoint = bcPublicKey.getQ(); - X9ECParameters x9ECParameters = ECUtil.getNamedCurveByName("sm2p256v1"); - ECDomainParameters ecDomainParameters = new ECDomainParameters( - x9ECParameters.getCurve(), x9ECParameters.getG(), x9ECParameters.getN()); - ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(ecPoint, ecDomainParameters); - - SM2Engine sm2Engine = new SM2Engine(); + SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2); + // 设置sm2为加密模式 sm2Engine.init(true, new ParametersWithRandom(publicKeyParameters, new SecureRandom())); - byte[] input = plainText.getBytes(StandardCharsets.UTF_8); - byte[] encrypted = sm2Engine.processBlock(input, 0, input.length); - return Base64.toBase64String(encrypted); + byte[] in = data.getBytes(StandardCharsets.UTF_8); + byte[] arrayOfBytes = sm2Engine.processBlock(in, 0, in.length); + return Hex.toHexString(arrayOfBytes); } catch (Exception e) { throw new RuntimeException("SM2加密失败", e); } } /** - * SM2解密 + * SM2解密算法 * - * @param privateKey 私钥 - * @param cipherText Base64编码的密文 + * @param privateKey 十六进制私钥 + * @param cipherData 十六进制密文数据 * @return 明文 */ - public static String decrypt(PrivateKey privateKey, String cipherText) { + public static String decrypt(String privateKey, String cipherData) { try { - BCECPrivateKey bcPrivateKey = (BCECPrivateKey) privateKey; - BigInteger privateKeyValue = bcPrivateKey.getD(); - X9ECParameters x9ECParameters = ECUtil.getNamedCurveByName("sm2p256v1"); - ECDomainParameters ecDomainParameters = new ECDomainParameters( - x9ECParameters.getCurve(), x9ECParameters.getG(), x9ECParameters.getN()); - ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKeyValue, ecDomainParameters); + // 使用BC库加解密时密文以04开头,传入的密文前面没有04则补上 + if (!cipherData.startsWith("04")) { + cipherData = "04" + cipherData; + } + byte[] cipherDataByte = Hex.decode(cipherData); + BigInteger privateKeyD = new BigInteger(privateKey, 16); + //获取一条SM2曲线参数 + X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1"); + //构造domain参数 + ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN()); + ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKeyD, domainParameters); - SM2Engine sm2Engine = new SM2Engine(); + SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2); + // 设置sm2为解密模式 sm2Engine.init(false, privateKeyParameters); - byte[] encrypted = Base64.decode(cipherText); - byte[] decrypted = sm2Engine.processBlock(encrypted, 0, encrypted.length); - return new String(decrypted, StandardCharsets.UTF_8); + byte[] arrayOfBytes = sm2Engine.processBlock(cipherDataByte, 0, cipherDataByte.length); + return new String(arrayOfBytes, StandardCharsets.UTF_8); } catch (Exception e) { throw new RuntimeException("SM2解密失败", e); } } /** - * 验证SM2密钥对是否匹配 - * - * @param publicKeyStr 公钥字符串 - * @param privateKeyStr 私钥字符串 - * @return 是否匹配 + * 生成密钥对 */ - public static boolean verifyKeyPair(String publicKeyStr, String privateKeyStr) { + public static Map createKey() { try { - String testData = "test_sm2_encryption"; - PublicKey publicKey = loadPublicKey(publicKeyStr); - PrivateKey privateKey = loadPrivateKey(privateKeyStr); - - String encrypted = encrypt(publicKey, testData); - String decrypted = decrypt(privateKey, encrypted); - - return testData.equals(decrypted); + ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1"); + // 获取一个椭圆曲线类型的密钥对生成器 + KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider()); + // 使用SM2参数初始化生成器 + kpg.initialize(sm2Spec); + // 获取密钥对 + KeyPair keyPair = kpg.generateKeyPair(); + PublicKey publicKey = keyPair.getPublic(); + BCECPublicKey p = (BCECPublicKey) publicKey; + PrivateKey privateKey = keyPair.getPrivate(); + BCECPrivateKey s = (BCECPrivateKey) privateKey; + + Map result = new HashMap<>(); + result.put(KEY_PUBLIC_KEY, Hex.toHexString(p.getQ().getEncoded(false))); + result.put(KEY_PRIVATE_KEY, Hex.toHexString(s.getD().toByteArray())); + return result; } catch (Exception e) { - return false; + throw new RuntimeException("生成SM2密钥对失败", e); } } - /** - * 生成SM2密钥对并返回Base64编码的字符串 - * - * @return 包含公钥和私钥的字符串数组 [公钥, 私钥] - */ - public static String[] generateKeyPairStrings() { - KeyPair keyPair = generateKeyPair(); - String publicKeyStr = getPublicKeyStr(keyPair.getPublic()); - String privateKeyStr = getPrivateKeyStr(keyPair.getPrivate()); - return new String[]{publicKeyStr, privateKeyStr}; - } + } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java index c015240f..8fb1d775 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java @@ -82,6 +82,7 @@ public class ShiroConfig { filterMap.put("/user/captcha", "anon"); filterMap.put("/user/smsVerification", "anon"); filterMap.put("/user/login", "anon"); + filterMap.put("/user/sm2-public-key", "anon"); filterMap.put("/user/pub-config", "anon"); filterMap.put("/user/register", "anon"); filterMap.put("/user/retrieve-password", "anon"); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/controller/LoginController.java b/main/manager-api/src/main/java/xiaozhi/modules/security/controller/LoginController.java index 0fe7eafe..1a9fdd6c 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/controller/LoginController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/controller/LoginController.java @@ -17,6 +17,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import xiaozhi.common.constant.Constant; import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.RenException; @@ -44,6 +45,7 @@ import xiaozhi.modules.sys.vo.SysDictDataItem; /** * 登录控制层 */ +@Slf4j @AllArgsConstructor @RestController @RequestMapping("/user") @@ -91,9 +93,27 @@ public class LoginController { throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR); } + String username = login.getUsername(); String password = login.getPassword(); - // 如果密码是SM2加密格式(Base64编码),则进行解密 + // 如果用户名是SM2加密格式,则进行解密 + if (isSM2Encrypted(username)) { + try { + // 获取SM2私钥 + String privateKeyStr = sysParamsService.getValue(Constant.SM2_PRIVATE_KEY, true); + if (StringUtils.isBlank(privateKeyStr)) { + throw new RenException(ErrorCode.SM2_KEY_NOT_CONFIGURED); + } + + // 使用SM2私钥解密用户名(十六进制格式) + String decryptedUsername = SM2Utils.decrypt(privateKeyStr, username); + login.setUsername(decryptedUsername); + } catch (Exception e) { + throw new RenException(ErrorCode.SM2_DECRYPT_ERROR); + } + } + + // 如果密码是SM2加密格式,则进行解密 if (isSM2Encrypted(password)) { try { // 获取SM2私钥 @@ -103,7 +123,7 @@ public class LoginController { } // 使用SM2私钥解密密码 - String decryptedPassword = SM2Utils.decrypt(SM2Utils.loadPrivateKey(privateKeyStr), password); + String decryptedPassword = SM2Utils.decrypt(privateKeyStr, password); login.setPassword(decryptedPassword); } catch (Exception e) { throw new RenException(ErrorCode.SM2_DECRYPT_ERROR); @@ -132,7 +152,8 @@ public class LoginController { if (StringUtils.isBlank(str)) { return false; } - if (str.length() > 100 && str.matches("^[A-Za-z0-9+/=]+$")) { + // 十六进制格式检测(长度较长且只包含0-9,a-f,A-F字符) + if (str.length() > 100 && str.matches("^[0-9a-fA-F]+$")) { return true; } return false; diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java index 31daf08f..b22dd2b9 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java @@ -224,9 +224,9 @@ public class SysParamsServiceImpl extends BaseServiceImpl keyPair = SM2Utils.createKey(); + String newPublicKey = keyPair.get(SM2Utils.KEY_PUBLIC_KEY); + String newPrivateKey = keyPair.get(SM2Utils.KEY_PRIVATE_KEY); // 更新数据库中的密钥对 updateValueByCode(Constant.SM2_PUBLIC_KEY, newPublicKey); diff --git a/main/manager-web/package.json b/main/manager-web/package.json index e9f0d65c..1ffd1c33 100644 --- a/main/manager-web/package.json +++ b/main/manager-web/package.json @@ -13,6 +13,7 @@ "dotenv": "^16.5.0", "element-ui": "^2.15.14", "flyio": "^0.6.14", + "jsbn": "^1.1.0", "normalize.css": "^8.0.1", "opus-decoder": "^0.7.7", "opus-recorder": "^8.0.5", diff --git a/main/manager-web/src/apis/module/user.js b/main/manager-web/src/apis/module/user.js index 6c16c3e6..78477cd1 100755 --- a/main/manager-web/src/apis/module/user.js +++ b/main/manager-web/src/apis/module/user.js @@ -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() } } diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index 14f4d5a6..1e981a53 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -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' } \ No newline at end of file diff --git a/main/manager-web/src/i18n/zh_CN.js b/main/manager-web/src/i18n/zh_CN.js index 27b0b9e0..3b9e6c24 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -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': '公钥获取重试失败' } \ No newline at end of file diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index a9f224f1..39916bec 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -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': '公鑰獲取重試失敗' } \ No newline at end of file diff --git a/main/manager-web/src/utils/index.js b/main/manager-web/src/utils/index.js index aef8de0a..1954df09 100755 --- a/main/manager-web/src/utils/index.js +++ b/main/manager-web/src/utils/index.js @@ -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; + } +} + diff --git a/main/manager-web/src/views/login.vue b/main/manager-web/src/views/login.vue index 1958c915..eeb6a89b 100644 --- a/main/manager-web/src/views/login.vue +++ b/main/manager-web/src/views/login.vue @@ -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); + } }, };