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:
@@ -199,6 +199,12 @@
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
<version>${knife4j.version}</version>
|
||||
</dependency>
|
||||
<!-- BouncyCastle SM2加密 -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk18on</artifactId>
|
||||
<version>1.78</version>
|
||||
</dependency>
|
||||
<!-- springdoc -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
|
||||
@@ -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地址
|
||||
*/
|
||||
|
||||
@@ -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解密失败
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
+48
@@ -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<Map<String, Object>>().ok(config);
|
||||
}
|
||||
|
||||
@GetMapping("/sm2-public-key")
|
||||
@Operation(summary = "获取SM2公钥")
|
||||
public Result<String> 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<String>().ok(publicKey);
|
||||
}
|
||||
}
|
||||
+26
@@ -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<SysParamsDao, SysParam
|
||||
String newSecret = UUID.randomUUID().toString();
|
||||
updateValueByCode(Constant.SERVER_SECRET, newSecret);
|
||||
}
|
||||
|
||||
// 初始化SM2密钥对
|
||||
initSM2KeyPair();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化SM2密钥对
|
||||
*/
|
||||
private void initSM2KeyPair() {
|
||||
// 获取SM2公钥
|
||||
String publicKey = getValue(Constant.SM2_PUBLIC_KEY, false);
|
||||
// 获取SM2私钥
|
||||
String privateKey = getValue(Constant.SM2_PRIVATE_KEY, false);
|
||||
|
||||
// 如果公钥或私钥为空,则生成新的密钥对
|
||||
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];
|
||||
|
||||
// 更新数据库中的密钥对
|
||||
updateValueByCode(Constant.SM2_PUBLIC_KEY, newPublicKey);
|
||||
updateValueByCode(Constant.SM2_PRIVATE_KEY, newPrivateKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
-- 添加SM2国密算法密钥参数
|
||||
-- 用于服务器端SM2加密解密功能
|
||||
|
||||
-- 添加SM2密钥参数
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES
|
||||
(120, 'server.public_key', '', 'string', 1, '服务器SM2公钥'),
|
||||
(121, 'server.private_key', '', 'string', 1, '服务器SM2私钥');
|
||||
@@ -359,3 +359,10 @@ databaseChangeLog:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202509161701.sql
|
||||
- changeSet:
|
||||
id: 202509221530
|
||||
author: cgd
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202509221530.sql
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"normalize.css": "^8.0.1",
|
||||
"opus-decoder": "^0.7.7",
|
||||
"opus-recorder": "^8.0.5",
|
||||
"sm-crypto": "^0.3.13",
|
||||
"vue": "^2.6.14",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-i18n": "^8.28.2",
|
||||
|
||||
Reference in New Issue
Block a user