加密解密加签验签——接口安全的最后一道防线

加密解密加签验签------接口安全的最后一道防线

密评来了

等保密评(密码应用安全性评估)要求下发。

整改项

  • 用户信息存储:明文 → 加密
  • 数据传输:明文 → HTTPS + 加密
  • 接口调用:无认证 → 加签验签
  • 登录口令:明文传输 → SM3 摘要

加密方案演进

算法研究

复制代码
加密算法的问题
加密研究
加密算法实现
vc解密算法实现

当时还在做技术预研,试过各种算法:

  • RSA 1024/2048
  • AES 128/256
  • DES/3DES
  • MD5/SHA

结论:政务系统要求用国密算法,不能用国际算法。

接口加密

复制代码
新接口加密功能开发及动态库改写

接口升级,首次引入加密:

c 复制代码
// 动态库中的加密函数(VC编写)
int EncryptData(
    const char* plainText,  // 明文
    char* cipherText,       // 密文(输出)
    const char* key         // 密钥
);

int DecryptData(
    const char* cipherText, // 密文
    char* plainText,        // 明文(输出)
    const char* key         // 密钥
);

问题:密钥硬编码在动态库里,每发一个版本都要重新编译。

电子签名

复制代码
电子签名文件生产函数编写

PDF电子签名,用于医保凭证:

java 复制代码
public byte[] signPdf(byte[] pdfData, X509Certificate cert, PrivateKey key) {
    // 1. 计算PDF摘要
    MessageDigest md = MessageDigest.getInstance("SM3");
    byte[] digest = md.digest(pdfData);
    
    // 2. 用私钥签名
    Signature sig = Signature.getInstance("SM2");
    sig.initSign(key);
    sig.update(digest);
    byte[] signature = sig.sign();
    
    // 3. 嵌入PDF
    PdfSigner signer = new PdfSigner();
    signer.sign(pdfData, cert, signature);
    
    return signer.getSignedPdf();
}

全面密评改造

1. HTTPS 证书问题
复制代码
解决认证https不能访问的问题
证书不符合要求

问题:自签名证书被浏览器拦截。

解决:用 OpenSSL 生成合法证书。

bash 复制代码
# 生成RSA私钥和自签名证书
openssl req -newkey rsa:2048 -nodes -keyout rsa_private.key \
  -x509 -days 365 -out cert.crt

# 导出为PFX格式(Tomcat使用)
openssl pkcs12 -export -out certificate.pfx \
  -inkey rsa_private.key -in cert.crt

注意:生产环境必须用 CA 颁发的证书,不能自签名。

2. 用户信息加密存储
复制代码
用户信息加密解密开发、部署

需求:数据库中存储的用户信息(姓名、身份证、手机号)必须加密。

java 复制代码
// 加密存储方案
public class UserInfoEncryption {
    
    // 存储时加密
    public void saveUser(User user) {
        String encryptedName = SM4.encrypt(user.getName(), secretKey);
        String encryptedIdCard = SM4.encrypt(user.getIdCard(), secretKey);
        
        jdbcTemplate.update(
            "INSERT INTO t_user (name, id_card, ...) VALUES (?, ?, ...)",
            encryptedName, encryptedIdCard
        );
    }
    
    // 查询时解密
    public User getUser(Long id) {
        Map<String, Object> row = jdbcTemplate.queryForMap(
            "SELECT * FROM t_user WHERE id = ?", id
        );
        
        User user = new User();
        user.setName(SM4.decrypt((String)row.get("name"), secretKey));
        user.setIdCard(SM4.decrypt((String)row.get("id_card"), secretKey));
        
        return user;
    }
}

问题 :加密后无法模糊查询(LIKE '%张%')。

解决

java 复制代码
// 方案1:加密字段精确查询
SELECT * FROM t_user WHERE name = SM4.encrypt('张三', key)

// 方案2:增加明文索引字段(脱敏后)
ALTER TABLE t_user ADD name_index VARCHAR(50);
// name_index = "张**"(只存姓氏,不存全名)

// 方案3:使用可搜索加密(Searchable Encryption)
// 但国密不支持,暂不采用
3. 接口加签验签
复制代码
加密、解密、加签、验签测试
加密、解密、加签、验签方法确定、代码编写、更新数据、查询数据测试

完整方案

java 复制代码
public class ApiSecurity {
    
    // ========== 加签(发送方) ==========
    public String signRequest(Map<String, String> params, String secretKey) {
        // 1. 参数排序
        TreeMap<String, String> sortedParams = new TreeMap<>(params);
        
        // 2. 拼接字符串
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
            sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
        }
        sb.append("key=").append(secretKey);
        
        // 3. SM3 摘要
        byte[] digest = SM3.digest(sb.toString().getBytes(StandardCharsets.UTF_8));
        
        // 4. 转十六进制字符串
        return Hex.encodeHexString(digest);
    }
    
    // ========== 验签(接收方) ==========
    public boolean verifySign(Map<String, String> params, String sign, String secretKey) {
        // 1. 去除签名字段
        Map<String, String> paramsWithoutSign = new HashMap<>(params);
        paramsWithoutSign.remove("sign");
        
        // 2. 重新计算签名
        String calculatedSign = signRequest(paramsWithoutSign, secretKey);
        
        // 3. 比对
        return calculatedSign.equals(sign);
    }
    
    // ========== 接口加密 ==========
    public String encryptRequest(String data, String sessionKey) {
        // 1. 生成随机密钥
        byte[] randomKey = SM4.generateKey();
        
        // 2. 用会话密钥加密随机密钥
        byte[] encryptedKey = SM4.encrypt(randomKey, sessionKey);
        
        // 3. 用随机密钥加密数据
        byte[] encryptedData = SM4.encrypt(data.getBytes(), randomKey);
        
        // 4. 组装
        return Base64.encode(encryptedKey) + "." + Base64.encode(encryptedData);
    }
}

验签流程

复制代码
请求方                         接收方
  │                              │
  │ 1. 参数排序                   │
  │ 2. 拼接 key=value&key=value  │
  │ 3. SM3 摘要                  │
  │ 4. 得到签名 sign             │
  │                              │
  │ ──── 请求参数 + sign ──────▶ │
  │                              │
  │                              │ 1. 去除 sign 字段
  │                              │ 2. 同样方式计算签名
  │                              │ 3. 比对两个签名
  │                              │
  │ ◀──── 响应 + sign ────────── │
  │                              │
  │ 验签响应                      │
4. 登录口令密文传输
复制代码
登录口令密文传输改造(历史数据查询)
前端sm3算法加密口令(实际为计算摘要)
后端解密存储在数据中的口令,再sm3算法加密
比较是否相同

改造前

javascript 复制代码
// 登录时明文传输
$.ajax({
    url: "/login",
    data: {
        username: "admin",
        password: "123456"  // 明文!抓包就能看到
    }
});

改造后

javascript 复制代码
// 前端:SM3 摘要后传输
async function login(username, password) {
    // 1. 获取随机盐值
    const salt = await getSalt(username);
    
    // 2. 计算 SM3(password + salt)
    const hash = sm3(password + salt);
    
    // 3. 传输摘要(不是原文)
    $.ajax({
        url: "/login",
        data: {
            username: username,
            passwordHash: hash,
            salt: salt
        }
    });
}
java 复制代码
// 后端:验证逻辑
public boolean verifyPassword(String username, String passwordHash, String salt) {
    // 1. 从数据库取出加密存储的口令
    String storedPassword = userDao.getPassword(username);
    
    // 2. 解密存储的口令(SM4解密)
    String decryptedPassword = SM4.decrypt(storedPassword, secretKey);
    
    // 3. 计算 SM3(解密口令 + salt)
    String expectedHash = SM3.digest(decryptedPassword + salt);
    
    // 4. 比对
    return expectedHash.equals(passwordHash);
}

国密算法选择

为什么用国密?

等保密评要求:政务系统必须使用国密算法

算法对照表

用途 国际算法 国密算法 说明
对称加密 AES SM4 分组加密,128位密钥
非对称加密 RSA SM2 基于椭圆曲线,256位密钥
摘要 SHA-256 SM3 256位摘要
随机数 DRBG SM3派生 用于生成密钥

实际使用对比

java 复制代码
// AES 加密(旧)
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, aesKey, iv);
byte[] encrypted = cipher.doFinal(plainText);

// SM4 加密(新)
SMS4 sm4 = new SMS4();
byte[] encrypted = sm4.encrypt(plainText, sm4Key, sm4Iv);

SM2 vs RSA 性能

算法 密钥生成 签名(100次) 验签(100次) 安全性
RSA-2048 慢(2s) 慢(1s) 快(0.3s) 中等
SM2-256 快(0.1s) 快(0.3s) 慢(1s) 高(同等RSA-3072)

性能问题

加密对性能的影响

复制代码
测试数据:
- 100万条用户信息加密存储
- SM4加密每条耗时:0.1ms
- SM4解密每条耗时:0.1ms
- 总耗时:100万 × 0.1ms = 100秒

优化后:
- 批量加密:1000条/批
- 并行处理:4线程
- 总耗时:100万 × 0.1ms / 4 / 1000 = 25秒

查询性能对比

操作 明文 加密后 下降
精确查询 2ms 5ms 2.5x
范围查询 10ms 无法 -
模糊查询 15ms 无法 -
批量查询 5ms 15ms 3x

教训:加密不是免费的,需要为性能下降买单。

常见坑

坑1:编码不一致

java 复制代码
// 前端JavaScript
const hash = sm3("张三");
// 输出:e8d7...(UTF-8编码)

// 后端Java
byte[] digest = SM3.digest("张三".getBytes(StandardCharsets.UTF_8));
String hash = Hex.encodeHexString(digest);
// 输出:e8d7...(一致)

// 如果后端用了GBK
byte[] digest = SM3.digest("张三".getBytes("GBK"));
// 输出:f3a2...(不一致!)

坑2:Base64 vs Hex

java 复制代码
// Base64:大小写敏感,有+和/字符
// Hex:只含0-9a-f,URL安全

// 建议:传输用Base64URL(无+无/无=)
String safe = Base64.getUrlEncoder().withoutPadding().encodeToString(data);

坑3:密钥泄露

复制代码
# 密钥被上传到Git仓库
git add .
git commit -m "add config"
git push
# 密钥文件被公开!

# 预防措施
echo "*.key" >> .gitignore
echo "keystore.*" >> .gitignore

坑4:证书过期

复制代码
# 证书过期,服务不可用
javax.net.ssl.SSLHandshakeException: 
  sun.security.validator.ValidatorException: 
    PKIX path validation failed: 
      java.security.cert.CertPathValidatorException: 
        validity check failed

# 监控证书过期时间
openssl x509 -in cert.pem -noout -enddate
# 设置提前30天告警

经验教训

1. 加密不是万能的

  • 加密解决的是存储和传输安全
  • 业务安全需要权限控制、审计日志配合
  • 数据脱敏和加密是两回事

2. 国密算法生态不完善

  • Java 默认不支持 SM2/SM3/SM4,需要 BouncyCastle
  • 部分数据库不支持加密函数
  • 硬件加密机(HSM)兼容性差

3. 性能损耗不可忽视

  • 加密后无法做数据库内计算
  • 模糊查询需要额外设计
  • 批量操作需要分页

4. 密钥管理是最薄弱的环节

复制代码
最常见的密钥管理问题:
1. 密钥硬编码 → 泄露
2. 密钥定期轮换 → 旧数据无法解密
3. 密钥多环境 → 开发、测试、生产混用
4. 密钥备份 → 丢失后数据永久丢失

最后的话

加密解密加签验签,看起来是纯技术问题,实际上是一个管理问题

技术上,SM2/SM3/SM4 算法都是现成的,BouncyCastle 库也成熟。真正难的是:

  1. 密钥怎么管------谁持有密钥?泄露了怎么办?
  2. 性能怎么保------加密后查询慢了10倍,业务能接受吗?
  3. 兼容怎么搞------旧数据没加密,新数据加密了,怎么平滑过渡?

最实在:

复制代码
加密、解密、加签、验签方法确定、代码编写、更新数据、查询数据测试

"方法确定"放在"代码编写"前面------先想清楚再做,加密这事,一步错步步错。

相关推荐
智慧医养结合软件开源2 小时前
智慧养老系统医生管理模块:专业赋能,筑牢老人诊疗安全防线
大数据·人工智能·安全·生活
w1wi4 小时前
CRA 差距分析完全指南 | 合规落地第一步
网络·人工智能·安全
晓梦林5 小时前
translate靶场学习笔记
笔记·学习·安全·web安全
小鹿软件办公6 小时前
在 Windows 中什么是 iphlpsvc?禁用它安全吗?
windows·安全·iphlpsvc
竹云科技8 小时前
“IAM适配AI智能体“被列为2026年Gartner六大安全趋势之一
人工智能·安全
厚国兄8 小时前
Agent 工程化系列 · 第 13 篇_Agent安全与可靠性如何保障
人工智能·安全·llm·prompt·agent
云祺vinchin9 小时前
云祺x鼎捷,为制造企业ERP打造双保险
数据库·安全·制造
效能革命笔记9 小时前
DevOps工具链选型推荐:聚焦本土适配与安全可控
人工智能·安全·devops