鉴权认证机制
签名算法
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.
| Field | Type | Description |
|---|---|---|
| Date | Required | The date in RFC1123 format: Thu, 18 Jul 2019 00:18:03 GMT |
| Host | Required | The endpoint for the transaction. Valid values:Live: gateway.pingpongx.com Sandbox: sandbox-gateway.pingpongx.com test-gateway.pingpongx.com |
| Digest | Required | It is a hash of the JSON payload made using a SHA-256 hashing algorithm. Send this header field with empty string when GET request |
| Signature | Required | A comma-separated list of parameters that are formatted as name-value pairs. |
签名过程
1.生成摘要Digest
在请求头中传递的Digest,是请求体JSON类型的请求体里的哈希值。使用SHA-256散列算法创建此散列。
当GET请求时,发送这个带有空字符串的报头字段
要生成摘要步骤:
- 使用SHA-256散列函数转换JSON有效负载(REST主体)。以字节数组的形式计算哈希值。
- 从字节数组生成base64编码的字符串。
- 获取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 生成报头字段及其值的字符串。
- 每行使用一个字段及其值,并以\n结束所有行
- 不要在字符串的末尾使用\n。
- 请确保将报头字段的顺序与您在消息头中传递它们的顺序相同。
- 对于host、date、merchantID和摘要使用与传入消息头相同的值。不要在这个字符串中包含签名。
- 在字符串中包含一个(request-target)字段。
- (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);
}
}
}