diff --git a/main/manager-api/pom.xml b/main/manager-api/pom.xml
index 874a743a..b3798571 100644
--- a/main/manager-api/pom.xml
+++ b/main/manager-api/pom.xml
@@ -199,6 +199,12 @@
knife4j-openapi3-jakarta-spring-boot-starter
${knife4j.version}
+
+
+ org.bouncycastle
+ bcprov-jdk18on
+ 1.78
+
org.springdoc
diff --git a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java
index 55f8fcdb..b80ad6bb 100644
--- a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java
+++ b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java
@@ -86,6 +86,16 @@ public interface Constant {
*/
String SERVER_SECRET = "server.secret";
+ /**
+ * SM2公钥
+ */
+ String SM2_PUBLIC_KEY = "server.public_key";
+
+ /**
+ * SM2私钥
+ */
+ String SM2_PRIVATE_KEY = "server.private_key";
+
/**
* websocket地址
*/
diff --git a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java
index e84fbba8..5761ab7c 100644
--- a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java
+++ b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java
@@ -164,4 +164,8 @@ public interface ErrorCode {
// 字典相关错误码
int DICT_LABEL_DUPLICATE = 10128; // 字典标签重复
+
+ // SM2加密相关错误码
+ int SM2_KEY_NOT_CONFIGURED = 10129; // SM2密钥未配置
+ int SM2_DECRYPT_ERROR = 10130; // SM2解密失败
}
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
new file mode 100644
index 00000000..25670ce3
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/common/utils/SM2Utils.java
@@ -0,0 +1,191 @@
+package xiaozhi.common.utils;
+
+import org.bouncycastle.asn1.x9.X9ECParameters;
+import org.bouncycastle.crypto.engines.SM2Engine;
+import org.bouncycastle.crypto.params.ECDomainParameters;
+import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
+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 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;
+
+/**
+ * SM2加密工具类
+ */
+public class SM2Utils {
+
+ static {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ /**
+ * 生成SM2密钥对
+ *
+ * @return 密钥对
+ */
+ public static KeyPair generateKeyPair() {
+ 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);
+ }
+ }
+
+ /**
+ * 获取公钥字符串
+ *
+ * @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.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);
+ } catch (Exception e) {
+ throw new RuntimeException("SM2加密失败", e);
+ }
+ }
+
+ /**
+ * SM2解密
+ *
+ * @param privateKey 私钥
+ * @param cipherText Base64编码的密文
+ * @return 明文
+ */
+ public static String decrypt(PrivateKey privateKey, String cipherText) {
+ 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);
+
+ SM2Engine sm2Engine = new SM2Engine();
+ sm2Engine.init(false, privateKeyParameters);
+
+ byte[] encrypted = Base64.decode(cipherText);
+ byte[] decrypted = sm2Engine.processBlock(encrypted, 0, encrypted.length);
+ return new String(decrypted, 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) {
+ 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);
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * 生成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/controller/LoginController.java b/main/manager-api/src/main/java/xiaozhi/modules/security/controller/LoginController.java
index d7333273..0fe7eafe 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
@@ -31,6 +31,8 @@ import xiaozhi.modules.security.password.PasswordUtils;
import xiaozhi.modules.security.service.CaptchaService;
import xiaozhi.modules.security.service.SysUserTokenService;
import xiaozhi.modules.security.user.SecurityUser;
+import xiaozhi.common.utils.SM2Utils;
+import org.apache.commons.lang3.StringUtils;
import xiaozhi.modules.sys.dto.PasswordDTO;
import xiaozhi.modules.sys.dto.RetrievePasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO;
@@ -88,6 +90,26 @@ public class LoginController {
if (!validate) {
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
}
+
+ String password = login.getPassword();
+
+ // 如果密码是SM2加密格式(Base64编码),则进行解密
+ if (isSM2Encrypted(password)) {
+ 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 decryptedPassword = SM2Utils.decrypt(SM2Utils.loadPrivateKey(privateKeyStr), password);
+ login.setPassword(decryptedPassword);
+ } catch (Exception e) {
+ throw new RenException(ErrorCode.SM2_DECRYPT_ERROR);
+ }
+ }
+
// 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
// 判断用户是否存在
@@ -100,6 +122,21 @@ public class LoginController {
}
return sysUserTokenService.createToken(userDTO.getId());
}
+
+ /**
+ * 判断字符串是否为SM2加密格式
+ * @param str 待判断的字符串
+ * @return 是否为SM2加密格式
+ */
+ private boolean isSM2Encrypted(String str) {
+ if (StringUtils.isBlank(str)) {
+ return false;
+ }
+ if (str.length() > 100 && str.matches("^[A-Za-z0-9+/=]+$")) {
+ return true;
+ }
+ return false;
+ }
@PostMapping("/register")
@Operation(summary = "注册")
@@ -211,4 +248,15 @@ public class LoginController {
return new Result