主题
开发者接入指南-签名与加密
获取Token成功后,开发者调用开放能力接口,需要携带token,使用token获取的access_token,在请求头中设置Authorization参数,参数值为:Bearer + token。
同时,云从公有云平台使用国家密码局认定的国产密码算法进行传输加解密及签名认证,以保障数据安全性。 传入报文使用SM3算法计算摘要,SM2算法生成签名,使用SM4算法加密。接口返回报文同样使用SM4算法加密,调用方需使用密钥进行解密。具体调用流程及示例代码如下。
调用流程:

参数解释:
nonceStr: 随机字符串,用于生成签名,用户需自行生成
timestamp: 时间戳,用于生成签名
digest: 对业务参数明文计算的摘要
workKey: 随机生成的16位长度的sm4算法的对称密钥
keyCipher: workKey密文
contentCipher: 业务参数密文
示例代码
以联网核查接口为例,示例代码如下
JAVA版本
java
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.ECKeyUtil;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import cn.hutool.crypto.digest.SM3;
import cn.hutool.crypto.symmetric.SM4;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.bouncycastle.crypto.engines.SM2Engine;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class InterfaceUseTest {
/**
* sm2公钥
*/
public static String SM2_PUB = "xxxxxxxxxxxxxxxx";
public static String HTTP_ADDRESS = "https://api-ai.cloudwalk.com";
/**
* 身份令牌
*/
private static String accessToken = "xxxxxxxxx";
/**
* 请求路径
*/
private static final String URL = HTTP_ADDRESS + "/ai-cloud-cweis/netCheck/checkCIdAndName";
public static void main(String[] args) throws Exception {
checkIdNameSm(accessToken, "张三", "123");
}
private static void checkIdNameSm(String token, String name, String id) {
// 随机字符串,用于生成签名
String nonceStr = UUID.fastUUID().toString(true);
System.out.println("nonceStr:" + nonceStr);
//时间戳,用于生成签名
long timestamp = System.currentTimeMillis();
System.out.println("timestamp:" + timestamp);
//业务参数
Map<String, Object> busMap = new HashMap<>();
busMap.put("cId", id);
busMap.put("cName", name);
String busFlowId = RandomUtil.randomString(16);
System.out.println("busFlowId:" + busFlowId);
busMap.put("busFlowId", busFlowId);
Map<String, Object> signMap = new HashMap<>(busMap);
Map<String, Object> sortedMap = sortByKey(signMap);
//计算摘要
String digest = sm3Digest(JSON.toJSONString(sortedMap), nonceStr);
System.out.println("digest:" + digest);
//随机生成16位长度的workKey作为sm4的对称密钥
String workKey = genSm4Key();
System.out.println("workKey:" + workKey);
// 进行sm4加密
String contentCipher = sm4Encrypt(JSON.toJSONString(busMap), workKey);
System.out.println("contentCipher:" + contentCipher);
// 对workKey进行非对称sm2加密
String keyCipher = sm2Encrypt(workKey);
System.out.println("keyCipher:" + keyCipher);
Map<String, Object> finalMap = new HashMap<>();
finalMap.put("contentCipher", contentCipher);
finalMap.put("keyCipher", keyCipher);
finalMap.put("digest", digest);
finalMap.put("timestamp", timestamp);
finalMap.put("nonceStr", nonceStr);
// 创建一个POST请求
HttpRequest request = HttpUtil.createPost(URL);
// 设置请求头参数
request.header("Content-Type", "application/json");
request.header("Authorization", "Bearer " + token);
// 设置请求体
request.body(JSON.toJSONString(finalMap));
// 发送请求并获取响应
String result = request.execute().body();
SM4 sm4 = new SM4(Mode.ECB, Padding.PKCS5Padding, workKey.getBytes(StandardCharsets.UTF_8));
try {
JSONObject object = JSON.parseObject(result);
String data = object.getString("data");
// 响应报文解密
String plain = sm4.decryptStr(data);
object.replace("data", JSON.parseObject(plain));
System.out.println(object.toJSONString());
} catch (Exception e) {
System.out.println(result);
}
}
public static Map<String, Object> sortByKey(Map<String, Object> map) {
List<Map.Entry<String, Object>> list = new ArrayList<>(map.entrySet());
list.sort(Map.Entry.comparingByKey());
Map<String, String> result = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : list) {
result.put(entry.getKey(), entry.getValue());
}
return result;
}
private static String sm2Encrypt(String plain) {
SM2 sm2 = new SM2(null, ECKeyUtil.toSm2PublicParams("04".concat(SM2_PUB)));
sm2.setMode(SM2Engine.Mode.C1C2C3);
return sm2.encryptHex(plain, KeyType.PublicKey);
}
private static String sm3Digest(String content, String nonceStr) {
String salt = nonceStr.substring(nonceStr.length() - 16);
SM3 sm3 = new SM3(salt.getBytes(StandardCharsets.UTF_8));
return sm3.digestHex(content.getBytes(StandardCharsets.UTF_8));
}
private static String genSm4Key() {
return UUID.fastUUID().toString(true).substring(8, 24);
}
private static String sm4Encrypt(String plain, String key) {
SM4 sm4 = new SM4(Mode.ECB, Padding.PKCS5Padding, key.getBytes(StandardCharsets.UTF_8));
return sm4.encryptHex(plain, StandardCharsets.UTF_8);
}
}GO语言版本
go
package main
import (
"bytes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"sort"
"time"
"github.com/tjfoc/gmsm/sm2" // SM2 非对称加密
"github.com/tjfoc/gmsm/sm3" // SM3 摘要
"github.com/tjfoc/gmsm/sm4" // SM4 对称加密
)
const (
// SM2 公钥(去掉前缀 0x04 的裸公钥 hex,下面拼接时会加上 04)
SM2PubHex = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
// 接口地址
HTTPAddress = "https://api-ai.cloudwalk.com"
// 请求路径
CheckCIDAndNamePath = "/ai-cloud-cweis/netCheck/checkCIdAndName"
// 身份令牌
AccessToken = "xxxxxxxxxxxxxxxx"
)
func main() {
if err := checkIdNameSm(AccessToken, "张三", "123"); err != nil {
fmt.Println("checkIdNameSm error:", err)
}
}
// checkIdNameSm 调用云从接口,完成 SM2/SM3/SM4 的加解密和签名逻辑
func checkIdNameSm(token, name, id string) error {
// 随机字符串,用于生成签名(这里用 UUID v4 简单替代)
nonceStr, err := genUUID()
if err != nil {
return err
}
fmt.Println("nonceStr:", nonceStr)
// 时间戳,用于生成签名
timestamp := time.Now().UnixMilli()
fmt.Println("timestamp:", timestamp)
// 业务参数
busMap := map[string]any{
"cId": id,
"cName": name,
}
// 业务流水号,随机 16 位字符串
busFlowId, err := randomString(16)
if err != nil {
return err
}
fmt.Println("busFlowId:", busFlowId)
busMap["busFlowId"] = busFlowId
// 按 key 排序的 map,用于签名摘要
sortedMap := sortByKey(busMap)
// 计算摘要,等价 sm3Digest(JSON(sortedMap), nonceStr)
sortedJSON, err := json.Marshal(sortedMap)
if err != nil {
return err
}
digest, err := sm3Digest(string(sortedJSON), nonceStr)
if err != nil {
return err
}
fmt.Println("digest:", digest)
// 随机生成 16 字符长度的 workKey 作为 sm4 的对称密钥
workKey, err := genSm4Key()
if err != nil {
return err
}
fmt.Println("workKey:", workKey)
// 进行 sm4 加密业务内容
busJSON, err := json.Marshal(busMap)
if err != nil {
return err
}
contentCipher, err := sm4Encrypt(string(busJSON), workKey)
if err != nil {
return err
}
fmt.Println("contentCipher:", contentCipher)
// 对 workKey 进行非对称 sm2 加密
keyCipher, err := sm2Encrypt(workKey)
if err != nil {
return err
}
fmt.Println("keyCipher:", keyCipher)
// 组装最终请求体
finalMap := map[string]any{
"contentCipher": contentCipher,
"keyCipher": keyCipher,
"digest": digest,
"timestamp": timestamp,
"nonceStr": nonceStr,
}
bodyBytes, err := json.Marshal(finalMap)
if err != nil {
return err
}
// 发送 POST 请求
url := HTTPAddress + CheckCIDAndNamePath
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(bodyBytes))
if err != nil {
return err
}
// 设置请求头参数
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
// 使用同一个 workKey 初始化 SM4,用于解密返回 data 字段
respSM4, err := newSM4ECB(workKey)
if err != nil {
return err
}
// 尝试解析返回 JSON
var respObj map[string]any
if err := json.Unmarshal(respBytes, &respObj); err != nil {
// 若非 JSON,直接输出
fmt.Println(string(respBytes))
return nil
}
dataVal, ok := respObj["data"].(string)
if !ok || dataVal == "" {
// 如果 data 不是字符串,则直接打印
fmt.Println(string(respBytes))
return nil
}
// 解密 data
plainData, err := sm4DecryptWith(ctxSM4{c: respSM4}, dataVal)
if err != nil {
// 解密失败则原样输出
fmt.Println(string(respBytes))
return nil
}
// 把解密后的 data 再解析成 JSON
var plainObj any
if err := json.Unmarshal([]byte(plainData), &plainObj); err != nil {
// 不是 JSON 就当普通字符串
plainObj = plainData
}
respObj["data"] = plainObj
finalResp, _ := json.Marshal(respObj)
fmt.Println(string(finalResp))
return nil
}
// sortByKey 按 key 排序并返回新的 map,用于签名
func sortByKey(m map[string]any) map[string]any {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
res := make(map[string]any, len(m))
for _, k := range keys {
res[k] = m[k]
}
return res
}
// sm2Encrypt 使用公钥对明文进行 SM2 加密,并返回十六进制字符串
func sm2Encrypt(plain string) (string, error) {
// hutool 使用 "04" + SM2_PUB 作为未压缩公钥
pubKeyHex := "04" + SM2PubHex
pubBytes, err := hex.DecodeString(pubKeyHex)
if err != nil {
return "", err
}
// 未压缩公钥格式: 04 + X(32字节) + Y(32字节) = 65字节
if len(pubBytes) != 65 || pubBytes[0] != 0x04 {
return "", fmt.Errorf("invalid uncompressed public key format")
}
// 解析 X 和 Y 坐标
curve := sm2.P256Sm2()
x := new(big.Int).SetBytes(pubBytes[1:33])
y := new(big.Int).SetBytes(pubBytes[33:65])
// 创建公钥
pubKey := &sm2.PublicKey{
Curve: curve,
X: x,
Y: y,
}
// 使用 C1C2C3 模式加密(与 Java Hutool 的 SM2Engine.Mode.C1C2C3 一致)
cipher, err := sm2.Encrypt(pubKey, []byte(plain), rand.Reader, sm2.C1C2C3)
if err != nil {
return "", err
}
return hex.EncodeToString(cipher), nil
}
// sm3Digest 计算 SM3 摘要,使用 nonceStr 最后 16 位作为盐
func sm3Digest(content, nonceStr string) (string, error) {
if len(nonceStr) < 16 {
return "", fmt.Errorf("nonceStr length must be >= 16")
}
salt := nonceStr[len(nonceStr)-16:]
h := sm3.New()
_, _ = h.Write([]byte(salt))
_, _ = h.Write([]byte(content))
sum := h.Sum(nil)
return hex.EncodeToString(sum), nil
}
// genSm4Key 生成 16 字符长度的 SM4 workKey
func genSm4Key() (string, error) {
u, err := genUUID()
if err != nil {
return "", err
}
if len(u) < 24 {
return "", fmt.Errorf("generated uuid too short")
}
// 等价于 Java: UUID.fastUUID().toString(true).substring(8, 24)
return u[8:24], nil
}
// sm4Encrypt 使用 SM4/ECB/PKCS5Padding 加密,并返回 hex
func sm4Encrypt(plain, key string) (string, error) {
c, err := newSM4ECB(key)
if err != nil {
return "", err
}
cipherBytes, err := sm4EncryptWith(ctxSM4{c: c}, []byte(plain))
if err != nil {
return "", err
}
return hex.EncodeToString(cipherBytes), nil
}
// ----------------- SM4 ECB + PKCS5Padding 封装 -----------------
// ctxSM4 简单包装 SM4 Cipher
type ctxSM4 struct {
c cipher.Block
}
// newSM4ECB 创建基于 key 的 SM4 ECB Cipher
func newSM4ECB(key string) (cipher.Block, error) {
kb := []byte(key)
if len(kb) != 16 {
return nil, fmt.Errorf("sm4 key length must be 16 bytes, got %d", len(kb))
}
return sm4.NewCipher(kb)
}
// sm4EncryptWith 使用 SM4 ECB + PKCS5Padding 加密
func sm4EncryptWith(ctx ctxSM4, src []byte) ([]byte, error) {
bs := sm4.BlockSize
src = pkcs5Padding(src, bs)
dst := make([]byte, len(src))
for start := 0; start < len(src); start += bs {
end := start + bs
ctx.c.Encrypt(dst[start:end], src[start:end])
}
return dst, nil
}
// sm4DecryptWith 使用 SM4 ECB + PKCS5Padding 解密 hexCipher
func sm4DecryptWith(ctx ctxSM4, hexCipher string) (string, error) {
cipherBytes, err := hex.DecodeString(hexCipher)
if err != nil {
return "", err
}
bs := sm4.BlockSize
if len(cipherBytes)%bs != 0 {
return "", fmt.Errorf("cipher length not multiple of block size")
}
plain := make([]byte, len(cipherBytes))
for start := 0; start < len(cipherBytes); start += bs {
end := start + bs
ctx.c.Decrypt(plain[start:end], cipherBytes[start:end])
}
plain, err = pkcs5UnPadding(plain)
if err != nil {
return "", err
}
return string(plain), nil
}
// pkcs5Padding PKCS5/7 填充
func pkcs5Padding(src []byte, blockSize int) []byte {
padding := blockSize - len(src)%blockSize
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(src, padText...)
}
// pkcs5UnPadding 取消 PKCS5/7 填充
func pkcs5UnPadding(src []byte) ([]byte, error) {
if len(src) == 0 {
return nil, fmt.Errorf("pkcs5UnPadding: empty src")
}
length := len(src)
unPadding := int(src[length-1])
if unPadding <= 0 || unPadding > length {
return nil, fmt.Errorf("pkcs5UnPadding: invalid padding")
}
return src[:(length - unPadding)], nil
}
// ----------------- 工具函数 -----------------
// genUUID 生成一个不带连字符的 32 位 UUID 字符串
func genUUID() (string, error) {
// 16 字节 = 128 bit UUID
u := make([]byte, 16)
if _, err := rand.Read(u); err != nil {
return "", err
}
// 设置 version 和 variant,标准 UUID v4
u[6] = (u[6] & 0x0f) | 0x40
u[8] = (u[8] & 0x3f) | 0x80
// 转成 32 位 hex 字符串
return hex.EncodeToString(u), nil
}
// randomString 生成指定长度的随机字符串 [0-9a-zA-Z]
func randomString(n int) (string, error) {
const letters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", err
}
for i := range b {
b[i] = letters[int(b[i])%len(letters)]
}
return string(b), nil
}