国密实战(二):SM2 签名验签前后端对接全指南
作者:林 | 系列:国密实战 | 适合读者:Java 后端/架构师 + 前端开发者
一、背景与问题
在很多业务场景中,前端需要对请求参数进行签名,后端进行验签,以确保数据完整性和防篡改。传统方案多使用 RSA 签名,但在信创项目中,SM2 签名验签成为必选项。
典型场景:
- 前端提交表单数据,附带 SM2 签名
- 后端验签通过后才处理业务逻辑
- API 网关层面的请求签名校验
- 合同/文档电子签章
前后端对接 SM2 签名验签的核心挑战:
- 库的选择:Java 端用什么库?前端用什么库?
- 密钥格式约定:私钥和公钥的格式必须统一
- userId 的一致性:SM2 签名有一个独特的
userId参数,两端必须一致 - hash 参数的一致性:是否对消息做预哈希,两端必须统一
- 签名格式:
r || s拼接 vs DER 编码,两端必须约定
本文将给出 Java(Hutool)+ TypeScript(sm-crypto)的完整对接方案。
二、SM2 签名验签原理
2.1 签名过程
SM2 签名过程比 ECDSA 多了一个 Z 值(用户标识哈希)的计算。完整流程如下:
输入参数:
- 待签名消息 M
- 用户标识 ID(默认
"1234567812345678") - 私钥 d(256 位整数)
- 公钥 P = d × G = (xA, yA)
步骤:
计算用户标识哈希值 Z:
Z = SM3(ENTL ‖ ID ‖ a ‖ b ‖ xG ‖ yG ‖ xA ‖ yA)
其中 ENTL 是 ID 的比特长度(2 字节),a、b 是曲线参数,(xG, yG)是基点 G,(xA, yA)是公钥点。
拼接并哈希:e = SM3(Z ‖ M)
生成随机数 k(范围 [1, n-1]),计算椭圆曲线点:(x1, y1)= k × G
计算签名分量:
- r = (e + x1) mod n
- s = ((1 + d)⁻¹ × (k - r × d)) mod n
如果 r = 0 或 r + k = n,重新选择 k(概率极低)
签名结果为 (r, s),其中 r、s 各为 256 位整数
验签过程:
- 计算 Z 和 e(同签名步骤 1-2)
- 计算 t = (r + s) mod n,如果 t = 0 则验签失败
- 计算椭圆曲线点(x1, y1)= s × G + t × P
- 计算 R = (e + x1) mod n
- 如果 R = r,验签通过
2.2 关键参数:userId
SM2 签名中的 userId(也叫 ZA 计算中的 ID)是一个重要的参数:
| 场景 | userId | 说明 |
|---|---|---|
| 默认值 | "1234567812345678" | GM/T 0009-2012 规定的默认值 |
| 自定义 | 按业务约定 | 如用户身份证号、企业标识等 |
前后端的 userId 必须完全一致,否则计算出的 Z 值不同,e 值不同,签名结果不同,验签必然失败。而且这个错误不会有任何报错提示,只会得到"验签不通过"。
排查方法:如果验签一直失败且确认密钥正确,首先检查两端的 userId 是否一致。
2.3 签名格式
SM2 签名结果 (r, s) 有两种常见编码方式:
| 格式 | 说明 | 长度 |
|---|---|---|
r || s | 两个 32 字节整数直接拼接 | 64 字节 = 128 hex 字符 |
| DER 编码 | ASN.1 SEQUENCE { INTEGER r, INTEGER s } | 可变长度(通常 70-72 字节) |
r ‖ s 长度 = 32 + 32 = 64 bytes = 128 hex chars
DER 编码结构:
30 xx -- SEQUENCE
02 xx [r] -- INTEGER r(32 字节,可能有前导 00)
02 xx [s] -- INTEGER s(32 字节,可能有前导 00)0x00 以避免被解释为负数。因此 DER 编码的签名长度通常在 70-72 字节之间。三、方案选型
3.1 Java 端:Hutool
Hutool 是一个轻量级 Java 工具库,从 5.4.x 版本开始内置 SM2 签名验签支持,API 简洁,适合快速接入。
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<version>5.8.44</version>
</dependency>Hutool 与前端库的默认值不能靠印象对齐,下面在固定版本下逐项列出差异。
3.2 前端:sm-crypto
sm-crypto 是一个纯 JavaScript 实现的国密算法库,支持 SM2/SM3/SM4,可直接在浏览器和 Node.js 中使用。
npm install sm-crypto@0.3.14| 项目 | Hutool 5.8.44 | sm-crypto 0.3.14 | 对接要求 |
|---|---|---|---|
| 签名格式 | 固定宽度 `r | s` | |
| 默认 userId | 1234567812345678 | 1234567812345678 | 双方显式传入并固定编码 |
| SM2 密文顺序 | C1C3C2 | C1C3C2 | 加解密接口声明顺序 |
| 消息哈希 | 由调用方式决定 | hash: true 时执行 SM3 与 ZA 流程 | 测试向量固定原文、userId 与模式 |
默认行为可能随版本和 API 入口变化,生产代码应显式设置格式、userId、字符编码和 hash 模式,并用固定向量双向验签。
四、后端 Java 实现(Hutool)
4.1 Maven 依赖
<dependencies>
<!-- Hutool 加密模块 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<version>5.8.44</version>
</dependency>
<!-- BouncyCastle(Hutool 底层依赖) -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.84</version>
</dependency>
</dependencies>4.2 SM2 签名验签工具类
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.SM2;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.nio.charset.StandardCharsets;
import java.security.Security;
/**
* SM2 签名验签工具类
* 基于 Hutool 5.8.44 + BouncyCastle 1.84
*
* 默认行为:
* - userId: "1234567812345678"(GM/T 0009-2012 默认值)
* - 签名格式: r||s(128 hex 字符)
* - hash: false(不额外哈希,与 sm-crypto 的 hash: false 模式一致)
*/
public class Sm2SignUtil {
static {
// 注册 BouncyCastle Provider
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
// 默认 userId(GM/T 0009-2012 规定的默认值)
private static final String DEFAULT_USER_ID = "1234567812345678";
/**
* SM2 签名
*
* @param privateKeyHex 私钥 hex(64 字符)
* @param message 待签名消息(明文字符串)
* @param userId 用户标识(通常用默认值 "1234567812345678")
* @return 签名结果 hex 字符串(128 字符,r||s 拼接格式)
*/
public static String sign(String privateKeyHex, String message, String userId) {
// 创建 SM2 签名对象:SmUtil.sm2(私钥, 公钥)
SM2 sm2 = SmUtil.sm2(privateKeyHex, null);
// 设置 userId(影响 Z 值计算)
sm2.setUserId(userId.getBytes(StandardCharsets.UTF_8));
// 对消息进行签名(Hutool 默认返回 r||s 格式字节数组)
byte[] msgBytes = message.getBytes(StandardCharsets.UTF_8);
byte[] signBytes = sm2.sign(msgBytes);
// 转为 hex 字符串(128 字符)
return HexUtil.encodeHexStr(signBytes);
}
/**
* SM2 签名(使用默认 userId)
*
* @param privateKeyHex 私钥 hex(64 字符)
* @param message 待签名消息
* @return 签名结果 hex(128 字符)
*/
public static String sign(String privateKeyHex, String message) {
return sign(privateKeyHex, message, DEFAULT_USER_ID);
}
/**
* SM2 验签
*
* @param publicKeyHex 公钥 hex(130 字符,04 开头)
* @param message 原始消息
* @param signHex 签名 hex(128 字符)
* @param userId 用户标识
* @return 验签是否通过
*/
public static boolean verify(String publicKeyHex, String message, String signHex, String userId) {
SM2 sm2 = SmUtil.sm2(null, publicKeyHex);
sm2.setUserId(userId.getBytes(StandardCharsets.UTF_8));
byte[] msgBytes = message.getBytes(StandardCharsets.UTF_8);
byte[] signBytes = HexUtil.decodeHex(signHex);
return sm2.verify(msgBytes, signBytes);
}
/**
* SM2 验签(使用默认 userId)
*
* @param publicKeyHex 公钥 hex(130 字符,04 开头)
* @param message 原始消息
* @param signHex 签名 hex(128 字符)
* @return 验签是否通过
*/
public static boolean verify(String publicKeyHex, String message, String signHex) {
return verify(publicKeyHex, message, signHex, DEFAULT_USER_ID);
}
/**
* 生成 SM2 密钥对
*
* @return String[] {privateKeyHex, publicKeyHex}
*/
public static String[] generateKeyPair() {
SM2 sm2 = SmUtil.sm2();
return new String[] {
sm2.getPrivateKeyBase64(),
sm2.getPublicKeyBase64()
};
}
}4.3 Spring Boot 接口示例
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/sm2")
public class Sm2SignController {
// 后端持有的公钥(用于验签),从配置或 KeyStore 获取
private static final String PUBLIC_KEY = "04xxxxxxxxxxxx..."; // 130 字符 hex
/**
* 验签接口
* 前端传入消息和签名,后端验签
*
* 请求示例:
* POST /api/sm2/verify
* Content-Type: application/json
* {
* "message": "Hello, SM2!",
* "signature": "aabbccdd...(128字符)"
* }
*
* 响应示例:
* { "code": 200, "valid": true, "message": "验签通过" }
*/
@PostMapping("/verify")
public Map<String, Object> verify(@RequestBody Map<String, String> request) {
String message = request.get("message");
String signature = request.get("signature");
Map<String, Object> result = new HashMap<>();
try {
boolean valid = Sm2SignUtil.verify(PUBLIC_KEY, message, signature);
result.put("code", 200);
result.put("valid", valid);
result.put("message", valid ? "验签通过" : "验签失败");
} catch (Exception e) {
result.put("code", 500);
result.put("valid", false);
result.put("message", "验签异常: " + e.getMessage());
}
return result;
}
}五、前端 TypeScript 实现(sm-crypto)
5.1 安装依赖
npm install sm-crypto@0.3.14
# TypeScript 类型支持(社区维护)
npm install @types/sm-crypto --save-dev5.2 类型声明
如果没有 @types/sm-crypto,创建类型声明文件 src/types/sm-crypto.d.ts:
declare module 'sm-crypto' {
interface Sm2 {
/**
* SM2 签名
* @param msg 待签名消息
* @param privateKey 私钥 hex(64 字符)
* @param options 配置项
*/
doSignature(msg: string, privateKey: string, options?: {
pointPool?: any[]; // 预计算点池(提升性能)
der?: boolean; // 是否使用 DER 编码格式
hash?: boolean; // 是否对消息预哈希(SM3)
userId?: string; // 用户标识
publicKey?: string; // 公钥(某些场景需要)
}): string;
/**
* SM2 验签
* @param msg 原始消息
* @param sigValue 签名 hex
* @param publicKey 公钥 hex
* @param options 配置项
*/
doVerifySignature(msg: string, sigValue: string, publicKey: string, options?: {
pointPool?: any[];
der?: boolean;
hash?: boolean;
userId?: string;
}): boolean;
/** SM2 加密 */
doEncrypt(msg: string | number[], publicKey: string, cipherMode?: number): string;
/** SM2 解密 */
doDecrypt(cipherText: string, privateKey: string, cipherMode?: number): string | false;
/** 生成密钥对 */
generateKeyPairHex(): {
privateKey: string; // 64 字符 hex
publicKey: string; // 130 字符 hex(04 开头)
};
}
export const sm2: Sm2;
/** SM3 哈希 */
export function sm3(msg: string | number[]): string;
/** SM4 对称加密 */
export const sm4: {
encrypt(msg: string | number[], key: string | number[], options?: any): string;
decrypt(cipherText: string | number[], key: string | number[], options?: any): string | false;
};
}5.3 SM2 签名工具类
// src/utils/sm2.ts
import { sm2 } from 'sm-crypto';
// 默认 userId(GM/T 0009-2012 规定,与 Hutool 保持一致)
const DEFAULT_USER_ID = '1234567812345678';
export interface Sm2KeyPair {
privateKey: string; // 64 字符 hex
publicKey: string; // 130 字符 hex,04 开头
}
/**
* 生成 SM2 密钥对
*/
export function generateKeyPair(): Sm2KeyPair {
const keypair = sm2.generateKeyPairHex();
return {
privateKey: keypair.privateKey, // 64 字符 hex
publicKey: keypair.publicKey, // 130 字符 hex,04 开头
};
}
/**
* SM2 签名
*
* @param message 待签名消息
* @param privateKey 私钥 hex(64 字符)
* @param userId 用户标识(默认 "1234567812345678")
* @returns 签名结果 hex 字符串(128 字符,r||s 格式)
*/
export function sign(
message: string,
privateKey: string,
userId: string = DEFAULT_USER_ID
): string {
// hash: false 表示不对消息做额外哈希,与 Hutool 行为一致
// sm-crypto 内部会计算 Z 值并用 SM3 哈希,不需要外部再哈希
const signature = sm2.doSignature(message, privateKey, {
hash: false,
userId: userId,
});
return signature;
}
/**
* SM2 验签
*
* @param message 原始消息
* @param signature 签名 hex(128 字符)
* @param publicKey 公钥 hex(130 字符,04 开头)
* @param userId 用户标识
* @returns 验签是否通过
*/
export function verify(
message: string,
signature: string,
publicKey: string,
userId: string = DEFAULT_USER_ID
): boolean {
return sm2.doVerifySignature(message, signature, publicKey, {
hash: false,
userId: userId,
});
}
/**
* SM2 加密
*
* @param msg 明文
* @param publicKey 公钥 hex(130 字符,04 开头)
* @returns 密文 hex(C1C3C2 格式)
*/
export function encrypt(msg: string, publicKey: string): string {
return sm2.doEncrypt(msg, publicKey, 1); // 1 = C1C3C2
}
/**
* SM2 解密
*
* @param cipherText 密文 hex
* @param privateKey 私钥 hex(64 字符)
* @returns 明文或 false(解密失败)
*/
export function decrypt(cipherText: string, privateKey: string): string | false {
return sm2.doDecrypt(cipherText, privateKey, 1); // 1 = C1C3C2
}hash: true(sm-crypto 默认):先对消息做 SM3 哈希,再签名hash: false:直接对原始消息签名(Hutool 的行为)
前后端必须统一。本文方案前后端都使用 hash: false,保持一致。
如果两端 hash 参数不一致,验签会永远失败且无报错提示。这是 SM2 前后端对接常见的坑。
5.4 React 组件示例
import React, { useState } from 'react';
import { sign, verify, generateKeyPair } from '../utils/sm2';
const Sm2SignDemo: React.FC = () => {
const [message, setMessage] = useState('Hello, SM2!');
const [signature, setSignature] = useState('');
const [verifyResult, setVerifyResult] = useState<string>('');
// 模拟前端持有的私钥(实际项目中应从安全存储获取)
const PRIVATE_KEY = 'your_private_key_hex_64_chars';
const handleSign = () => {
const sig = sign(message, PRIVATE_KEY);
setSignature(sig);
console.log('签名结果:', sig);
};
const handleVerify = async () => {
// 调用后端验签接口
const response = await fetch('/api/sm2/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, signature }),
});
const result = await response.json();
setVerifyResult(result.message);
};
return (
<div>
<h2>SM2 签名验签演示</h2>
<div>
<label>消息:</label>
<input value={message} onChange={e => setMessage(e.target.value)} />
</div>
<button onClick={handleSign}>前端签名</button>
<div>
<label>签名:</label>
<textarea value={signature} readOnly rows={3} style={{ width: '100%' }} />
</div>
<button onClick={handleVerify}>后端验签</button>
{verifyResult && <p>验签结果:{verifyResult}</p>}
</div>
);
};
export default Sm2SignDemo;六、前后端联调完整流程
6.1 联调时序
前端 后端
│ │
│ 1. 用户输入消息 M │
│ │
│ 2. 用私钥签名 │
│ sig = SM2.sign(M, privateKey, userId) │
│ │
│ 3. 发送请求 ──────────────────────────────→│
│ POST /api/sm2/verify │
│ { message: M, signature: sig } │
│ │
│ 4. 用公钥验签 │
│ valid = SM2.verify( │
│ M, sig, pubKey, │
│ userId) │
│ │
│ 5. 返回结果 ←──────────────────────────────│
│ { code: 200, valid: true } │
│ │6.2 实际请求示例
前端发起请求:
curl -X POST http://localhost:8080/api/sm2/verify \
-H "Content-Type: application/json" \
-d '{
"message": "{\"orderId\":\"20240101001\",\"amount\":9999.00}",
"signature": "a1b2c3d4e5f6...(共128字符)...7890abcdef"
}'后端响应:
{
"code": 200,
"valid": true,
"message": "验签通过"
}6.3 完整测试代码
Java 端测试:
public class Sm2SignTest {
public static void main(String[] args) {
// 测试密钥对(实际项目中从证书或配置获取)
String privateKey = "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344";
String publicKey = "04"
+ "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344"
+ "aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344";
String message = "Hello, SM2!";
String userId = "1234567812345678";
// 1. 签名
String signature = Sm2SignUtil.sign(privateKey, message, userId);
System.out.println("签名结果: " + signature);
System.out.println("签名长度: " + signature.length() + " 字符"); // 128
// 2. 验签(使用正确的公钥和 userId)
boolean valid = Sm2SignUtil.verify(publicKey, message, signature, userId);
System.out.println("验签结果(正确参数): " + valid); // true
// 3. 验签失败场景:错误的 userId
boolean validWrongUserId = Sm2SignUtil.verify(publicKey, message, signature, "wrong_user_id");
System.out.println("验签结果(错误userId): " + validWrongUserId); // false
// 4. 验签失败场景:篡改消息
boolean validTampered = Sm2SignUtil.verify(publicKey, "Tampered message", signature, userId);
System.out.println("验签结果(篡改消息): " + validTampered); // false
// 5. 验签失败场景:错误公钥
String wrongPubKey = "04" + "00".repeat(64);
boolean validWrongKey = Sm2SignUtil.verify(wrongPubKey, message, signature, userId);
System.out.println("验签结果(错误公钥): " + validWrongKey); // false
}
}TypeScript 端测试:
import { sign, verify, generateKeyPair } from './utils/sm2';
// 1. 生成密钥对
const keyPair = generateKeyPair();
console.log('私钥:', keyPair.privateKey, '长度:', keyPair.privateKey.length); // 64
console.log('公钥:', keyPair.publicKey, '长度:', keyPair.publicKey.length); // 130
// 2. 签名
const message = 'Hello, SM2!';
const signature = sign(message, keyPair.privateKey);
console.log('签名:', signature, '长度:', signature.length); // 128
// 3. 本地验签
const valid = verify(message, signature, keyPair.publicKey);
console.log('本地验签:', valid); // true
// 4. 篡改消息验签
const validTampered = verify('Tampered', signature, keyPair.publicKey);
console.log('篡改验签:', validTampered); // false
七、关键注意事项详解
7.1 userId 一致性
| 项目 | Hutool 默认值 | sm-crypto 默认值 | 是否一致 |
|---|---|---|---|
| userId | "1234567812345678" | "1234567812345678" | ✅ |
如果业务需要自定义 userId,两端都需要显式设置:
// Java 端
sm2.setUserId("custom_user_id".getBytes(StandardCharsets.UTF_8));// TypeScript 端
sm2.doSignature(message, privateKey, {
userId: 'custom_user_id',
});7.2 hash 参数统一
这是容易被忽略的坑:
| 库 | hash=true 行为 | hash=false 行为 |
|---|---|---|
| Hutool | 对消息先做 SM3 哈希再签名 | 直接对原始消息签名(默认) |
| sm-crypto | 对消息先做 SM3 哈希再签名(默认) | 直接对原始消息签名 |
⚠️ 必须统一:要么两端都
hash: true,要么都hash: false。本文方案采用hash: false。
建议:在项目文档和代码注释中明确记录 hash 参数的选择,避免后续维护者踩坑。
7.3 签名格式统一
| 库 | 默认签名格式 | DER 支持 |
|---|---|---|
| Hutool | r || s(128 hex 字符) | 需手动转换 |
| sm-crypto | r || s(128 hex 字符) | der: true 参数 |
本文方案使用默认的 r || s 格式,两端一致。
格式转换(如果需要 DER 格式):
// r||s → DER
public static String rsToDer(String rsHex) {
byte[] rs = HexUtil.decodeHex(rsHex);
byte[] r = new byte[32];
byte[] s = new byte[32];
System.arraycopy(rs, 0, r, 0, 32);
System.arraycopy(rs, 32, s, 0, 32);
ASN1EncodableVector v = new ASN1EncodableVector();
v.add(new ASN1Integer(new BigInteger(1, r))); // 注意:BigInteger(1, ...) 避免负数
v.add(new ASN1Integer(new BigInteger(1, s)));
return HexUtil.encodeHexStr(new DERSequence(v).getEncoded());
}7.4 字符编码
签名时消息的字节编码必须一致:
// Java 端:显式使用 UTF-8
byte[] msgBytes = message.getBytes(StandardCharsets.UTF_8);// TypeScript 端:JavaScript 字符串默认 UTF-16
// sm-crypto 内部会处理编码,通常不需要额外处理
// 但如果消息包含中文,确保两端编码一致
八、踩坑总结
| 坑点 | 现象 | 原因 | 解决方案 |
|---|---|---|---|
| userId 不一致 | 验签永远失败,无报错 | SM2 签名的 Z 值计算依赖 userId | 两端统一使用 "1234567812345678" |
| hash 参数不一致 | 验签永远失败 | 消息处理方式不同 | 两端统一使用 hash: false |
| 签名格式不一致 | 验签失败 | r||s vs DER | 两端统一使用 r||s 格式 |
| 公钥缺少 04 前缀 | 解码报错 Invalid point encoding | sm-crypto 要求完整格式 | 补齐 04 前缀 |
| 私钥前导零丢失 | 签名结果错误 | BigInteger 去掉了前导零 | 补齐到 64 字符 |
| 编码不一致 | 验签失败 | Java 和 JS 的字符串编码差异 | 明确使用 UTF-8 |
| 中文签名不一致 | 验签失败 | Unicode 规范化差异 | 对消息做 NFC 规范化 |
九、安全注意事项
- 私钥保护:前端私钥不应硬编码在 JS 中,建议使用 Web Crypto API 或 HSM 方案
- 传输安全:签名验签请求必须走 HTTPS,防止中间人篡改
- 重放攻击:签名中应包含时间戳和 nonce,后端校验时效性
- 密钥轮换:定期更换密钥对,支持多版本公钥并存
- 日志脱敏:日志中不打印完整私钥和签名,只记录关键信息
// 重放攻击防护示例
@PostMapping("/verify")
public Map<String, Object> verify(@RequestBody SignRequest request) {
// 1. 检查时间戳(5 分钟内有效)
long now = System.currentTimeMillis();
if (Math.abs(now - request.getTimestamp()) > 5 * 60 * 1000) {
return Map.of("code", 403, "message", "签名已过期");
}
// 2. 检查 nonce 是否已使用(Redis 缓存)
if (nonceCache.exists(request.getNonce())) {
return Map.of("code", 403, "message", "签名已使用");
}
nonceCache.set(request.getNonce(), "1", 5, TimeUnit.MINUTES);
// 3. 验签
boolean valid = Sm2SignUtil.verify(PUBLIC_KEY, request.getMessage(), request.getSignature());
return Map.of("code", 200, "valid", valid);
}十、总结与成熟实践
- 库的选择:Java 端推荐 Hutool(简洁易用),前端推荐 sm-crypto(纯 JS,无原生依赖)
- 默认参数优先:使用默认的
userId="1234567812345678"和r||s签名格式,减少出错概率 - hash 参数统一:这是容易被忽略的坑,前后端必须统一
hash: false或hash: true - 密钥格式统一:私钥 64 字符 hex,公钥 130 字符 hex(04 开头)
- 先本地验签,再联调:分别在前后端独立验签通过后,再进行跨端联调
- 测试异常场景:篡改消息、错误 userId、错误公钥等,确保验签能正确拒绝
- 防重放:签名中加入时间戳 + nonce,后端校验时效性
十一、参考资料
- Hutool SM2 官方文档
- sm-crypto npm 包
- GM/T 0009-2012 SM2 密码算法使用规范
- GM/T 0003.2-2012 SM2 签名算法
- GB/T 32918-2016 SM2 椭圆曲线公钥密码算法