跳到主要内容

鉴权认证机制

签名算法

PingPong网关签名算法使用draft-ietf-httpbis-message-signatures 标准,使用SHA-256的app_secret作为key。

签名仅支持hmac-sha256算法

计算中使用的header为:

(request-target) Host Date Digest

当request payload为空时, 请将Digest请求头设为空字符串。

下面这些例子说明在请求PingPong的接口时需要携带的REST HTTP请求头信息。

REST HTTP POST或PUT请求时,请求头如下:

Host: gateway.pingpongx.com
Date: Tue, 07 Jun 2014 20:51:35 GMT
Digest: SHA-256=H6C/O5Bnwaum+OdHWqdk/oIQfbpQzlaWgb7ZSnR05i4=
Signature: Signature keyId="{your app id}",created=1644908541,algorithm="hmac-sha256",headers="(request-target) host date digest",signature="DapgMslWAJnvkzzB7737pWPP7qwRFEH63BNOLcFH5Xg="

REST HTTP GET请求时,请求头如下:

Host: gateway.pingpongx.com
Date: Tue, 07 Jun 2014 20:51:35 GMT
Digest:
Signature: Signature keyId="{your app id}",created=1644908597,algorithm="hmac-sha256",headers="(request-target) host date digest",signature="2WUS7MLHI85TbyBd/2ize2ZkK8z9b4ug68SRzeGfdbI="

请求头字段

Include these fields in your REST message header.

FieldTypeDescription
DateRequiredThe date in RFC1123 format: Thu, 18 Jul 2019 00:18:03 GMT
HostRequiredThe endpoint for the transaction. Valid values:Live: gateway.pingpongx.com
Sandbox: sandbox-gateway.pingpongx.com test-gateway.pingpongx.com
DigestRequiredIt is a hash of the JSON payload made using a SHA-256 hashing algorithm. Send this header field with empty string when GET request
SignatureRequiredA comma-separated list of parameters that are formatted as name-value pairs.

签名过程

1.生成摘要Digest

在请求头中传递的Digest,是请求体JSON类型的请求体里的哈希值。使用SHA-256散列算法创建此散列。

当GET请求时,发送这个带有空字符串的报头字段

要生成摘要步骤:

  1. 使用SHA-256散列函数转换JSON有效负载(REST主体)。以字节数组的形式计算哈希值。
  2. 从字节数组生成base64编码的字符串。
  3. 获取base64编码的字符串,并在其前面加上SHA-256=。 摘要字段的格式:
Digest: SHA-256=H6C/O5Bnwaum+OdHWqdk/oIQfbpQzlaWgb7ZSnR05i4=

使用以下代码示例来验证代码是否正常运行。如果将POST或PUT主体文本插入到这些函数中,则可以将结果摘要值与您自己的应用程序生成的值进行比较。如果值匹配,则您的摘要函数工作正常。

public class Test {

public static void main(String[] args) throws NoSuchAlgorithmException {
String bodyText = "{\n" +
" \"clientId\": \"1234567890\"\n" +
"}";
String expectDigest = "SHA-256=H6C/O5Bnwaum+OdHWqdk/oIQfbpQzlaWgb7ZSnR05i4=";
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(bodyText.getBytes(StandardCharsets.UTF_8));
byte[] digestByte = md.digest();
String actualDigest = "SHA-256=" + Base64.getEncoder().encodeToString(digestByte);
System.out.println(expectDigest.equals(actualDigest));
}
}

2.生成签名哈希

签名哈希是在REST消息的signature头中传递的名称-值对或参数之一。它是报头字段及其值的base64编码散列。创建每个报头字段名称及其关联值的字符串。然后,将字符串转换为哈希值(HMACSHA256)并对其进行base64编码。 包含签名哈希的签名头字段示例

Signature: Signature keyId="{your app id}",created=1644908541,algorithm="hmac-sha256",headers="(request-target) host date

生成签名哈希的具体步骤如下:

1 生成报头字段及其值的字符串。

  1. 每行使用一个字段及其值,并以\n结束所有行
  2. 不要在字符串的末尾使用\n。
  3. 请确保将报头字段的顺序与您在消息头中传递它们的顺序相同。
  4. 对于host、date、merchantID和摘要使用与传入消息头相同的值。不要在这个字符串中包含签名。
  5. 在字符串中包含一个(request-target)字段。
  6. (request-target)值是小写的HTTP动词,后跟一个空格,然后是资源路径(去除主机路径)。下面的示例显示了对/pts/v2/payments/资源的POST请求。在请求目标值中包含查询字符串和请求id。
(request-target): post /foo/bar

POST或PUT请求的字符串示例

(request-target): post /foo/bar
host: gateway.pingpongx.com
date: Tue, 07 Jun 2014 20:51:35 GMT
digest: SHA-256=H6C/O5Bnwaum+OdHWqdk/oIQfbpQzlaWgb7ZSnR05i4=

GET请求的字符串示例

(request-target): get /foo/bar
host: gateway.pingpongx.com
date: Tue, 07 Jun 2014 20:51:35 GMT
digest:

2 生成您在上一步中创建的字符串的字节数组。

3 生成密钥哈希。使用SHA-256哈希函数转换应用秘密并生成base64编码的字符串(如生成摘要)。

4 为您在Business Center中生成的已解码的Secret Key Hash(来自步骤3)创建字节数组。

5 实例化一个基于解码密钥字节数组的HMACSHA256对象(从步骤4)。

6 使用这个HMACSHA256对象计算基于字符串字节数组的HMACSHA256哈希值(从步骤2开始)。

7 从上一步的HMACSHA256对象的字节数组生成一个base64编码的字符串。

8 结果值是签名哈希值:

signature="2WUS7MLHI85TbyBd/2ize2ZkK8z9b4ug68SRzeGfdbI="

使用Java生成哈希签名的示例代码

    byte[]decodedKey=Base64.getDecoder().decode(keyString);
SecretKey originalKey=new SecretKeySpec(decodedKey,0,decodedKey.length,"HmacSHA256");
Mac hmacSha256=Mac.getInstance("HmacSHA256");
hmacSha256.init(originalKey);
hmacSha256.update(signatureParams.getBytes());
byte[]HmachSha256DigestBytes=hmacSha256.doFinal();
String signatureHash=Base64.getEncoder().encodeToString(HmachSha256DigestBytes);

3.将签名信息添加到请求

通过将签名信息添加到名为 Authorization 的 HTTP 标头来实现认证鉴权信息的传递。

此标头内容是在按前面的步骤所述计算签名之后创建的,因此 Authorization 标头未包含在已签名标头的列表中。尽管此标头名为 Authorization,但签名信息实际上用于身份验证。

Java SDK签名用例

1. 首先引入tomitribe-http-signatures作为签名工具, 当前最新版本1.8,本例使用 1.7


<dependency>
<groupId>org.tomitribe</groupId>
<artifactId>tomitribe-http-signatures</artifactId>
<version>1.7</version>
</dependency>

2. java签名工具类

import java.io.IOException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import javax.crypto.spec.SecretKeySpec;
import org.tomitribe.auth.signatures.Base64;
import org.tomitribe.auth.signatures.Signature;
import org.tomitribe.auth.signatures.Signer;

/**
* @author jicp
* @version 1.0.0
* @date 2023/08/2023/8/22 6:25 PM
*/
public class SignatureUtil {

public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(
"EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);

public static String generateDigest(String payload) throws NoSuchAlgorithmException {
if (payload == null) {
return "";
}
// 计算 digest
final byte[] digest = MessageDigest.getInstance("SHA-256").digest(payload.getBytes()); // (1)
return "SHA-256=" + new String(Base64.encodeBase64(digest));
}

public static String generateSignature(String appId, String appSecret, String httpMethod, String uri,
Map<String, String> headers) throws NoSuchAlgorithmException, IOException {
// 计算签名
final Signature signature = new Signature(appId, "hmac-sha256", "hmac-sha256", null,
Arrays.asList("(request-target)", "Host", "Date", "Digest"));
final String secretHash = secretHash(appSecret);
final Key key = new SecretKeySpec(secretHash.getBytes(), "HmacSHA256");
final Signer signer = new Signer(key, signature);
Signature sign = signer.sign(httpMethod, uri, headers);

return sign.toString();
}

public static String secretHash(String appSecret) throws NoSuchAlgorithmException {
final byte[] secretHashBytes = MessageDigest.getInstance("SHA-256").digest(appSecret.getBytes());
return new String(Base64.encodeBase64(secretHashBytes));
}

}

3. Java请求PingPong接口加签Demo

import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

/**
* @author jicp
* @version 1.0.0
* @date 2023/08/2023/8/22 6:25 PM
*/
public class SignatureDemo {

private static final int TIME_OUT_MILLISECONDS = 10 * 1000;

public static void main(String[] args) throws NoSuchAlgorithmException, IOException {
final String appId = "{your app id}";
final String appSecret = "{your app sercret}";
String host = "test-gateway.pingpongx.com";
String accessToken = "{your access token query from getToken api}";
// 请求的payload, requestBody
JSONObject jsonObject = new JSONObject();
jsonObject.put("client_id", "IT8623081103775418152");
final String payload = JSON.toJSONString(jsonObject);
final String httpMethod = "POST";
// 客户注册信息查询接口
final String uri = "/v1/mid-open-api/merchant/query";
// 请求头
String dateStr = SignatureUtil.DATE_TIME_FORMATTER.format(LocalDateTime.now());
String digest = SignatureUtil.generateDigest(payload);
final Map<String, String> headers = new HashMap<>();

headers.put("Host", host);
headers.put("Date", dateStr);
headers.put("Digest", digest);
String signature = SignatureUtil.generateSignature(appId, appSecret, httpMethod, uri, headers);
headers.put("Signature", signature);

String url = "https://" + host + uri;
try (HttpResponse response = HttpRequest.post(url)
.header("Authorization", "Bearer " + accessToken)
.header("Content-Type", "application/json;charset=UTF-8")
.header("Accept", "application/json;charset=UTF-8")
.headerMap(headers, true)
.body(payload)
.charset(StandardCharsets.UTF_8)
.timeout(TIME_OUT_MILLISECONDS)
.execute()) {
String responseBody = response.body();
System.out.println(responseBody);
}
}

}