mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-21 22:53:56 +08:00
update:前端SM2非对称加密登录功能
This commit is contained in:
@@ -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<String, String> 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<String, String> 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};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
+24
-3
@@ -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;
|
||||
|
||||
+3
-3
@@ -224,9 +224,9 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
|
||||
// 如果公钥或私钥为空,则生成新的密钥对
|
||||
if (StringUtils.isBlank(publicKey) || StringUtils.isBlank(privateKey) ||
|
||||
"null".equals(publicKey) || "null".equals(privateKey)) {
|
||||
String[] keyPair = SM2Utils.generateKeyPairStrings();
|
||||
String newPublicKey = keyPair[0];
|
||||
String newPrivateKey = keyPair[1];
|
||||
Map<String, String> 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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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': '公钥获取重试失败'
|
||||
}
|
||||
@@ -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': '公鑰獲取重試失敗'
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user