文章目录
- 概述
- 1. 缘起:挑战与目标
- 2 . 核心架构:非对称签名与摘要算法的珠联璧合
- 威胁模型(我们要防的攻击)
- 密钥管理体系
- 3 . 签名与验证:一步一解,安全闭环
- 3.1 A系统:签名的生成(请求前)
- 3.2 B系统:签名的验证(收到请求后)
- 4. API接口设计规约
- 请求头 (Request Headers)
- 请求体 (Request Body)
- 响应体 (Response Body)
- 关键错误码
- 5. 实操
- 项目结构
- 技术选型
- 核心依赖库
- 国密算法应用
- 核心功能实现
- 1. 密钥对生成
- 2. 签名流程
- 3. 验证流程
- 安全设计要点
- 1. 防重放攻击
- 2. 密钥版本管理
- 3. 参数标准化
- 常见问题与答疑(FAQ)
- 小结
- 6. 总结
- 7. 附

概述
在当今的分布式系统架构中,系统间的安全通信,尤其是文件传输,是保障业务流程安全和数据隐私的基石。一个微小的安全漏洞都可能导致敏感信息泄露、数据被恶意篡改或系统遭受重放攻击。本文将深度解析一个基于国家商用密码(简称“国密”)标准设计的系统间文件上传方案,旨在为开发者和架构师提供一个安全、合规、可落地的技术范本。
1. 缘起:挑战与目标
我们面临的场景是:A系统需要通过API调用B系统,安全地上传一个文件。这个看似简单的需求背后,隐藏着一系列严峻的安全挑战:
- 谁在调用? 如何确保调用方是合法的A系统,而非伪装的攻击者?(身份认证)
- 信道是否安全? 如何防止文件内容在传输过程中被窃听?(数据机密性)
- 数据是否被篡改? 如何保证B系统收到的文件与A系统发出的文件一字不差?(数据完整性)
- 请求是否唯一? 如何防止攻击者截获合法请求后,重复发送以造成系统混乱?(防重放攻击)
- 行为是否可追溯? 如何确保每一次上传操作都有据可查,且调用方无法否认其行为?(不可否认性与审计)
为了应对这些挑战,并满足国家信息安全合规的要求,我们确立了以下设计目标:
- 安全为核:采用国密SM系列算法(SM2、SM3),构建一个零信任(Zero Trust)的调用环境。
- 性能兼顾:在确保强安全性的前提下,优化密码运算流程,降低性能开销。
- 易于集成:提供清晰、规范的API接口,降低接入方(A系统)的开发难度。
- 面向未来:架构设计具备良好的扩展性,便于未来更多系统或更复杂的安全策略接入。
2 . 核心架构:非对称签名与摘要算法的珠联璧合
本方案的核心是**“摘要+签名”**的消息认证机制,并结合HTTPS协议实现传输层加密。
- HTTPS (TLS/SSL):作为第一道防线,它负责建立安全的传输通道,对整个HTTP报文(包括请求头和请求体)进行加密,解决了数据机密性的问题。
- SM3 摘要算法:类似于MD5或SHA-256,SM3用于计算文件内容的“数字指纹”。任何对文件的微小改动都会导致其SM3摘要值发生巨大变化。这用于校验文件内容的完整性。
- SM2 非对称加密算法:这是整个方案的灵魂。我们利用其签名/验签功能,实现身份认证、核心参数的完整性保护和行为的不可否认性。
* 使用 SM3 对文件求摘要(file\_sm3);
* 使用 SM2 私钥对参数串签名(sign),B 使用公钥验签;
* HTTPS 保护传输机密性(暂不在应用层用 SM4 加密文件);
* 使用 nonce 防重放(不使用 timestamp,因不能保证时钟同步)。
威胁模型(我们要防的攻击)
按优先级列出要防范的主要威胁:
- 冒充(Impersonation):恶意方伪造 A 系统发起请求 → 通过签名机制阻断(持私钥者才能生成有效签名)
- 重放(Replay):拦截并重复已有请求 → 通过 nonce(与已用缓存)或 timestamp+window 防止
- 中间人(MITM)/窃听:获取文件明文 → HTTPS(TLS)+必要时应用层加密(SM4)
- 篡改(Tampering):在传输或请求中修改内容 → SM2 验签与 SM3 文件指纹确保完整性
- 密钥泄露:私钥被窃取 → 使用 KMS/HSM、严格运维、定期轮换与版本控制
- 拒绝服务(DoS):大量恶意请求耗尽 B 系统资源 → 接入流量控制、验签前流量过滤
密钥管理体系
这是一个典型的非对称密钥架构:
-
B系统(服务提供方):
- 为每个合法的调用方(如A系统)生成一个唯一的
APPID
。 - 为每个
APPID
生成一对SM2密钥对(公钥和私钥)。 - 安全地将
APPID
和私钥
分发给A系统。 - 自身仅保留
APPID
与对应的公钥
,用于后续的签名验证。
- 为每个合法的调用方(如A系统)生成一个唯一的
-
A系统(服务调用方):
- 从B系统处安全地获取并存储
APPID
和SM2私钥
。 - 私钥是A系统的最高机密,绝不能泄露。它代表了A系统在数字世界的唯一身份。
- 从B系统处安全地获取并存储
实际环境中应有严格的密钥分发和保管流程
-
密钥生成:在可信环境(推荐 HSM 或 KMS)生成 SM2 密钥对。记录
key_version
(例如 v1, v2)。 -
私钥存储(A 系统):
- 最好不要直接把私钥写在代码或配置文件。使用云厂商 KMS 或本地 HSM。若无法使用 HSM,至少使用加密存储(OS keystore)并最小化访问权限。
-
公钥分发(B 系统):
- B 系统仅保存公钥与 app_id、key_version、meta 信息。公钥可以 PEM 格式存储在配置中心或数据库中。
-
换钥(Rotate):
- 支持
key_version
:新钥生成后更新 A 系统并在 B 系统配置新公钥,旧公钥在一段兼容期再废弃。 - 回滚策略与兼容周期(例如 30 天)应在同意下确定。
- 支持
-
撤销:若发现私钥泄露,立即标记 key_version 为撤销并拒绝所有该版本签名;必要时封禁 app_id。
-
审计:密钥操作(生成、分发、轮换、撤销)都应记录审计日志并保留(符合合规保留期)。
3 . 签名与验证:一步一解,安全闭环
整体流程可分为:签名生成(A端) → 上传请求(HTTPS,multipart/form-data) → 验签与业务处理(B端) → 响应。
请求参数(Header)
必填 Header(HTTP):
app_id
:A 系统唯一标识nonce
:随机且全局唯一字符串(建议 UUIDv4 或 32 字节随机)key_version
:密钥版本号(便于换钥)file_sm3
:对文件二进制的 SM3 值(hex 或 base64)sign
:对参数串用 SM2 私钥签名后的 base64 编码值
注意:不要在 Header 中放敏感数据(虽 Header 受 TLS 保护,但有日志/代理泄露风险)。
file_sm3
可放 Header 或 Body 元数据,视实现而定。
3.2 请求 Body(multipart/form-data)
version
:请求体格式版本(默认 “0”)file
:实际 Excel 二进制内容
3.3 签名生成(A 系统)
- 读取文件流,计算 SM3 摘要
file_sm3
(hex 小写) - 生成随机
nonce
- 组装参与签名的参数(app_id、file_sm3、key_version、nonce),按字典序升序(key 名称)排序
- 用
key=value
串联并用&
连接,得到原始签名串 - 使用 SM2 私钥对签名串做签名(得到 bytes),用 base64 编码得到
sign
- 发起 HTTPS POST,Header 带上上述字段,Body 上传文件
验签(B 系统)——伪代码
- 接收请求并先做基础校验(app_id 存在性、参数齐全)
- 根据
app_id
查找公钥和 key_version,若未找到返回 1003 - Nonce 校验:在 Redis/内存缓存中尝试写入 nonce(SETNX),如果已存在返回 1002;写入成功设置 TTL(例如 24 小时或更短)
- 按字典序拼接同样的签名串并使用 SM2 公钥验签;验签失败返回 1001
- 验签成功后,比对
file_sm3
与实际上传文件计算的 SM3 值,若不一致返回 1004 - 校验通过后,继续业务处理并写审计日志
下面,我们来详细拆解一次完整的文件上传请求中,签名与验证的每一步。
3.1 A系统:签名的生成(请求前)
在A系统向B系统发起上传请求之前,必须生成一个有效的签名sign
。
第一步:计算文件摘要
首先,A系统需要读取待上传文件的完整二进制内容,并使用SM3算法计算其摘要值。
file_sm3 = SM3(file_content) // e.g., "abc..."
第二步:准备签名参数
将所有需要保护的核心请求参数整理出来,这些参数将共同构成签名的“原材料”。
app_id
: 调用方身份标识 (e.g., “zy…”)nonce
: 一次性随机字符串,用于防重放 (e.g., “xyz…”)file_sm3
: 上一步计算出的文件摘要 (e.g., “abc…”)key_version
: 使用的密钥版本号 (e.g., “def…”)
第三步:参数排序与拼接
这是至关重要且极易出错的一步。为了保证A系统和B系统能生成完全一致的待签/待验字符串,必须遵循统一的规则:
-
按参数名的字典序(ASCII码)升序排列。
- 排序前:
file_sm3
,nonce
,app_id
,key_version
- 排序后:
app_id
,file_sm3
,key_version
,nonce
- 排序前:
-
按
key=value
的格式拼接,并用&
连接。- 拼接结果(
stringToSign
):
app_id=zy...&file_sm3=abc...&key_version=def...&nonce=xyz...
- 拼接结果(
第四步:SM2私钥签名
最后,A系统使用其持有的SM2私钥,对上一步拼接好的字符串stringToSign
进行签名。
sign = SM2_Sign(stringToSign, privateKey)
至此,A系统准备好了所有需要发送的数据:请求头中的app_id
, nonce
, file_sm3
, key_version
, sign
,以及请求体中的文件内容。
3.2 B系统:签名的验证(收到请求后)
B系统收到请求后,会像一个严谨的门卫,执行一系列检查。
第一步:提取参数并初步校验
从请求头中获取app_id
, nonce
, file_sm3
, key_version
和sign
。
第二步:查找公钥
使用app_id
作为索引,从自己的密钥库中查找对应的SM2公钥。如果app_id
不存在,说明是无效的调用方,直接拒绝请求(错误码1003
)。
第三步:Nonce重放校验
检查nonce
值。B系统需要维护一个近期已使用的nonce
缓存(如使用Redis并设置过期时间)。如果该nonce
已存在于缓存中,说明是重放攻击,立即拒绝请求(错误码1002
)。若nonce
有效,则将其存入缓存。
第四步:重建待验签字符串
B系统必须严格按照与A系统完全相同的规则(字典序排序、key=value&
拼接),重建待验签的字符串。
stringToVerify = "app_id=zy...&file_sm3=abc...&key_version=def...&nonce=xyz..."
第五步:SM2公钥验签
使用第二步中获取的SM2公钥,对重建的stringToVerify
和收到的sign
进行验证。
is_valid = SM2_Verify(stringToVerify, sign, publicKey)
如果is_valid
为false
,说明签名无效(可能是参数被篡改,或私钥不匹配),拒绝请求(错误码1001
)。
第六步:文件完整性校验
签名验证通过,仅代表“发送这个请求的指令”是真实、完整的。我们还需最后一步确认文件本身是否完整。B系统计算收到的文件内容的SM3摘要,并与请求头中的file_sm3
字段进行比对。
received_file_sm3 = SM3(received_file_content)
if (received_file_sm3 != file_sm3_from_header) {// 文件内容不一致,拒绝// 返回错误码 1004
}
只有当所有验证全部通过,B系统才会开始处理真正的业务逻辑。
4. API接口设计规约
一个好的安全方案需要一个清晰的API接口来承载。
- 协议与请求方式:
POST
HTTPS://<domain>/xxxx
- 请求体格式:
multipart/form-data
请求头 (Request Headers)
参数名 | 类型 | 是否必填 | 描述 |
---|---|---|---|
app_id | string | 是 | A系统的唯一标识符,用于查找公钥。 |
nonce | string | 是 | 随机字符串,防重放,每次请求必须唯一。 |
key_version | string | 是 | 密钥版本号,便于未来密钥平滑升级。 |
file_sm3 | string | 是 | 文件内容的SM3摘要值,用于校验文件完整性。 |
sign | string | 是 | 对核心参数的SM2签名值。 |
请求体 (Request Body)
{"version": "0","file": "<binary content of the file>"
}
version
: 字符串,请求体格式的版本号,用于API的向后兼容。file
: 文件的二进制内容。
响应体 (Response Body)
成功响应示例:
{"code": 0,"message": "Success","data": {"version": "0"}
}
失败响应示例:
{"code": 1001,"message": "Signature verification failed","data": {"version": "0"}
}
关键错误码
错误码 | 描述 |
---|---|
0 | 成功 |
1001 | 签名验证失败 |
1002 | Nonce已使用,疑似重放攻击 |
1003 | 无效的app_id,调用方身份不明 |
1004 | 文件SM3校验失败,文件内容可能已损坏 |
1005 | 文件格式错误 |
1006 | 服务器内部错误 |
5. 实操
本项目是一个基于国密算法的文件签名验证系统,主要用于对Excel文件进行数字签名和验证。系统采用SM3算法对文件内容进行摘要计算,再使用SM2算法对摘要进行数字签名,确保文件的完整性和来源可信性。
项目结构
test-sign/
├── src/
│ └── main/
│ └── java/
│ └── com/
│ └── artisan/
│ ├── App.java
│ └── sign/
│ ├── SM2KeyPairGenerator.java
│ ├── SM2SignValidateDemo.java
│ └── SignatureUtil.java
├── pom.xml
└── .gitignore
该项目采用标准的Maven项目结构,主要功能集中在 com.artisan.sign
包中,包含密钥生成、签名验证和工具类三个核心模块。
技术选型
核心依赖库
项目主要依赖以下几个核心库:
- Bouncy Castle (bcprov-jdk18on): 提供国密算法支持,是Java平台中最广泛使用的密码学库之一
- Hutool: 一个功能丰富且易用的Java工具库,项目中主要用于Excel文件读取和SM3摘要计算
- Apache POI: 用于处理Excel文件格式,与Hutool配合完成Excel文件内容读取
- Lombok: 简化Java代码,减少样板代码的编写
<dependencies><dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk18on</artifactId><version>1.80</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.39</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-poi</artifactId><version>5.8.39</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>4.1.2</version></dependency>
</dependencies>
国密算法应用
项目中主要使用了两种国密算法:
- SM2: 一种基于椭圆曲线的公钥密码算法,用于数字签名和验证
- SM3: 一种密码杂凑算法,用于生成文件摘要
核心功能实现
1. 密钥对生成
SM2KeyPairGenerator类负责生成SM2密钥对,为签名和验证提供基础密钥材料:
public class SM2KeyPairGenerator {public static SM2 generateKeyPair() {// 使用Hutool生成SM2密钥对KeyPair keyPair = SecureUtil.generateKeyPair("SM2");return new SM2(keyPair.getPrivate(), keyPair.getPublic());}
}
2. 签名流程
签名过程包含以下几个关键步骤:
- 读取文件内容: 使用Hutool读取Excel文件内容并转换为字符串
- 计算SM3摘要: 对文件内容进行SM3哈希运算,生成摘要
- 生成随机数: 创建唯一的nonce值,防止重放攻击
- 参数排序: 将签名相关参数按字典序排列
- 拼接签名字符串: 按照指定格式拼接待签名字符串
- SM2签名: 使用私钥对拼接后的字符串进行SM2签名
public static String generateSignature(String excelFilePath, String keyVersion) throws Exception {// 1. 生成真实的appId和私钥String appId = IdUtil.simpleUUID();SM2 sm2KeyPair = SM2KeyPairGenerator.generateKeyPair();// 2. 读取Excel文件内容String content = SignatureUtil.readExcelContent(excelFilePath);// 3. 对Excel内容进行SM3摘要计算String fileSm3 = SignatureUtil.calculateSM3Digest(content);// 4. 生成随机nonceString nonce = IdUtil.simpleUUID();// 5. 准备并排序参数Map<String, Object> params = SignatureUtil.prepareParams(appId, nonce, fileSm3, keyVersion);// 6. 拼接字符串String signStr = SignatureUtil.concatSignString(params);// 7. 使用SM2私钥签名SM2 sm2 = new SM2(sm2KeyPair.getPrivateKey(), null);byte[] signedData = sm2.sign(signStr.getBytes("UTF-8"));// 将签名值转换为十六进制字符串return SignatureUtil.byteArrayToHexString(signedData);
}
3. 验证流程
验证过程与签名过程相对应,主要包括:
- 获取公钥: 根据appId和密钥版本获取对应公钥
- 防重放检查: 验证nonce是否已被使用
- 文件摘要计算: 对待验证文件重新计算SM3摘要
- 参数拼接: 按照相同规则拼接待验证字符串
- 签名验证: 使用公钥验证签名的有效性
public static boolean verifySignature(String appId, String excelFilePath, String nonce, String receivedSignature, String keyVersion) throws Exception {// 1. 获取公钥并校验Map<String, String> publicKeyVersions = APP_PUBLIC_KEYS.get(appId);String publicKeyBase64 = publicKeyVersions.get(keyVersion);// 2. Nonce校验,防止重放攻击 (生产环境请使用redis bloom)if (USED_NONCES.containsKey(nonce)) {return false;}// 3. 读取Excel文件内容String content = SignatureUtil.readExcelContent(excelFilePath);// 4. 对Excel内容进行SM3摘要计算String fileSm3 = SignatureUtil.calculateSM3Digest(content);// 5. 准备并排序参数(与签名时保持一致)Map<String, Object> params = SignatureUtil.prepareParams(appId, nonce, fileSm3, keyVersion);// 6. 拼接字符串String signStr = SignatureUtil.concatSignString(params);// 7. 使用SM2公钥验证签名byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64);SM2 sm2 = new SM2(null, publicKeyBytes);byte[] signBytes = SignatureUtil.hexStringToByteArray(receivedSignature);boolean isValid = sm2.verify(signStr.getBytes("UTF-8"), signBytes);// 验证成功后,将nonce存入缓存 (生产环境请使用redis bloom)if (isValid) {USED_NONCES.put(nonce, System.currentTimeMillis());}return isValid;
}
安全设计要点
1. 防重放攻击
系统通过nonce机制防止重放攻击。每次签名时生成唯一的nonce值,并在验证时检查该nonce是否已被使用,确保每个签名只能被验证一次。
2. 密钥版本管理
支持密钥版本管理机制,允许系统在密钥更新时保持向后兼容性。通过 key_version
参数区分不同版本的密钥对。
3. 参数标准化
签名和验证过程中,所有参数都按照字典序进行排序,确保签名字符串的一致性,避免因参数顺序不同导致签名验证失败。
常见问题与答疑(FAQ)
Q:为什么不把文件内容也用 SM2/SM4 在应用层加密?
A:SM2 是非对称,适合签名/密钥交换,效率不适合用于大文件对称加密。若要求更高的机密性,建议:用 SM4 对文件进行对称加密(流式加密),并用 SM2 对 SM4 的对称密钥做密钥封装(KEM)。但这增加实现与密钥管理复杂度。若对中间人威胁只依赖 TLS 已足够时,可以暂时先用 TLS。
Q:不使用 timestamp 是否足够?
A:nonce + 全局唯一能防重放,但没有时间窗口控制会导致 nonce 缓存规模增大。若能可靠做 NTP 同步,加入 timestamp 是更优方案。
Q:如何处理大文件?
A:建议分片上传(chunk),每片有片级 file_sm3 或整体验证在最后合并时完成。签名可以在上传开始时生成,对整体验证在合并时用。
小结
本项目展示了如何使用国密算法构建一个完整的文件签名验证系统。通过SM2和SM3算法的结合使用,实现了文件完整性保护和来源身份认证的双重安全保障。
在实际生产环境中,还需要考虑以下改进点:
- 密钥存储安全: 当前示例中密钥存储在内存中,生产环境应使用安全的密钥管理系统
- 性能优化: 对于大量文件处理场景,需要考虑并发处理和缓存机制
- 日志审计: 增加完整的操作日志记录,便于安全审计
6. 总结
- 实现要点:SM3 计算文件指纹 → 按字典序拼接签名串 → A 用 SM2 私钥签名 → B 用公钥验签 → Redis 存 nonce 防重放 → HTTPS 保护传输。
- 关键保障:私钥必须安全管理(HSM/KMS)、公钥与 key_version 明确、审计日志齐全、异常监控告警到位。
- 可选增强:引入 timestamp、端到端 SM4 加密、分片上传、HSM 集成。
通过HTTPS + SM3文件摘要 + SM2签名的多层防御体系,系统性地解决了跨系统文件上传中的身份认证、数据机密性、完整性和不可否认性等核心安全问题。它将安全逻辑与业务逻辑解耦,通过请求头传递认证信息,使得安全策略的升级和维护更加便捷。
但,当前方案中文件内容的机密性完全依赖于HTTPS。在某些对安全性要求更高的场景下(如TLS被中间人攻击或代理卸载),可以考虑引入SM4对称加密算法,在应用层对文件内容本身进行加密,实现端到端的加密保护,从而构建一个更加坚不可摧的安全堡垒。
总而言之,一个优秀的安全设计不仅是算法的堆砌,更是对业务流程、潜在风险和运维成本的综合考量。
7. 附
import cn.hutool.core.util.IdUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.SM2;import java.security.KeyPair;
import java.util.Base64;/*** SM2密钥对生成器 */
public class SM2KeyPairGenerator {/*** 生成SM2密钥对* * @return SM2密钥对对象*/public static SM2 generateKeyPair() {// 使用Hutool生成SM2密钥对KeyPair keyPair = SecureUtil.generateKeyPair("SM2");return new SM2(keyPair.getPrivate(), keyPair.getPublic());}/*** 生成并打印密钥对信息* * @param appId 应用ID(用于标识密钥对用途)*/public static void generateAndPrintKeyPair(String appId) {System.out.println("正在为应用 [" + appId + "] 生成SM2密钥对...");// 生成密钥对SM2 sm2 = generateKeyPair();// 获取公钥和私钥的Base64编码String publicKeyBase64 = Base64.getEncoder().encodeToString(sm2.getPublicKey().getEncoded());String privateKeyBase64 = Base64.getEncoder().encodeToString(sm2.getPrivateKey().getEncoded());// 打印密钥对信息System.out.println("应用ID: " + appId);System.out.println("公钥 (Base64): " + publicKeyBase64);System.out.println("私钥 (Base64): " + privateKeyBase64);System.out.println("密钥对生成完成。");}public static void main(String[] args) { generateAndPrintKeyPair(IdUtil.simpleUUID());}
}
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.poi.excel.ExcelReader;
import cn.hutool.poi.excel.ExcelUtil;import java.util.List;
import java.util.Map;
import java.util.TreeMap;/*** 签名工具类,提供公共方法*/
public class SignatureUtil {/*** 读取Excel文件内容并转换为字符串* * @param excelFilePath Excel文件路径* @return 文件内容字符串* @throws Exception 读取文件异常*/public static String readExcelContent(String excelFilePath) throws Exception {// 读取Excel文件内容ExcelReader reader = ExcelUtil.getReader(excelFilePath);List<List<Object>> excelData = reader.read();reader.close();// 将Excel内容转换为字符串StringBuilder contentBuilder = new StringBuilder();for (List<Object> row : excelData) {for (Object cell : row) {contentBuilder.append(cell != null ? cell.toString() : "");contentBuilder.append("|"); // 使用|分隔单元格}contentBuilder.append("\n"); // 换行分隔行}return contentBuilder.toString();}/*** 对内容进行SM3摘要计算* * @param content 内容* @return SM3摘要* @throws Exception 摘要计算异常*/public static String calculateSM3Digest(String content) throws Exception {return DigestUtil.digester("SM3").digestHex(content.getBytes("UTF-8"));}/*** 准备并排序签名参数* * @param appId 应用ID* @param nonce 随机数* @param fileSm3 文件SM3摘要* @return 排序后的参数Map*/public static Map<String, Object> prepareParams(String appId, String nonce, String fileSm3) {Map<String, Object> params = new TreeMap<>();params.put("app_id", appId);params.put("nonce", nonce);params.put("file_sm3", fileSm3);return params;}/*** 准备并排序签名参数(带key_version)* * @param appId 应用ID* @param nonce 随机数* @param fileSm3 文件SM3摘要* @param keyVersion 密钥版本* @return 排序后的参数Map*/public static Map<String, Object> prepareParams(String appId, String nonce, String fileSm3, String keyVersion) {Map<String, Object> params = new TreeMap<>();// 考到扩展,需要修改这里,目前仅做演示params.put("app_id", appId);params.put("key_version", keyVersion);params.put("nonce", nonce);params.put("file_sm3", fileSm3);return params;}/*** 拼接签名字符串* * @param params 参数Map* @return 拼接后的字符串*/public static String concatSignString(Map<String, Object> params) {StringBuilder sb = new StringBuilder();for (Map.Entry<String, Object> entry : params.entrySet()) {sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");}return sb.substring(0, sb.length() - 1); // 移除末尾的 '&'}/*** 将十六进制字符串转换为字节数组* * @param hexString 十六进制字符串* @return 字节数组*/public static byte[] hexStringToByteArray(String hexString) {int len = hexString.length();byte[] data = new byte[len / 2];for (int i = 0; i < len; i += 2) {data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)+ Character.digit(hexString.charAt(i+1), 16));}return data;}/*** 将字节数组转换为十六进制字符串* * @param bytes 字节数组* @return 十六进制字符串*/public static String byteArrayToHexString(byte[] bytes) {StringBuilder hexString = new StringBuilder();for (byte b : bytes) {String hex = Integer.toHexString(0xff & b);if (hex.length() == 1) {hexString.append('0');}hexString.append(hex);}return hexString.toString();}
}
import cn.hutool.core.util.IdUtil;
import cn.hutool.crypto.asymmetric.SM2;import java.security.Security;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;import org.bouncycastle.jce.provider.BouncyCastleProvider;public class SM2SignValidateDemo {static {Security.addProvider(new BouncyCastleProvider());}// 存储nonce的map,用于后续验证 (生产环境 请勿使用这种方式 , 这里仅是demo)private static final Map<String, String> NONCE_MAP = new ConcurrentHashMap<>();// 存储appId和对应公钥的数据库或缓存 (按key_version存储) (生产环境 请勿使用这种方式 , 这里仅是demo)private static final Map<String, Map<String, String>> APP_PUBLIC_KEYS = new ConcurrentHashMap<>();// 存储已用nonce的缓存,用于防重放攻击 (生产环境 请勿使用这种方式 , 这里仅是demo)private static final Map<String, Long> USED_NONCES = new ConcurrentHashMap<>();// 默认密钥版本private static final String DEFAULT_KEY_VERSION = "202508";/*** 生成SM2签名 (默认key_version)* * @param excelFilePath Excel文件路径* @return 签名值* @throws Exception 签名过程中可能抛出的异常*/public static String generateSignature(String excelFilePath) throws Exception {return generateSignature(excelFilePath, DEFAULT_KEY_VERSION);}/*** 生成SM2签名* * @param excelFilePath Excel文件路径* @param keyVersion 密钥版本* @return 签名值* @throws Exception 签名过程中可能抛出的异常*/public static String generateSignature(String excelFilePath, String keyVersion) throws Exception {// 1. 生成真实的appId和私钥String appId = IdUtil.simpleUUID();SM2 sm2KeyPair = SM2KeyPairGenerator.generateKeyPair();String privateKey = Base64.getEncoder().encodeToString(sm2KeyPair.getPrivateKey().getEncoded());String publicKey = Base64.getEncoder().encodeToString(sm2KeyPair.getPublicKey().getEncoded());// 2. 读取Excel文件内容String content = SignatureUtil.readExcelContent(excelFilePath);System.out.println("Excel内容: " + content);// 3. 对Excel内容进行SM3摘要计算String fileSm3 = SignatureUtil.calculateSM3Digest(content);// 4. 生成随机nonceString nonce = IdUtil.simpleUUID();// 5. 将nonce存储到map中 (生产环境 请勿使用这种方式 , 这里仅是demo)NONCE_MAP.put(nonce, appId);// 同时将公钥按版本存储到APP_PUBLIC_KEYS中供验证使用 (生产环境 请勿使用这种方式 , 这里仅是demo)APP_PUBLIC_KEYS.computeIfAbsent(appId, k -> new ConcurrentHashMap<>()).put(keyVersion, publicKey);// 6. 准备并排序参数Map<String, Object> params = SignatureUtil.prepareParams(appId, nonce, fileSm3, keyVersion);// 7. 拼接字符串String signStr = SignatureUtil.concatSignString(params);System.out.println("待签名字符串: " + signStr);// 8. 使用SM2私钥签名SM2 sm2 = new SM2(sm2KeyPair.getPrivateKey(), null);byte[] signedData = sm2.sign(signStr.getBytes("UTF-8"));// 将签名值转换为十六进制字符串return SignatureUtil.byteArrayToHexString(signedData);}/*** 验证SM2签名 (默认key_version)* * @param appId 应用ID* @param excelFilePath Excel文件路径* @param nonce 随机数* @param receivedSignature 接收到的签名* @return 验证结果* @throws Exception 验证过程中可能抛出的异常*/public static boolean verifySignature(String appId, String excelFilePath, String nonce, String receivedSignature) throws Exception {return verifySignature(appId, excelFilePath, nonce, receivedSignature, DEFAULT_KEY_VERSION);}/*** 验证SM2签名* * @param appId 应用ID* @param excelFilePath Excel文件路径* @param nonce 随机数* @param receivedSignature 接收到的签名* @param keyVersion 密钥版本* @return 验证结果* @throws Exception 验证过程中可能抛出的异常*/public static boolean verifySignature(String appId, String excelFilePath, String nonce, String receivedSignature, String keyVersion) throws Exception {// 1. 获取公钥并校验Map<String, String> publicKeyVersions = APP_PUBLIC_KEYS.get(appId);if (publicKeyVersions == null) {System.err.println("AppId不存在,验证失败。");return false;}String publicKeyBase64 = publicKeyVersions.get(keyVersion);if (publicKeyBase64 == null) {System.err.println("指定的密钥版本[" + keyVersion + "]不存在,验证失败。");return false;}System.out.println("密钥版本:" + publicKeyVersions);// 2. Nonce校验,防止重放攻击if (USED_NONCES.containsKey(nonce)) {System.err.println("Nonce已使用,可能存在重放攻击。");return false;}// 3. 读取Excel文件内容String content = SignatureUtil.readExcelContent(excelFilePath);// 4. 对Excel内容进行SM3摘要计算String fileSm3 = SignatureUtil.calculateSM3Digest(content);// 5. 准备并排序参数(与签名时保持一致)Map<String, Object> params = SignatureUtil.prepareParams(appId, nonce, fileSm3, keyVersion);// 6. 拼接字符串String signStr = SignatureUtil.concatSignString(params);System.out.println("待验签字符串: " + signStr);// 7. 使用SM2公钥验证签名byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64);SM2 sm2 = new SM2(null, publicKeyBytes);byte[] signBytes = SignatureUtil.hexStringToByteArray(receivedSignature);boolean isValid = sm2.verify(signStr.getBytes("UTF-8"), signBytes);// 验证成功后,将nonce存入缓存if (isValid) {USED_NONCES.put(nonce, System.currentTimeMillis());System.out.println("签名验证成功");} else {System.out.println("签名验证失败");}return isValid;}public static void main(String[] args) {try {// 示例参数String excelFilePath = "C:\\Users\\Administrator\\Desktop\\111.xls";// 使用默认版本生成签名String signature = generateSignature(excelFilePath);System.out.println("生成的签名: " + signature);// 获取生成签名时使用的appId和nonceString appId = null;String nonce = null;for (Map.Entry<String, String> entry : NONCE_MAP.entrySet()) {nonce = entry.getKey();appId = entry.getValue();break;}// 使用默认版本验证签名if (appId != null && nonce != null) {boolean isValid = verifySignature(appId, excelFilePath, nonce, signature);System.out.println("签名验证结果: " + (isValid ? "成功" : "失败"));}System.out.println("-------------------");// 使用指定版本生成签名String keyVersion = "2.0";String signatureV2 = generateSignature(excelFilePath, keyVersion);System.out.println("生成的签名 (版本 " + keyVersion + "): " + signatureV2);// 使用指定版本验证签名if (appId != null && nonce != null) {boolean isValid = verifySignature(appId, excelFilePath, nonce, signatureV2, keyVersion);System.out.println("签名验证结果 (版本 " + keyVersion + "): " + (isValid ? "成功" : "失败"));}} catch (Exception e) {e.printStackTrace();}}
}