Merge pull request #2288 from xinnan-tech/sm2_test

登录、注册页面添加非对称加密
This commit is contained in:
hrz
2025-09-30 11:12:50 +08:00
committed by GitHub
53 changed files with 3079 additions and 803 deletions
+6
View File
@@ -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地址
*/
@@ -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不能为空
}
@@ -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<String, String> 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<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) {
throw new RuntimeException("生成SM2密钥对失败", e);
}
}
}
@@ -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);
}
}
}
@@ -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<Void> 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<TokenDTO> 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<Map<String, Object>>().ok(config);
}
@@ -21,10 +21,6 @@ public class LoginDTO implements Serializable {
@NotBlank(message = "{sysuser.password.require}")
private String password;
@Schema(description = "验证码")
@NotBlank(message = "{sysuser.captcha.require}")
private String captcha;
@Schema(description = "手机验证码")
private String mobileCaptcha;
@@ -25,6 +25,10 @@ public class RetrievePasswordDTO implements Serializable {
@NotBlank(message = "{sysuser.password.require}")
private String password;
@Schema(description = "图形验证码ID")
@NotBlank(message = "{sysuser.uuid.require}")
private String captchaId;
}
@@ -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)) {
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);
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,6 +359,13 @@ 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
- changeSet:
id: 202509191545
author: RanChen
@@ -366,8 +373,6 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509191545.sql
- changeSet:
id: 202509220926
author: fyb
@@ -382,4 +387,4 @@ databaseChangeLog:
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202509220958.sql
path: classpath:db/changelog/202509220958.sql
@@ -135,4 +135,6 @@
10124=Your MQTT secret is not secure, MQTT secret must contain both uppercase and lowercase letters
10125=Your MQTT secret contains weak password
10128=Dictionary label is duplicated
10129=modelType and provideCode cannot be empty
10129=SM2 key not configured
10130=SM2 decryption failed
10131=modelType and provideCode cannot be empty
@@ -135,4 +135,6 @@
10124=您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母
10125=您的mqtt密钥包含弱密码
10128=字典标签重复
10129=modelType和provideCode不能为空
10129=SM2密钥未配置
10130=SM2解密失败
10131=modelType和provideCode不能为空
@@ -135,4 +135,6 @@
10124=您的MQTT密鑰不安全,MQTT密鑰必須同時包含大小寫字母
10125=您的MQTT密鑰包含弱密碼
10128=字典標籤重複
10129=modelType和provideCode不能為空
10129=SM2密鑰未配置
10130=SM2解密失敗
10131=modelType和provideCode不能為空
@@ -24,7 +24,6 @@ class loginControllerTest {
LoginDTO loginDTO = new LoginDTO();
loginDTO.setUsername("手机号码");
loginDTO.setPassword("密码");
loginDTO.setCaptcha("123456");
loginController.register(loginDTO);
}