💡 为什么需要国密? 在金融、政务、医疗等行业,国家密码管理局要求使用国产密码算法。本文带你从零开始,在 Spring Boot 中落地国密加密。
一、国密算法简介
1.1 什么是国密?
国密即国家密码局认定的国产密码算法,主要包括:
| 算法 | 类型 | 用途 | 国际对标 |
|---|---|---|---|
| SM2 | 非对称加密 | 数字签名、密钥协商 | RSA/ECC |
| SM3 | 哈希算法 | 消息摘要、完整性校验 | SHA-256 |
| SM4 | 对称加密 | 数据加密存储 | AES |
| SM1 | 对称加密 | 硬件实现(不公开) | AES |
1.2 应用场景
- 🔐 SM2:用户登录签名、接口验签、证书加密
- 🔐 SM3:密码哈希、文件完整性校验
- 🔐 SM4:敏感数据加密存储(身份证、手机号等)
二、环境准备
2.1 Maven 依赖
xml
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Bouncy Castle 国密支持 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>1.77</version>
</dependency>
<!-- Hutool 工具类(可选,简化开发) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.23</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2.2 注册 Bouncy Castle Provider
c
@Configuration
public class SecurityConfig {
@PostConstruct
public void init() {
// 注册 BC 提供者
Security.addProvider(new BouncyCastleProvider());
log.info("BouncyCastle Provider 已注册");
}
}
三、核心工具类实现
3.1 SM3 哈希工具类
c
@Component
public class SM3Util {
/**
* SM3 哈希
* @param data 输入数据
* @return 64 位 hex 字符串
*/
public static String hash(String data) {
SM3Digest sm3 = new SM3Digest();
byte[] input = data.getBytes(StandardCharsets.UTF_8);
sm3.update(input, 0, input.length);
byte[] result = new byte[sm3.getDigestSize()];
sm3.doFinal(result, 0);
return Hex.toHexString(result);
}
/**
* HMAC-SM3
*/
public static String hmac(String data, String key) {
KeyParameter keyParam = new KeyParameter(key.getBytes(StandardCharsets.UTF_8));
SM3Digest sm3 = new SM3Digest();
HMac hmac = new HMac(sm3);
hmac.init(keyParam);
hmac.update(data.getBytes(StandardCharsets.UTF_8), 0, data.length());
byte[] out = new byte[hmac.getMacSize()];
hmac.doFinal(out, 0);
return Hex.toHexString(out);
}
}
3.2 SM4 对称加密工具类
c
@Component
@Slf4j
public class SM4Util {
private static final String ALGORITHM = "SM4/CBC/PKCS5Padding";
private static final String PROVIDER = "BC";
/**
* 生成密钥(128 位)
*/
public static SecretKey generateKey() throws Exception {
KeyGenerator kg = KeyGenerator.getInstance("SM4", PROVIDER);
kg.init(128);
return kg.generateKey();
}
/**
* 从 hex 字符串加载密钥
*/
public static SecretKey loadKey(String keyHex) {
byte[] keyBytes = Hex.decode(keyHex);
return new SecretKeySpec(keyBytes, "SM4");
}
/**
* 加密
* @param plaintext 明文
* @param key 密钥
* @param iv 初始化向量(16 字节)
* @return base64 密文(包含 IV)
*/
public static String encrypt(String plaintext, SecretKey key, byte[] iv) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM, PROVIDER);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// IV + 密文
byte[] result = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, result, 0, iv.length);
System.arraycopy(encrypted, 0, result, iv.length, encrypted.length);
return Base64.getEncoder().encodeToString(result);
}
/**
* 解密
* @param ciphertext base64 密文(包含 IV)
* @param key 密钥
*/
public static String decrypt(String ciphertext, SecretKey key) throws Exception {
byte[] data = Base64.getDecoder().decode(ciphertext);
byte[] iv = Arrays.copyOfRange(data, 0, 16);
byte[] encrypted = Arrays.copyOfRange(data, 16, data.length);
Cipher cipher = Cipher.getInstance(ALGORITHM, PROVIDER);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] decrypted = cipher.doFinal(encrypted);
return new String(decrypted, StandardCharsets.UTF_8);
}
}
3.3 SM2 非对称加密工具类
c
@Component
@Slf4j
public class SM2Util {
private static final String PROVIDER = "BC";
/**
* 生成密钥对
*/
public static KeyPair generateKeyPair() throws Exception {
ECGenParameterSpec ecSpec = new ECGenParameterSpec("sm2p256v1");
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", PROVIDER);
kpg.initialize(ecSpec, new SecureRandom());
return kpg.generateKeyPair();
}
/**
* 加密
*/
public static String encrypt(PublicKey publicKey, String plaintext) throws Exception {
Cipher cipher = Cipher.getInstance("SM2", PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
return Hex.toHexString(encrypted);
}
/**
* 解密
*/
public static String decrypt(PrivateKey privateKey, String ciphertext) throws Exception {
Cipher cipher = Cipher.getInstance("SM2", PROVIDER);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decrypted = cipher.doFinal(Hex.decode(ciphertext));
return new String(decrypted, StandardCharsets.UTF_8);
}
/**
* 签名
*/
public static String sign(PrivateKey privateKey, String data) throws Exception {
Signature signature = Signature.getInstance("SM2", PROVIDER);
signature.initSign(privateKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
byte[] sigBytes = signature.sign();
return Hex.toHexString(sigBytes);
}
/**
* 验签
*/
public static boolean verify(PublicKey publicKey, String data, String signatureHex) throws Exception {
Signature signature = Signature.getInstance("SM2", PROVIDER);
signature.initVerify(publicKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
return signature.verify(Hex.decode(signatureHex));
}
/**
* 密钥转 PEM 格式(便于存储)
*/
public static String keyToPem(Key key) throws Exception {
return Base64.getEncoder().encodeToString(key.getEncoded());
}
/**
* PEM 格式加载密钥
*/
public static PublicKey pemToPublicKey(String pem, boolean isPublic) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(pem);
KeyFactory kf = KeyFactory.getInstance("EC", PROVIDER);
if (isPublic) {
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
return kf.generatePublic(spec);
} else {
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
return kf.generatePrivate(spec);
}
}
}
四、Spring Boot 实战集成
4.1 配置文件
yml
# application.yml
guomi:
sm4:
key: your-sm4-key-hex # 生产环境从配置中心/密钥管理服务获取
sm2:
public-key: your-sm2-public-key-pem
private-key: your-sm2-private-key-pem # 生产环境不要硬编码!
4.2 配置类
c
@Data
@Configuration
@ConfigurationProperties(prefix = "guomi")
public class GuomiProperties {
private Sm4Config sm4 = new Sm4Config();
private Sm2Config sm2 = new Sm2Config();
@Data
public static class Sm4Config {
private String key;
}
@Data
public static class Sm2Config {
private String publicKey;
private String privateKey;
}
}
4.3 加密服务
c
@Service
@Slf4j
@RequiredArgsConstructor
public class GuomiService {
private final GuomiProperties properties;
/**
* SM4 加密敏感数据
*/
public String encryptSensitiveData(String plaintext) {
try {
SecretKey key = SM4Util.loadKey(properties.getSm4().getKey());
byte[] iv = SM4Util.generateIv(); // 需要实现
return SM4Util.encrypt(plaintext, key, iv);
} catch (Exception e) {
log.error("SM4 加密失败", e);
throw new RuntimeException("加密失败", e);
}
}
/**
* SM4 解密敏感数据
*/
public String decryptSensitiveData(String ciphertext) {
try {
SecretKey key = SM4Util.loadKey(properties.getSm4().getKey());
return SM4Util.decrypt(ciphertext, key);
} catch (Exception e) {
log.error("SM4 解密失败", e);
throw new RuntimeException("解密失败", e);
}
}
/**
* SM2 签名
*/
public String sign(String data) throws Exception {
PrivateKey privateKey = SM2Util.pemToPrivateKey(
properties.getSm2().getPrivateKey());
return SM2Util.sign(privateKey, data);
}
/**
* SM2 验签
*/
public boolean verify(String data, String signature) throws Exception {
PublicKey publicKey = SM2Util.pemToPublicKey(
properties.getSm2().getPublicKey(), true);
return SM2Util.verify(publicKey, data, signature);
}
/**
* SM3 哈希
*/
public String hash(String data) {
return SM3Util.hash(data);
}
}
4.4 Controller 示例
c
@RestController
@RequestMapping("/api/guomi")
@Slf4j
@RequiredArgsConstructor
public class GuomiController {
private final GuomiService guomiService;
/**
* 加密用户手机号
*/
@PostMapping("/encrypt/phone")
public Result<String> encryptPhone(@RequestBody PhoneRequest request) {
String encrypted = guomiService.encryptSensitiveData(request.getPhone());
return Result.success(encrypted);
}
/**
* 解密手机号
*/
@PostMapping("/decrypt/phone")
public Result<String> decryptPhone(@RequestBody DecryptRequest request) {
String decrypted = guomiService.decryptSensitiveData(request.getCiphertext());
return Result.success(decrypted);
}
/**
* 接口签名(前端用公钥加密,后端用私钥解密验签)
*/
@PostMapping("/sign")
public Result<SignResponse> sign(@RequestBody SignRequest request) {
try {
String signature = guomiService.sign(request.getData());
String hash = guomiService.hash(request.getData());
return Result.success(new SignResponse(signature, hash));
} catch (Exception e) {
log.error("签名失败", e);
return Result.error("签名失败");
}
}
/**
* 验签
*/
@PostMapping("/verify")
public Result<Boolean> verify(@RequestBody VerifyRequest request) {
try {
boolean valid = guomiService.verify(request.getData(), request.getSignature());
return Result.success(valid);
} catch (Exception e) {
log.error("验签失败", e);
return Result.error("验签失败");
}
}
}
五、实际应用场景
5.1 用户密码加密存储
c
@Service
public class UserService {
@Autowired
private GuomiService guomiService;
public void register(User user) {
// SM3 哈希密码(加盐)
String salt = UUID.randomUUID().toString();
String hashedPassword = guomiService.hash(user.getPassword() + salt);
user.setPassword(hashedPassword);
user.setSalt(salt);
userRepository.save(user);
}
public boolean login(String username, String password) {
User user = userRepository.findByUsername(username);
String hashedPassword = guomiService.hash(password + user.getSalt());
return hashedPassword.equals(user.getPassword());
}
}
5.2 敏感数据脱敏
c
@Component
public class DataMaskingAspect {
@Autowired
private GuomiService guomiService;
@Around("@annotation(EncryptField)")
public Object encryptField(ProceedingJoinPoint pjp) throws Throwable {
Object result = pjp.proceed();
// 反射处理@EncryptField 注解的字段
// 调用 guomiService.encryptSensitiveData()
return result;
}
}
// 自定义注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {}
// 使用
public class UserDTO {
private String name;
@EncryptField
private String idCard;
@EncryptField
private String phone;
}
5.3 接口签名验签
c
// 前端请求头携带签名
@PostMapping("/api/data")
public Result<Void> submitData(
@RequestBody DataRequest request,
@RequestHeader("X-Signature") String signature,
@RequestHeader("X-Timestamp") Long timestamp) {
// 1. 校验时间戳(防重放)
if (System.currentTimeMillis() - timestamp > 5 * 60 * 1000) {
return Result.error("请求已过期");
}
// 2. 验签
String signData = request.toJson() + timestamp;
boolean valid = guomiService.verify(signData, signature);
if (!valid) {
return Result.error("签名无效");
}
// 3. 处理业务
return Result.success();
}