文章目錄
- 概述
- 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();}}
}