当前位置: 首页 > news >正文

Web安全 - 构建安全可靠的API:基于国密SM2/SM3的文件上传方案深度解析

文章目录

  • 概述
  • 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,因不能保证时钟同步)。

威胁模型(我们要防的攻击)

按优先级列出要防范的主要威胁:

  1. 冒充(Impersonation):恶意方伪造 A 系统发起请求 → 通过签名机制阻断(持私钥者才能生成有效签名)
  2. 重放(Replay):拦截并重复已有请求 → 通过 nonce(与已用缓存)或 timestamp+window 防止
  3. 中间人(MITM)/窃听:获取文件明文 → HTTPS(TLS)+必要时应用层加密(SM4)
  4. 篡改(Tampering):在传输或请求中修改内容 → SM2 验签与 SM3 文件指纹确保完整性
  5. 密钥泄露:私钥被窃取 → 使用 KMS/HSM、严格运维、定期轮换与版本控制
  6. 拒绝服务(DoS):大量恶意请求耗尽 B 系统资源 → 接入流量控制、验签前流量过滤

密钥管理体系

这是一个典型的非对称密钥架构:

  1. B系统(服务提供方)

    • 为每个合法的调用方(如A系统)生成一个唯一的APPID
    • 为每个APPID生成一对SM2密钥对(公钥和私钥)。
    • 安全地将APPID私钥分发给A系统
    • 自身仅保留APPID与对应的公钥,用于后续的签名验证。
  2. A系统(服务调用方)

    • 从B系统处安全地获取并存储APPIDSM2私钥
    • 私钥是A系统的最高机密,绝不能泄露。它代表了A系统在数字世界的唯一身份。

实际环境中应有严格的密钥分发和保管流程

  • 密钥生成:在可信环境(推荐 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 系统)

  1. 读取文件流,计算 SM3 摘要 file_sm3(hex 小写)
  2. 生成随机 nonce
  3. 组装参与签名的参数(app_id、file_sm3、key_version、nonce),按字典序升序(key 名称)排序
  4. key=value 串联并用 & 连接,得到原始签名串
  5. 使用 SM2 私钥对签名串做签名(得到 bytes),用 base64 编码得到 sign
  6. 发起 HTTPS POST,Header 带上上述字段,Body 上传文件

验签(B 系统)——伪代码

  1. 接收请求并先做基础校验(app_id 存在性、参数齐全)
  2. 根据 app_id 查找公钥和 key_version,若未找到返回 1003
  3. Nonce 校验:在 Redis/内存缓存中尝试写入 nonce(SETNX),如果已存在返回 1002;写入成功设置 TTL(例如 24 小时或更短)
  4. 按字典序拼接同样的签名串并使用 SM2 公钥验签;验签失败返回 1001
  5. 验签成功后,比对 file_sm3 与实际上传文件计算的 SM3 值,若不一致返回 1004
  6. 校验通过后,继续业务处理并写审计日志

下面,我们来详细拆解一次完整的文件上传请求中,签名与验证的每一步。

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系统能生成完全一致的待签/待验字符串,必须遵循统一的规则:

  1. 按参数名的字典序(ASCII码)升序排列

    • 排序前:file_sm3, nonce, app_id, key_version
    • 排序后:app_id, file_sm3, key_version, nonce
  2. 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_versionsign

第二步:查找公钥

使用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_validfalse,说明签名无效(可能是参数被篡改,或私钥不匹配),拒绝请求(错误码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_idstringA系统的唯一标识符,用于查找公钥。
noncestring随机字符串,防重放,每次请求必须唯一。
key_versionstring密钥版本号,便于未来密钥平滑升级。
file_sm3string文件内容的SM3摘要值,用于校验文件完整性。
signstring对核心参数的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签名验证失败
1002Nonce已使用,疑似重放攻击
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>

国密算法应用

项目中主要使用了两种国密算法:

  1. SM2: 一种基于椭圆曲线的公钥密码算法,用于数字签名和验证
  2. 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. 签名流程

签名过程包含以下几个关键步骤:

  1. 读取文件内容: 使用Hutool读取Excel文件内容并转换为字符串
  2. 计算SM3摘要: 对文件内容进行SM3哈希运算,生成摘要
  3. 生成随机数: 创建唯一的nonce值,防止重放攻击
  4. 参数排序: 将签名相关参数按字典序排列
  5. 拼接签名字符串: 按照指定格式拼接待签名字符串
  6. 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. 验证流程

验证过程与签名过程相对应,主要包括:

  1. 获取公钥: 根据appId和密钥版本获取对应公钥
  2. 防重放检查: 验证nonce是否已被使用
  3. 文件摘要计算: 对待验证文件重新计算SM3摘要
  4. 参数拼接: 按照相同规则拼接待验证字符串
  5. 签名验证: 使用公钥验证签名的有效性
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算法的结合使用,实现了文件完整性保护和来源身份认证的双重安全保障。

在实际生产环境中,还需要考虑以下改进点:

  1. 密钥存储安全: 当前示例中密钥存储在内存中,生产环境应使用安全的密钥管理系统
  2. 性能优化: 对于大量文件处理场景,需要考虑并发处理和缓存机制
  3. 日志审计: 增加完整的操作日志记录,便于安全审计

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();}}
}

在这里插入图片描述

http://www.lryc.cn/news/625181.html

相关文章:

  • 多智能体架构设计:从单Agent到复杂系统的演进逻辑
  • 人工智能 | 基于大数据的皮肤病症状数据可视化分析系统(matlab源码)
  • 发布npmjs组件库
  • AopAutoConfiguration源码阅读
  • 鼠标右键没有“通过VSCode打开文件夹”
  • JVM学习笔记-----类加载
  • FPGA-Vivado2017.4-建立AXI4用于单片机与FPGA之间数据互通
  • Google 的 Opal:重新定义自动化的 AI 平台
  • WPF 打印报告图片大小的自适应(含完整示例与详解)
  • Rust 入门 生命周期-next2 (十九)
  • 牛津大学xDeepMind 自然语言处理(1)
  • Centos7 使用lamp架构部署wordpress
  • 接口和抽象类的区别(面试回答)
  • 【深度长文】Anthropic发布Prompt Engineering全新指南
  • Java面向对象三大特性:封装、继承、多态深度解析与实践应用
  • ⭐CVPR2025 RigGS:从 2D 视频到可编辑 3D 关节物体的建模新范式
  • 音频分类模型笔记
  • OOP三大特性
  • 【计算机视觉与深度学习实战】05计算机视觉与深度学习在蚊子检测中的应用综述与假设
  • 网络基础——协议认识
  • Pytest项目_day18(读取ini文件)
  • Unity 中控开发 多路串口服务器(一)
  • 深层语义知识图谱:提升NLP文本预处理效果的关键技术
  • C++ 多进程编程深度解析【C++进阶每日一学】
  • 一个基于纯前端技术实现的五子棋游戏,无需后端服务,直接在浏览器中运行。
  • 深度学习篇---softmax层
  • Maven 生命周期和插件
  • 大数据分析-读取文本文件内容进行词云图展示
  • 大厂求职 | 2026海尔校园招聘,启动!
  • Vuex 状态持久化企业级解决方案