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.pswp.cn/web/93587.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/93587.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【MyBatis-Plus】一、快速入门

这里写自定义目录标题MyBatis-Plus 概述快速入门入门案例常用注解常见配置MyBatis-Plus 概述 MyBatis-Plus 简介&#xff1a; MyBatis-Plus 是在 MyBatis 基础上开发的一个 增强工具包&#xff0c;它简化了 MyBatis 的开发&#xff0c;减少了大量重复代码。它保持了 MyBatis …

PostgreSQL导入mimic4

一、PostgreSQL连接验证 正确连接命令 使用psql工具连接目标数据库&#xff0c;格式为&#xff1a;psql -h 127.0.0.1 -U 用户名 -d 数据库名 --password 示例&#xff08;用户名Shinelon&#xff0c;数据库mimic&#xff09;&#xff1a;psql -h 127.0.0.1 -U Shinelon -d mi…

css中 hsl() 的用法

好的 &#x1f44d; 我来详细介绍一下 CSS hsl() 的用法。1. 基本语法 color: hsl(hue, saturation, lightness);hue&#xff08;色相&#xff09; 取值范围&#xff1a;0 ~ 360&#xff08;角度值&#xff0c;代表色环的角度&#xff09;0 或 360 → 红色120 → 绿色240 → 蓝…

企业级Spring事务管理:从单体应用到微服务分布式事务完整方案

企业级Spring事务管理&#xff1a;从单体应用到微服务分布式事务完整方案 &#x1f31f; 你好&#xff0c;我是 励志成为糕手 &#xff01; &#x1f30c; 在代码的宇宙中&#xff0c;我是那个追逐优雅与性能的星际旅人。 ✨ 每一行代码都是我种下的星光&#xff0c;在逻辑的土…

继续记录面试题

坐在工位&#xff0c;没事干心慌的不行&#xff0c;可能也是房贷压的。一闲下来就开始胡思乱想&#xff0c;无法沉下心去背那些八股文。这才刚刚接到离职通知第三天啊。而且、我还在坐班呢&#xff01;&#xff01;&#xff01; 哎、怪不得有句老话说的&#xff0c;人穷志短&a…

从零开始学习:深度学习(基础入门版)(第2天)

&#xff08;一&#xff09;在pycharm软件中&#xff0c;用python语言&#xff0c;opencv库实现以下功能(1.1)图片的边界填充核心流程&#xff1a;读取原始图像使用 cv2.imread() 加载名为 yueshan.png 的图像文件统一边界参数设定四周留白尺寸均为 50px&#xff08;上下左右各…

HTTP协议-3-HTTP/2是如何维持长连接的?

先说结论&#xff1a;HTTP/2的“长连接” 一个TCP连接 多路复用 二进制帧 流控制 持久会话管理 它不只是“连接不断”&#xff0c;更关键的是&#xff1a;在这个长连接上&#xff0c;可以同时并发传输成百上千个请求和响应&#xff0c;互不阻塞&#xff01; 1、HTTP/2的“…

图解希尔排序C语言实现

1 希尔排序 希尔排序&#xff08;Shell Sort&#xff09;是D.L.Shell于1959年提出来的一种排序算法&#xff0c;在这之前排序算法的时间复杂度基本都是O(n)&#xff0c;希尔排序算法是突破这个时间复杂度的第一批算法之一。 1.1 基本概念与原理 希尔排序通过将原始列表分割成若…

网络协议——HTTPS协议

目录 一、HTTPS是什么 加密是什么 二、HTTPS的工作过程 &#xff08;一&#xff09;对称加密 &#xff08;二&#xff09;非对称加密 &#xff08;三&#xff09;在非对称加密的基础上&#xff0c;引入证书校验 证书是什么 证书的内容 用证书解决中间人攻击 三、总结 …

React 基础实战:从组件到案例全解析

React 基础实战专栏:从组件到案例全解析 本专栏围绕 React 核心概念(组件、Props、State、生命周期)展开,通过 6个实战案例+核心知识点拆解,帮你掌握 React 基础开发逻辑,每篇聚焦1个实战场景,搭配完整代码与原理讲解,适合 React 入门者巩固基础。 专栏目录 【组件传…

ARM芯片架构之CoreSight Channel Interface 介绍

CoreSight Channel Interface&#xff08;通道接口&#xff09;详解1. 概述 Channel Interface 是 ARM CoreSight 架构中用于在不同组件之间传递触发事件的专用接口。它是 Event Interface 的增强版本&#xff0c;支持多通道、双向通信&#xff0c;以及同步与异步两种时钟域连接…

Blender模拟结构光3D Scanner(二)投影仪内参数匹配

关于投影仪外参的设置可参见前一篇文章 Blender模拟结构光3D Scanner&#xff08;一&#xff09;外参数匹配-CSDN博客 使用Projectors插件模拟投影仪 Step 1 在Github下载插件&#xff08;https://github.com/Ocupe/Projectors&#xff09;。下载zip压缩包即可&#xff0c;无…

synchronized的作用

目录 一、核心作用 二、实现原理&#xff1a;基于"对象锁" 三、使用方式 四、锁的优化 五、优缺点 六、总结 synchronized 是 Java 中用于解决多线程并发安全问题的核心关键字&#xff0c;它的主要作用是实现线程间的同步&#xff0c;确保多个线程在访问共享资…

机试备考笔记 14/31

2025年8月14日 小结&#xff1a;&#xff08;17号整理14号的笔记&#xff0c;这辈子真是有了w(&#xff9f;Д&#xff9f;)w&#xff09;昨天摔了跤大的&#xff0c;今天好妈妈在家&#xff0c;松弛。省流&#xff1a;6道中等&#xff0c;明天只学了10分钟嘻嘻 目录LeetCode22…

dolphinscheduler中任务输出变量的问题出现ArrayIndexOutOfBoundsException

一段脚本任务如下&#xff1a;ret/data/dolphinscheduler/loadOraTable.sh "yonbip/yonbip10.16.10.69:1521/orcl" "select t.bondcontractno,t.olcunissuemny from yonbip.bond_contract t " "/dmp/biz" "bip" "2025-08-13"…

OpenCv(二)——边界填充、阈值处理

目录 一、边界填充&#xff08;Border Padding&#xff09; 1. 常见填充类型及效果 2.代码示例 &#xff08;1&#xff09;constant边界填充&#xff0c;填充指定宽度的像素 &#xff08;2&#xff09;REFLECT镜像边界填充 &#xff08;3&#xff09;REFLECT_101镜像边界…

Leetcode 15 java

今天复习一下翻转二叉树 226. 翻转二叉树 给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 示例 1&#xff1a; 输入&#xff1a;root [4,2,7,1,3,6,9] 输出&#xff1a;[4,7,2,9,6,3,1]示例 2&#xff1a; 输入&#xff1a;root [2…

嵌入式学习的第四十九天-时钟+EPIT+GPT定时器

一、时钟1.时钟系统基本概念&#xff08;1&#xff09;PLL (锁相环, Phase-Locked Loop)作用&#xff1a;PLL是一种反馈控制电路&#xff0c;用于生成稳定的高频时钟信号。它通过将输出时钟与参考时钟进行比较和调整&#xff0c;可以产生比输入参考时钟频率高得多的输出时钟。倍…

Python Sqlalchemy数据库连接

Python Sqlalchemy数据库连接一、连接数据二、模型三、ORM操作一、连接数据 from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker# 1. 连接数据库 dbHost postgres://用户名:密码主机:端口/数据库名 engine create_engine(dbHost) # create_engi…

【Node.js】ECMAScript标准 以及 npm安装

目录 一、 ECMAScript标准 - 默认导出和导入 二、ECMAScript标准 - 命名导出和导入 三、包的概念 五、 npm - 安装所有依赖 六、 npm - 全局软件包 Node.js总结 总结不易~ 本章节对我有很大的收获&#xff0c; 希望对你也是&#xff01;&#xff01;&#xff01; 本节素材…