接入流程
一、概述
为确保接口的安全性和数据的完整性,第三方服务商在调用开放平台接口时,需要使用 HMAC-SHA256 算法进行加签和验签。通过在请求中加入签名,可以有效防止请求被篡改,保障数据传输的安全性。
二、接入准备
1. IP 白名单报备
为确保服务的合法性和安全性,同时便于监管部门进行监督和管理,请在接入平台前联系平台运营人员进行 IP 白名单报备。
报备信息包括:
- 服务商名称
- 联系人信息
- 服务器出口 IP 地址
- 预计调用频率
2. 获取访问凭证
联系平台运营人员获取以下访问凭证:
accessKeyId:用于标识服务商身份的唯一 IDaccessKeySecret:用于生成签名的密钥,请妥善保管
3. 环境准备
测试环境: http://tech-test-api.jxszpt.com
生产环境: https://tech-api.jxszpt.com
三、签名算法
签名生成步骤
- 准备签名参数:
accessKeyId、accessKeySecret、timestamp - 将参数按格式拼接:
${accessKeyId}-${accessKeySecret}-${timestamp} - 使用
HMAC-SHA256算法对拼接字符串进行签名 - 将签名结果转换为十六进制字符串
请求头设置
将签名信息添加到请求头中:
| Header 名称 | 描述 | 示例 |
|---|---|---|
X-AccessKeyId | 访问密钥 ID | your_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))
}
}五、服务端验签逻辑
验签步骤
- 参数提取:从请求头中提取
X-AccessKeyId、X-Signature、X-Timestamp - 时间戳验证:检查请求时间戳是否在当前时间的 5 分钟范围内
- 签名验证:使用相同算法重新生成签名,与接收到的签名进行比较
服务端验签示例(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;
}