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
英文地址,請注明出處:http://en.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; 本節素材…