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 77d7be12..0da1699b 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
@@ -161,9 +161,8 @@ public interface ErrorCode {
int MQTT_SECRET_LENGTH_INSECURE = 10123; // mqtt密钥长度不安全
int MQTT_SECRET_CHARACTER_INSECURE = 10124; // mqtt密钥必须同时包含大小写字母
int MQTT_SECRET_WEAK_PASSWORD = 10125; // mqtt密钥包含弱密码
-
- // 字典相关错误码
int DICT_LABEL_DUPLICATE = 10128; // 字典标签重复
- // 模型相关错误码
- int MODEL_TYPE_PROVIDE_CODE_NOT_NULL = 10129; // modelType和provideCode不能为空
+ int SM2_KEY_NOT_CONFIGURED = 10129; // SM2密钥未配置
+ int SM2_DECRYPT_ERROR = 10130; // SM2解密失败
+ int MODEL_TYPE_PROVIDE_CODE_NOT_NULL = 10131; // modelType和provideCode不能为空
}
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..cf0ed9c5
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/common/utils/SM2Utils.java
@@ -0,0 +1,130 @@
+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;
+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.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.math.ec.ECPoint;
+import org.bouncycastle.util.encoders.Hex;
+
+import java.math.BigInteger;
+import java.security.*;
+import java.security.spec.ECGenParameterSpec;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 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加密算法
+ *
+ * @param publicKey 十六进制公钥
+ * @param data 明文数据
+ * @return 十六进制密文
+ */
+ public static String encrypt(String publicKey, String data) {
+ try {
+ // 获取一条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);
+
+ SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
+ // 设置sm2为加密模式
+ sm2Engine.init(true, new ParametersWithRandom(publicKeyParameters, new SecureRandom()));
+
+ 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解密算法
+ *
+ * @param privateKey 十六进制私钥
+ * @param cipherData 十六进制密文数据
+ * @return 明文
+ */
+ public static String decrypt(String privateKey, String cipherData) {
+ try {
+ // 使用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.Mode.C1C3C2);
+ // 设置sm2为解密模式
+ sm2Engine.init(false, privateKeyParameters);
+
+ byte[] arrayOfBytes = sm2Engine.processBlock(cipherDataByte, 0, cipherDataByte.length);
+ return new String(arrayOfBytes, StandardCharsets.UTF_8);
+ } catch (Exception e) {
+ throw new RuntimeException("SM2解密失败", e);
+ }
+ }
+
+ /**
+ * 生成密钥对
+ */
+ public static Map createKey() {
+ try {
+ 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) {
+ throw new RuntimeException("生成SM2密钥对失败", e);
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/main/manager-api/src/main/java/xiaozhi/common/utils/Sm2DecryptUtil.java b/main/manager-api/src/main/java/xiaozhi/common/utils/Sm2DecryptUtil.java
new file mode 100644
index 00000000..5dd1a312
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/common/utils/Sm2DecryptUtil.java
@@ -0,0 +1,61 @@
+package xiaozhi.common.utils;
+
+import org.apache.commons.lang3.StringUtils;
+import xiaozhi.common.constant.Constant;
+import xiaozhi.common.exception.ErrorCode;
+import xiaozhi.common.exception.RenException;
+import xiaozhi.modules.security.service.CaptchaService;
+import xiaozhi.modules.sys.service.SysParamsService;
+
+/**
+ * SM2解密和验证码验证工具类
+ * 封装了重复的SM2解密、验证码提取和验证逻辑
+ */
+public class Sm2DecryptUtil {
+
+ /**
+ * 验证码长度
+ */
+ private static final int CAPTCHA_LENGTH = 5;
+
+ /**
+ * 解密SM2加密内容,提取验证码并验证
+ * @param encryptedPassword SM2加密的密码字符串
+ * @param captchaId 验证码ID
+ * @param captchaService 验证码服务
+ * @param sysParamsService 系统参数服务
+ * @return 解密后的实际密码
+ */
+ public static String decryptAndValidateCaptcha(String encryptedPassword, String captchaId,
+ CaptchaService captchaService, SysParamsService sysParamsService) {
+ // 获取SM2私钥
+ String privateKeyStr = sysParamsService.getValue(Constant.SM2_PRIVATE_KEY, true);
+ if (StringUtils.isBlank(privateKeyStr)) {
+ throw new RenException(ErrorCode.SM2_KEY_NOT_CONFIGURED);
+ }
+
+ // 使用SM2私钥解密密码
+ String decryptedContent;
+ try {
+ decryptedContent = SM2Utils.decrypt(privateKeyStr, encryptedPassword);
+ } catch (Exception e) {
+ throw new RenException(ErrorCode.SM2_DECRYPT_ERROR);
+ }
+
+ // 分离验证码和密码:前5位是验证码,后面是密码
+ if (decryptedContent.length() > CAPTCHA_LENGTH) {
+ String embeddedCaptcha = decryptedContent.substring(0, CAPTCHA_LENGTH);
+ String actualPassword = decryptedContent.substring(CAPTCHA_LENGTH);
+
+ // 验证嵌入的验证码是否正确
+ boolean embeddedCaptchaValid = captchaService.validate(captchaId, embeddedCaptcha, true);
+ if (!embeddedCaptchaValid) {
+ throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
+ }
+
+ return actualPassword;
+ } else {
+ throw new RenException(ErrorCode.SM2_DECRYPT_ERROR);
+ }
+ }
+}
\ 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..a008c25b 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;
@@ -31,6 +32,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.Sm2DecryptUtil;
+import org.apache.commons.lang3.StringUtils;
import xiaozhi.modules.sys.dto.PasswordDTO;
import xiaozhi.modules.sys.dto.RetrievePasswordDTO;
import xiaozhi.modules.sys.dto.SysUserDTO;
@@ -42,6 +45,7 @@ import xiaozhi.modules.sys.vo.SysDictDataItem;
/**
* 登录控制层
*/
+@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping("/user")
@@ -66,10 +70,11 @@ public class LoginController {
@Operation(summary = "短信验证码")
public Result smsVerification(@RequestBody SmsVerificationDTO dto) {
// 验证图形验证码
- boolean validate = captchaService.validate(dto.getCaptchaId(), dto.getCaptcha(), true);
+ boolean validate = captchaService.validate(dto.getCaptchaId(), dto.getCaptcha(), false);
if (!validate) {
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
}
+
Boolean isMobileRegister = sysParamsService
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
if (!isMobileRegister) {
@@ -83,11 +88,14 @@ public class LoginController {
@PostMapping("/login")
@Operation(summary = "登录")
public Result login(@RequestBody LoginDTO login) {
- // 验证是否正确输入验证码
- boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
- if (!validate) {
- throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
- }
+ String password = login.getPassword();
+
+ // 使用工具类解密并验证验证码
+ String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
+ password, login.getCaptchaId(), captchaService, sysParamsService);
+
+ login.setPassword(actualPassword);
+
// 按照用户名获取用户
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
// 判断用户是否存在
@@ -100,6 +108,8 @@ public class LoginController {
}
return sysUserTokenService.createToken(userDTO.getId());
}
+
+
@PostMapping("/register")
@Operation(summary = "注册")
@@ -107,6 +117,15 @@ public class LoginController {
if (!sysUserService.getAllowUserRegister()) {
throw new RenException(ErrorCode.USER_REGISTER_DISABLED);
}
+
+ String password = login.getPassword();
+
+ // 使用工具类解密并验证验证码
+ String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
+ password, login.getCaptchaId(), captchaService, sysParamsService);
+
+ login.setPassword(actualPassword);
+
// 是否开启手机注册
Boolean isMobileRegister = sysParamsService
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
@@ -122,12 +141,6 @@ public class LoginController {
if (!validate) {
throw new RenException(ErrorCode.SMS_CODE_ERROR);
}
- } else {
- // 验证是否正确输入验证码
- validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
- if (!validate) {
- throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
- }
}
// 按照用户名获取用户
@@ -190,6 +203,14 @@ public class LoginController {
throw new RenException(ErrorCode.SMS_CODE_ERROR);
}
+ String password = dto.getPassword();
+
+ // 使用工具类解密并验证验证码
+ String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
+ password, dto.getCaptchaId(), captchaService, sysParamsService);
+
+ dto.setPassword(actualPassword);
+
sysUserService.changePasswordDirectly(userDTO.getId(), dto.getPassword());
return new Result<>();
}
@@ -208,6 +229,13 @@ public class LoginController {
config.put("beianIcpNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_ICP_NUM.getValue(), true));
config.put("beianGaNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_GA_NUM.getValue(), true));
config.put("name", sysParamsService.getValue(Constant.SysBaseParam.SERVER_NAME.getValue(), true));
+
+ // SM2公钥
+ String publicKey = sysParamsService.getValue(Constant.SM2_PUBLIC_KEY, true);
+ if (StringUtils.isBlank(publicKey)) {
+ throw new RenException(ErrorCode.SM2_KEY_NOT_CONFIGURED);
+ }
+ config.put("sm2PublicKey", publicKey);
return new Result