文档中心

接入流程

一、概述

为确保接口的安全性和数据的完整性,第三方服务商在调用开放平台接口时,需要使用 HMAC-SHA256 算法进行加签和验签。通过在请求中加入签名,可以有效防止请求被篡改,保障数据传输的安全性。

二、接入准备

1. IP 白名单报备

为确保服务的合法性和安全性,同时便于监管部门进行监督和管理,请在接入平台前联系平台运营人员进行 IP 白名单报备。

报备信息包括:

  • 服务商名称
  • 联系人信息
  • 服务器出口 IP 地址
  • 预计调用频率

2. 获取访问凭证

联系平台运营人员获取以下访问凭证:

  • accessKeyId:用于标识服务商身份的唯一 ID
  • accessKeySecret:用于生成签名的密钥,请妥善保管

3. 环境准备

测试环境: http://tech-test-api.jxszpt.com 生产环境: https://tech-api.jxszpt.com

三、签名算法

签名生成步骤

  1. 准备签名参数:accessKeyIdaccessKeySecrettimestamp
  2. 将参数按格式拼接:${accessKeyId}-${accessKeySecret}-${timestamp}
  3. 使用 HMAC-SHA256 算法对拼接字符串进行签名
  4. 将签名结果转换为十六进制字符串

请求头设置

将签名信息添加到请求头中:

Header 名称描述示例
X-AccessKeyId访问密钥 IDyour_access_key_id
X-Signature生成的签名a1b2c3d4e5f6...
X-Timestamp时间戳(毫秒)1692518400000
Content-Type内容类型application/json

四、代码实现示例

TypeScript/Node.js

import crypto from 'crypto';
import axios from 'axios';

class ApiClient {
    private accessKeyId: string;
    private accessKeySecret: string;
    private baseURL: string;

    constructor(accessKeyId: string, accessKeySecret: string, baseURL: string) {
        this.accessKeyId = accessKeyId;
        this.accessKeySecret = accessKeySecret;
        this.baseURL = baseURL;
    }

    /**
     * 生成签名
     */
    private generateSignature(timestamp: string): string {
        const stringToSign = `${this.accessKeyId}-${this.accessKeySecret}-${timestamp}`;
        const hmac = crypto.createHmac('sha256', this.accessKeySecret);
        hmac.update(stringToSign);
        return hmac.digest('hex');
    }

    /**
     * 发起 API 请求
     */
    async request(method: string, endpoint: string, data?: any) {
        const timestamp = Date.now().toString();
        const signature = this.generateSignature(timestamp);

        try {
            const response = await axios({
                method: method as any,
                url: `${this.baseURL}${endpoint}`,
                data,
                headers: {
                    'Content-Type': 'application/json',
                    'X-AccessKeyId': this.accessKeyId,
                    'X-Signature': signature,
                    'X-Timestamp': timestamp
                }
            });
            return response.data;
        } catch (error) {
            console.error('API 请求失败:', error);
            throw error;
        }
    }
}

// 使用示例
const client = new ApiClient('your_access_key_id', 'your_access_key_secret', 'https://tech-api.jxszpt.com');

// GET 请求
client.request('GET', '/api/v1/users')
    .then(data => console.log('用户列表:', data))
    .catch(error => console.error('错误:', error));

// POST 请求
client.request('POST', '/api/v1/users', { name: '张三', email: 'zhangsan@example.com' })
    .then(data => console.log('创建用户成功:', data))
    .catch(error => console.error('错误:', error));

Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.*;

public class ApiClient {
    private final String accessKeyId;
    private final String accessKeySecret;
    private final String baseURL;
    private final RestTemplate restTemplate;

    public ApiClient(String accessKeyId, String accessKeySecret, String baseURL) {
        this.accessKeyId = accessKeyId;
        this.accessKeySecret = accessKeySecret;
        this.baseURL = baseURL;
        this.restTemplate = new RestTemplate();
    }

    /**
     * 生成 HMAC-SHA256 签名
     */
    private String generateSignature(String timestamp) throws NoSuchAlgorithmException, InvalidKeyException {
        String stringToSign = accessKeyId + "-" + accessKeySecret + "-" + timestamp;
        
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(accessKeySecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        mac.init(secretKeySpec);
        
        byte[] hash = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
        
        // 转换为十六进制字符串
        StringBuilder hexString = new StringBuilder();
        for (byte b : hash) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) {
                hexString.append('0');
            }
            hexString.append(hex);
        }
        return hexString.toString();
    }

    /**
     * 创建请求头
     */
    private HttpHeaders createHeaders() throws NoSuchAlgorithmException, InvalidKeyException {
        String timestamp = String.valueOf(System.currentTimeMillis());
        String signature = generateSignature(timestamp);

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set("X-AccessKeyId", accessKeyId);
        headers.set("X-Signature", signature);
        headers.set("X-Timestamp", timestamp);
        
        return headers;
    }

    /**
     * GET 请求
     */
    public <T> ResponseEntity<T> get(String endpoint, Class<T> responseType) 
            throws NoSuchAlgorithmException, InvalidKeyException {
        HttpHeaders headers = createHeaders();
        HttpEntity<String> entity = new HttpEntity<>(headers);
        
        return restTemplate.exchange(
            baseURL + endpoint,
            HttpMethod.GET,
            entity,
            responseType
        );
    }

    /**
     * POST 请求
     */
    public <T> ResponseEntity<T> post(String endpoint, Object requestBody, Class<T> responseType) 
            throws NoSuchAlgorithmException, InvalidKeyException {
        HttpHeaders headers = createHeaders();
        HttpEntity<Object> entity = new HttpEntity<>(requestBody, headers);
        
        return restTemplate.exchange(
            baseURL + endpoint,
            HttpMethod.POST,
            entity,
            responseType
        );
    }

    // 使用示例
    public static void main(String[] args) {
        try {
            ApiClient client = new ApiClient("your_access_key_id", "your_access_key_secret", "https://tech-api.jxszpt.com");
            
            // GET 请求示例
            ResponseEntity<String> getResponse = client.get("/api/v1/users", String.class);
            System.out.println("GET 响应: " + getResponse.getBody());
            
            // POST 请求示例
            Map<String, String> userData = new HashMap<>();
            userData.put("name", "张三");
            userData.put("email", "zhangsan@example.com");
            
            ResponseEntity<String> postResponse = client.post("/api/v1/users", userData, String.class);
            System.out.println("POST 响应: " + postResponse.getBody());
            
        } catch (Exception e) {
            System.err.println("API 调用失败: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Go

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "bytes"
    "fmt"
    "io"
    "net/http"
    "strconv"
    "time"
)

type ApiClient struct {
    AccessKeyId     string
    AccessKeySecret string
    BaseURL         string
    HttpClient      *http.Client
}

// NewApiClient 创建新的 API 客户端
func NewApiClient(accessKeyId, accessKeySecret, baseURL string) *ApiClient {
    return &ApiClient{
        AccessKeyId:     accessKeyId,
        AccessKeySecret: accessKeySecret,
        BaseURL:         baseURL,
        HttpClient:      &http.Client{Timeout: 30 * time.Second},
    }
}

// generateSignature 生成 HMAC-SHA256 签名
func (c *ApiClient) generateSignature(timestamp string) string {
    stringToSign := fmt.Sprintf("%s-%s-%s", c.AccessKeyId, c.AccessKeySecret, timestamp)
    
    h := hmac.New(sha256.New, []byte(c.AccessKeySecret))
    h.Write([]byte(stringToSign))
    
    return hex.EncodeToString(h.Sum(nil))
}

// createRequest 创建带签名的 HTTP 请求
func (c *ApiClient) createRequest(method, endpoint string, body interface{}) (*http.Request, error) {
    url := c.BaseURL + endpoint
    
    var reqBody io.Reader
    if body != nil {
        jsonData, err := json.Marshal(body)
        if err != nil {
            return nil, fmt.Errorf("序列化请求体失败: %w", err)
        }
        reqBody = bytes.NewBuffer(jsonData)
    }
    
    req, err := http.NewRequest(method, url, reqBody)
    if err != nil {
        return nil, fmt.Errorf("创建请求失败: %w", err)
    }
    
    // 生成时间戳和签名
    timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
    signature := c.generateSignature(timestamp)
    
    // 设置请求头
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-AccessKeyId", c.AccessKeyId)
    req.Header.Set("X-Signature", signature)
    req.Header.Set("X-Timestamp", timestamp)
    
    return req, nil
}

// Get 发起 GET 请求
func (c *ApiClient) Get(endpoint string) ([]byte, error) {
    req, err := c.createRequest("GET", endpoint, nil)
    if err != nil {
        return nil, err
    }
    
    resp, err := c.HttpClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("发起请求失败: %w", err)
    }
    defer resp.Body.Close()
    
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("读取响应失败: %w", err)
    }
    
    if resp.StatusCode >= 400 {
        return nil, fmt.Errorf("请求失败,状态码: %d, 响应: %s", resp.StatusCode, string(body))
    }
    
    return body, nil
}

// Post 发起 POST 请求
func (c *ApiClient) Post(endpoint string, body interface{}) ([]byte, error) {
    req, err := c.createRequest("POST", endpoint, body)
    if err != nil {
        return nil, err
    }
    
    resp, err := c.HttpClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("发起请求失败: %w", err)
    }
    defer resp.Body.Close()
    
    respBody, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("读取响应失败: %w", err)
    }
    
    if resp.StatusCode >= 400 {
        return nil, fmt.Errorf("请求失败,状态码: %d, 响应: %s", resp.StatusCode, string(respBody))
    }
    
    return respBody, nil
}

// 使用示例
func main() {
    client := NewApiClient("your_access_key_id", "your_access_key_secret", "https://tech-api.jxszpt.com")
    
    // GET 请求示例
    getResp, err := client.Get("/api/v1/users")
    if err != nil {
        fmt.Printf("GET 请求失败: %v\n", err)
    } else {
        fmt.Printf("GET 响应: %s\n", string(getResp))
    }
    
    // POST 请求示例
    userData := map[string]interface{}{
        "name":  "张三",
        "email": "zhangsan@example.com",
    }
    
    postResp, err := client.Post("/api/v1/users", userData)
    if err != nil {
        fmt.Printf("POST 请求失败: %v\n", err)
    } else {
        fmt.Printf("POST 响应: %s\n", string(postResp))
    }
}

五、服务端验签逻辑

验签步骤

  1. 参数提取:从请求头中提取 X-AccessKeyIdX-SignatureX-Timestamp
  2. 时间戳验证:检查请求时间戳是否在当前时间的 5 分钟范围内
  3. 签名验证:使用相同算法重新生成签名,与接收到的签名进行比较

服务端验签示例(Node.js)

import crypto from 'crypto';
import express from 'express';

// 中间件:验证 API 签名
function validateSignature(req: express.Request, res: express.Response, next: express.NextFunction) {
    try {
        const accessKeyId = req.headers['x-accesskeyid'] as string;
        const signature = req.headers['x-signature'] as string;
        const timestamp = req.headers['x-timestamp'] as string;

        if (!accessKeyId || !signature || !timestamp) {
            return res.status(400).json({ error: '缺少必要的请求头参数' });
        }

        // 验证时间戳(5分钟内有效)
        const now = Date.now();
        const timestampNum = parseInt(timestamp, 10);
        if (Math.abs(now - timestampNum) > 5 * 60 * 1000) {
            return res.status(401).json({ error: '请求已过期' });
        }

        // 根据 accessKeyId 获取对应的 accessKeySecret(通常从数据库查询)
        const accessKeySecret = getAccessKeySecret(accessKeyId);
        if (!accessKeySecret) {
            return res.status(401).json({ error: '无效的访问密钥' });
        }

        // 重新生成签名并验证
        const stringToSign = `${accessKeyId}-${accessKeySecret}-${timestamp}`;
        const hmac = crypto.createHmac('sha256', accessKeySecret);
        hmac.update(stringToSign);
        const expectedSignature = hmac.digest('hex');

        if (signature !== expectedSignature) {
            return res.status(401).json({ error: '签名验证失败' });
        }

        // 验证通过,继续处理请求
        next();
    } catch (error) {
        return res.status(500).json({ error: '签名验证过程中发生错误' });
    }
}

// 模拟获取密钥的函数(实际应用中应从数据库查询)
function getAccessKeySecret(accessKeyId: string): string | null {
    const keyMap: { [key: string]: string } = {
        'your_access_key_id': 'your_access_key_secret'
    };
    return keyMap[accessKeyId] || null;
}