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>().ok(config); } + + @GetMapping("/sm2-public-key") + @Operation(summary = "获取SM2公钥") + public Result getSM2PublicKey() { + // 获取SM2公钥 + String publicKey = sysParamsService.getValue(Constant.SM2_PUBLIC_KEY, true); + if (StringUtils.isBlank(publicKey)) { + throw new RenException(ErrorCode.SM2_KEY_NOT_CONFIGURED); + } + return new Result().ok(publicKey); + } } \ No newline at end of file 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 02f595d2..31daf08f 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 @@ -21,6 +21,7 @@ import xiaozhi.common.page.PageData; import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.JsonUtils; +import xiaozhi.common.utils.SM2Utils; import xiaozhi.modules.sys.dao.SysParamsDao; import xiaozhi.modules.sys.dto.SysParamsDTO; import xiaozhi.modules.sys.entity.SysParamsEntity; @@ -206,6 +207,31 @@ public class SysParamsServiceImpl extends BaseServiceImpl