Skip to main content

OpenApi 指引

对 OpenApi 调用签名

第三方应用需要对 OpenApi 调用进行签名,以确保请求由第三方发出,否则调用将失败。 第三方使用 appId 和 appSecret 计算请求签名,请妥善保管 appSecret。

请求公共参数

以 HTTP request header 形式传参

参数名类型必填描述
X-FX-Timestampinteger请求发起时的时间戳。如果与服务器时间相差超过 5 分钟,会引起签名错误
AuthorizationStringHTTP 标准身份认证头部字段,例如
FX-HMAC-SHA256 Credential=SthdsPY6u5pDZhyV/, SignedHeaders=content-type;host, Signature=fa62a592c586ad1a35474ffd8db3ac52d4ce43d26fdb5d28b2106affa030b2dd
其中:

FX-HMAC-SHA256:签名方法,取固定值
Credential:签名凭证,其中 SthdsPY6u5pDZhyV 是 appId
SignedHeaders:参与签名计算的头部信息,content-type 和 host 必选
Signature:签名,计算方式见下文
AuthorizationHeader.java
@Data
public class AuthorizationHeader {


public static final String CREDENTIAL_PREFIX = "Credential=";
public static final String SIGNED_HEADERS_PREFIX = "SignedHeaders=";
public static final String SIGNATURE_PREFIX = "Signature=";

public static final String ALGORITHM = "FX-HMAC-SHA256";

private String algorithm;
private String appId;
private String scope = ""; // 没用,占位
private List<String> signedHeaders;
private String signature;

@Override
public String toString() {
return String.format("%s %s, %s, %s", algorithm, CREDENTIAL_PREFIX + appId + "/" + scope, SIGNED_HEADERS_PREFIX + StringUtils.join(signedHeaders, ";"), SIGNATURE_PREFIX + signature);
}

public static AuthorizationHeader parse(String header) {
String[] splits = header.split(" ");
if (splits.length != 4) {
return null;
}
if (!ALGORITHM.equals(splits[0])) {
return null;
}
if (!splits[1].endsWith(",") || !splits[1].startsWith(CREDENTIAL_PREFIX)) {
return null;
}
if (!splits[2].endsWith(",") || !splits[2].startsWith(SIGNED_HEADERS_PREFIX)) {
return null;
}
if (!splits[3].startsWith(SIGNATURE_PREFIX)) {
return null;
}
AuthorizationHeader authorizationHeader = new AuthorizationHeader();
authorizationHeader.setAlgorithm(splits[0]);
authorizationHeader.setAppId(splits[1].substring(CREDENTIAL_PREFIX.length(), splits[1].length() - 2));
String signedHeaders = splits[2].substring(SIGNED_HEADERS_PREFIX.length(), splits[2].length() - 1);
authorizationHeader.setSignedHeaders(Arrays.asList(signedHeaders.split(";")));
authorizationHeader.setSignature(splits[3].substring(SIGNATURE_PREFIX.length()));
return authorizationHeader;
}
}
RequestWrapper.java
@Data
public class RequestWrapper {
private String httpMethod;

private String path;

private String queryString;

private Map<String, String> headersWithLowercaseName;

public RequestWrapper(String httpMethod, URI uri, Map<String, String> headers) throws Exception {
this(httpMethod, uri.getPath(), uri.getQuery(), headers);
}

public RequestWrapper(String httpMethod, String path, String queryString, Map<String, String> headers) throws Exception {
this.httpMethod = httpMethod.toUpperCase();
this.path = path;
this.queryString = (String)StringUtils.defaultIfBlank(queryString, "");
this
.headersWithLowercaseName = (Map<String, String>)headers.entrySet().stream().collect(Collectors.toMap(entry -> ((String)entry.getKey()).toLowerCase(), Map.Entry::getValue));
if (StringUtils.isNotBlank(queryString)) {
if (!queryString.startsWith("?"))
queryString = "?" + queryString;
List<Pair<String, String>> params = (List<Pair<String, String>>)URLEncodedUtils.parse(new URI(queryString), Charset.forName("UTF-8")).stream().map(nameValuePair -> Pair.of(nameValuePair.getName(), nameValuePair.getValue())).sorted(Comparator.comparing(Pair::getLeft).thenComparing(Pair::getRight)).collect(Collectors.toList());
this
.queryString = StringUtils.join((Iterable)params.stream().map(p -> (String)p.getLeft() + "=" + (String)p.getRight()).collect(Collectors.toList()), "&");
}
}

public boolean valid() {
return (StringUtils.isNotBlank(this.httpMethod) &&
StringUtils.isNotBlank(this.path) && MapUtils.isNotEmpty(this.headersWithLowercaseName));
}

}

计算流程

  1. 计算规范请求串
CanonicalRequest =
HTTPRequestMethod + '\n' +
CanonicalURI + '\n' +
CanonicalQueryString + '\n' +
CanonicalHeaders + '\n' +
SignedHeaders
字段名称解释
HTTPRequestMethodHTTP 请求方法(GET、POST )。此示例取值为 POST。
CanonicalURIHTTP 请求 path ,例如 /metis-account/api/current
CanonicalQueryStringHTTP 请求 URL 中的查询字符串
按参数名字典序排序,如果参数名相同,按值字典序排序。
CanonicalHeaders参与签名的头部信息,至少包含 host 和 content-type 两个头部,也可加入自定义的头部参与签名以提高自身请求的唯一性和安全性。 拼接规则:头部 key 和 value 统一转成小写,并去掉首尾空格,按照 key:value\n 格式拼接;多个头部,按照头部 key(小写)的 ASCII 升序进行拼接。此示例计算结果是 content-type:application/json; charset=utf-8\nhost:mapi.feixiangxingqiu.com\n。
注意:content-type 必须和实际发送的相符合,有些编程语言网络库即使未指定也会自动添加 charset 值,如果签名时和发送时不一致,服务器会返回签名校验失败。
SignedHeaders参与签名的头部信息,说明此次请求有哪些头部参与了签名,和 CanonicalHeaders 包含的头部内容是一一对应的。content-type 和 host 为必选头部。 拼接规则:头部 key 统一转成小写;多个头部 key(小写)按照 ASCII 升序进行拼接,并且以分号(;)分隔。此示例为 content-type;host
  1. 计算 StringToSign
StringToSign =
Algorithm + '\n' +
RequestTimestamp + '\n' +
CredentialScope + '\n' +
HashedCanonicalRequest
字段名称解释
Algorithm签名算法,目前固定为 FX-HMAC-SHA256。
RequestTimestamp请求时间戳,即请求头部的公共参数 X-FX-Timestamp 取值,取当前时间 UNIX 时间戳,精确到秒。
CredentialScope凭证范围,目前为固定空串
HashedCanonicalRequest前述步骤拼接所得规范请求串的哈希值,计算伪代码为 Lowercase(HexEncode(Hash.SHA256(CanonicalRequest)))。
  1. 计算签名
Signature = HexEncode(HMAC_SHA256(appSecret, StringToSign))
SignatureUtils.java
public class SignatureUtils {
private static byte[] hmac256(byte[] key, String msg) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
mac.init(secretKeySpec);
return mac.doFinal(msg.getBytes(StandardCharsets.UTF_8));
}

private static String sha256Hex(String s) {
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
byte[] d = md.digest(s.getBytes(StandardCharsets.UTF_8));
return DatatypeConverter.printHexBinary(d).toLowerCase();
}

private static String sha256Hex(byte[] data) {
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
byte[] d = md.digest(data);
return DatatypeConverter.printHexBinary(d).toLowerCase();
}

private static String getCanonicalHeaders(Map<String, String> headersWithLowercaseName, Collection<String> signedHeaders) {
List<Pair<String, String>> signedHeaderList = new ArrayList<>();
for (String header : signedHeaders)
signedHeaderList.add(
Pair.of(header.toLowerCase(), headersWithLowercaseName.getOrDefault(header.toLowerCase(), "")));
signedHeaderList.sort(Comparator.comparing(Pair::getLeft));
StringBuilder ans = new StringBuilder();
for (Pair<String, String> header : signedHeaderList) {
ans.append((String)header.getLeft());
ans.append(':');
ans.append((String)header.getRight());
ans.append('\n');
}
return ans.toString();
}

private static String getSignedHeaders(Collection<String> signedHeaders) {
return signedHeaders.stream().map(String::toLowerCase)
.sorted().collect(Collectors.joining(";"));
}

private static String getStringToSign(String httpMethod, String path, String queryString, Map<String, String> headersWithLowercaseName, Collection<String> signedHeaders, long timestampInSecond) {
String canonicalRequest = httpMethod.toUpperCase() + '\n' + path + '\n' + queryString + '\n' + getCanonicalHeaders(headersWithLowercaseName, signedHeaders) + '\n' + getSignedHeaders(signedHeaders);
String stringToSign = "FX-HMAC-SHA256\n" + timestampInSecond + '\n' + "" + '\n' + sha256Hex(canonicalRequest);
return stringToSign;
}

private static String signature(String httpMethod, String path, String queryString, Map<String, String> headersWithLowercaseName, Collection<String> signedHeaders, long timestampInSecond, String appSecret) throws Exception {
String stringToSign = getStringToSign(httpMethod, path, queryString, headersWithLowercaseName, signedHeaders, timestampInSecond);
String sign = DatatypeConverter.printHexBinary(hmac256(appSecret.getBytes(StandardCharsets.UTF_8), stringToSign)).toLowerCase();
return sign;
}

private static String getSignature(String appSecret, long timestampInSecond, Collection<String> signedHeaders, RequestWrapper requestWrapper) throws Exception {
return signature(requestWrapper.getHttpMethod(), requestWrapper.getPath(), requestWrapper.getQueryString(), requestWrapper
.getHeadersWithLowercaseName(), signedHeaders, timestampInSecond, appSecret);
}

public static Map<String, String> getSignatureHeaders(String appId, String appSecret, long timestampInSecond, Collection<String> signedHeaders, RequestWrapper requestWrapper) throws Exception {
Preconditions.checkArgument(StringUtils.isNotEmpty(appId), "appId cannot be empty");
Preconditions.checkArgument(StringUtils.isNotEmpty(appSecret), "appSecret cannot be empty");
Preconditions.checkArgument((requestWrapper != null && requestWrapper.valid()), "requestWrapper is invalid");
Preconditions.checkArgument((timestampInSecond > 0L), "timestampInSecond cannot be less than zero");
Preconditions.checkArgument(CollectionUtils.isNotEmpty(signedHeaders), "signedHeaders cannot be empty");
Preconditions.checkArgument(CollectionUtils.containsAll((Collection)signedHeaders.stream()
.map(String::toLowerCase).collect(Collectors.toList()), (Collection)SignatureConstant.MIN_SIGNED_HEADERS), "signedHeaders must contain all headers in SignatureConstant.MIN_SIGNED_HEADERS");
Preconditions.checkArgument(CollectionUtils.containsAll(requestWrapper.getHeadersWithLowercaseName().keySet(), signedHeaders), "request headers must contain all signedHeaders");
Map<String, String> headers = Maps.newHashMapWithExpectedSize(2);
headers.put("X-FX-Timestamp", String.valueOf(timestampInSecond));
headers.put("Authorization", (new AuthorizationHeader("FX-HMAC-SHA256", appId, "", signedHeaders,

getSignature(appSecret, timestampInSecond, signedHeaders, requestWrapper))).toString());
return headers;
}
}

签名 SDK

平台提供 Java 版本签名 SDK。下载地址

openApi调用示例代码

java示例代码

import cn.hutool.http.HttpUtil;
import com.google.common.collect.ImmutableMap;
import com.yuanfudao.metis.openapi.sdk.constant.SignatureConstant;
import com.yuanfudao.metis.openapi.sdk.data.RequestWrapper;
import com.yuanfudao.metis.openapi.sdk.util.SignatureUtils;

import java.net.URI;
import java.util.Map;
import java.util.Map.Entry;

public class Main1 {

public static void main(String[] args) throws Exception {
String url = "url"; // 请求url
String appId = "appId";
String appSecret = "secret";

RequestWrapper requestWrapper = new RequestWrapper("GET",
new URI(url), ImmutableMap.of(
"content-type", "application/json;charset=UTF-8",
"host", "mapi.feixiangxingqiu.com"));

Map<String, String> headers = SignatureUtils.getSignatureHeaders(appId,
appSecret, System.currentTimeMillis() / 1000, SignatureConstant.MIN_SIGNED_HEADERS,
requestWrapper);

for (Entry<String, String> entry : headers.entrySet()) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
headers.put("content-type","application/json;charset=UTF-8");

final String body = HttpUtil.createGet(url).addHeaders(headers).timeout(5000).execute().body();
System.out.println(body);
}
}

排错指南

code说明
40001url encode 错误
40002签名错误
40004Header错误
40005请求发起时的时间戳与服务器时间相差超过 5 分钟
40006请求发起时的时间戳错误
40007SignedHeaders错误
40008Authorization错误