国密算法 Spring Boot 实战:SM2/SM3/SM4 完整集成指南

💡 为什么需要国密? 在金融、政务、医疗等行业,国家密码管理局要求使用国产密码算法。本文带你从零开始,在 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();
}
相关推荐
一条泥憨鱼5 小时前
Stream流-从进阶到起飞
java·ide·后端·stream
Devin~Y5 小时前
大厂Java面试实战:Spring Boot微服务、Redis缓存、Kafka消息队列与Spring AI RAG
java·spring boot·redis·kafka·mybatis·spring mvc·hikaricp
Hesionberger5 小时前
LeetCode105:前序中序构建二叉树(三解法)
java·数据结构·python·算法·leetcode·深度优先
@小柯555m5 小时前
算法(移动零)
数据结构·算法·leetcode
小江的记录本5 小时前
【Java基础】Java 8-21新特性 :JDK17:密封类、模式匹配、Record类(附《思维导图》+《面试高频考点清单》)
java·数据结构·后端·python·mysql·面试·职场和发展
Mahir085 小时前
Spring 核心原理:IoC/DI 与 Bean 生命周期全景解析
java·后端·spring·面试·bean生命周期·控制反转ioc·依赖注入di
weixin_489690025 小时前
NAS部署实测:Solon vs Spring Boot,从内存到包体积的“降维打击”
java·spring boot·后端
重生之我是Java开发战士5 小时前
【贪心算法】柠檬水找零,将数组和减半的最少操作次数,最大数,摆动序列, 最长递增子序列,递增的三元子序列
算法·贪心算法
Godspeed Zhao5 小时前
从零开始学AI17——SVM的数学支撑知识
算法·机器学习·支持向量机