签名逻辑

为了确保你的支付应用与 SHOPLINE 平台间的访问是安全的、未被篡改的,SHOPLINE 对整个 交易流程商家激活绑定流程 增加了签名验证逻辑。
对于 SHOPLINE 主动请求支付应用的场景,你需要:

  • 通过签名算法,验证 SHOPLINE 请求数据的合法性,如果签名验证不通过,不应该处理此次请求。
  • 对于返回给 SHOPLINE 的响应数据,确保有按照签名算法进行加签;SHOPLINE 将对响应进行签名验证,如果验证不通过,将不会信任支付应用返回的结果。

对于支付应用主动通知 SHOPLINE 的场景,你需要确保对请求 SHOPLINE 的数据按照签名算法进行加签;SHOPLINE 将对请求内容进行签名验证,如果验证不通过,将不会处理本次请求。
阅读本文,你将了解相关的签名算法及其使用场景。

签名算法

算法简介

支付应用与 SHOPLINE 平台间的访问使用 SHA1withRSA 算法加密,大体流程如下: 支付应用-签名算法.png

  • 在生成 RSA 公私钥时请注意,密钥格式需要使用 PCKS#8,长度需要使用 2048 位。
  • 双方需各自持有本人的私钥及对方的公钥,并务必妥善保管私钥,以防泄露导致安全风险。
  • 加签时,使用己方私钥进行加签。
  • 验签时,使用对方公钥进行验签。

SHOPLINE 公钥获取

你可以在支付应用首页找到 SHOPLINE 平台的公钥;该公钥用于对 SHOPLINE 发起的请求进行验签,防止第三方伪造请求。 2.png

支付应用公钥提交

你需要在 SHOPLINE 合作伙伴后台 > 应用 > 应用详情 > 支付应用拓展 页面提交你的公钥(具体步骤)。SHOPLINE 平台将使用该公钥对支付应用的响应或请求进行验签,防止第三方伪造。

加签与验签

生成待签名文本

加签或验签的第一步,需要将原始报文组装成待签名文本。
生成待签名文本时,要求对参数名进行字典排序,然后使用 = 拼接参数名和参数值,并使用 & 拼接多个参数,即 key1=val1&key2=val2&key3=val3

注意
  • 所有非 null 的字段,都需要参与签名。
  • 当某个值为复杂类型时,处理规则如下:
    • 键值对象:需要将该对象的每一个键值对按组装方式进行递归组装;同时,当前的字段名将会被忽略。
    • 列表类型:
      • 如果列表中的元素是键值对象,则按键值对象的规则进行递归处理。
      • 如果列表中的元素是简单标量,则只需要将列表中的每个元素处理完后,用英文逗号(,)连接起来。
    • 键值对象和列表类型在加入待签名文本时,均不添加 & 连接符号。

例如,有以下待签名原文:

{
"normalKey": "normalValue",
"emptyKey": "",
"nullKey": null,
"list": ["elem1", "elem2"],
"listObject": [
{
"elemKey": "elem1"
},
{
"elemKey": "elem2"
}
],
"obj": {
"emptyObjectKey": "",
"objectKey": "objectValue"
}
}

则生成后的待签名文本为:

emptyKey=&list=elem1,elem2&elemKey=elem1&elemKey=elem2&normalKey=normalValue&emptyObjectKey=&objectKey=objectValue

代码示例

Java 代码示例:

import org.apache.commons.lang3.StringUtils;
import java.math.BigDecimal;
import java.util.*;
/**
* 生成签名字符串示例
*/
public class SignatureSourceStringDemo {
public static void main(String[] args) {
Map<String, Object> params = new HashMap<>();
params.put("normalKey", "normalValue");
params.put("emptyKey", "");
params.put("list", Lists.newArrayList("elem1", "elem2"));
params.put("obj", new HashMap<String, Object>() {{
put("objectKey", "objectValue");
put("emptyObjectKey", "");
}});
params.put("listObject", Lists.newArrayList(
new HashMap<String, Object>() {{
put("elemKey", "elem1");
}}, new HashMap<String, Object>() {{
put("elemKey", "elem2");
}}
));
String signatureSourceString = buildSignatureSourceString(params);
System.out.println(signatureSourceString);
}
/**
* 生成待签名的文本
*/
public static String buildSignatureSourceString(Map<String, Object> params) {
StringBuilder content = new StringBuilder();
append(content, params);
return content.toString();
}
private static void append(StringBuilder content, Map<String, Object> sourceObj) {
if (null == sourceObj || sourceObj.keySet().size() == 0) {
return;
}
List<String> keyList = new ArrayList<>(sourceObj.keySet());
Collections.sort(keyList);
for (String key : keyList) {
Object value = sourceObj.get(key);
if (value instanceof List) {
Object itemValue = ((List<?>) value).get(0);
if (isScalarValue(itemValue)) {
content.append(key);
content.append("=");
content.append(StringUtils.join(((List<?>) value).toArray(), ","));
} else {
for (int i = 0; i < ((List<?>) value).size(); ++i) {
Object item = ((List<?>) value).get(i);
if (item instanceof Map) {
append(content, (Map) item);
}
}
}
} else if (value instanceof Map) {
append(content, (Map) value);
} else if (isScalarValue(value)) {
if (content.length() > 0) {
content.append("&");
}
content.append(key);
content.append("=");
content.append(value);
}
}
}
/**
* 检查值是否是标量
*/
private static boolean isScalarValue(Object value) {
return value instanceof String
|| value instanceof Float
|| value instanceof Double
|| value instanceof Integer
|| value instanceof Long
|| value instanceof BigDecimal
|| value instanceof Boolean;
}
}

加签

使用你的私钥,对待签名文本进行加签操作;获取到签名值后,需要在 HTTP Header 中传递:

  • 如果是响应 SHOPLINE 的请求,需要在响应头 pay-api-signature 中携带签名值;具体字段信息可参考 付款请求
  • 如果是主动通知 SHOPLINE,需要在请求头 signature 中携带签名值;具体字段信息可参考 付款状态通知

代码示例

Java 代码示例:

import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import org.apache.commons.codec.binary.Base64;
/**
* 加签示例
*/
public class SignatureDemo {
private static String priKey ="你的私钥";
public static final String KEY_ALG_NAME = "RSA";
public static final String SIGN_ALG_NAME = "SHA1withRSA";
public static void main(String[] args) {
String signatureSourceString = "emptyKey=list=elem1,elem2&elemKey=elem1&elemKey=elem2&normalKey=normalValue&emptyObjectKey=&objectKey=objectValue";
String signature = signWithPrivateKey(priKey, signatureSourceString);
System.out.println(signature);
}
private static String signWithPrivateKey(String privateKey, String signSourceStr) {
try {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(decodeBase64(privateKey));
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALG_NAME);
PrivateKey myPrivateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Signature signet = Signature.getInstance(SIGN_ALG_NAME);
signet.initSign(myPrivateKey);
signet.update(signSourceStr.getBytes(StandardCharsets.UTF_8));
byte[] signed = signet.sign();
return new String(Base64.encodeBase64(signed));
} catch (Exception ex) {
throw new RuntimeException(ex.getMessage(), ex);
}
}
private static byte[] decodeBase64(String sourceStr) {
return Base64.decodeBase64(sourceStr.getBytes(StandardCharsets.UTF_8));
}
}

验签

使用 SHOPLINE 的公钥,对待签名文本进行验签操作。
SHOPLINE 只会在主动调用支付应用时,在请求头 pay-api-signature 中携带签名值;具体字段信息可参考 付款请求

注意

验签时,需要保证 SHOPLINE 传递的所有数据,都已正确地组装到待签名文本;必须先对 SHOPLINE 传递的原始报文验签后,再将其结构化并进行业务逻辑处理,否则会导致验签失败。
为此,SHOPLINE 会在请求中添加随机键值对,来确保使用的是 SHOPLINE 的原始报文进行验签。此机制可以保证后续 SHOPLINE 接口新增参数时(新增的参数一定会参与签名),已对接但未升级的支付应用不会验签失败。

代码示例

Java 代码示例:

package com.example.demo_jdk8.sign.demo01;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
/**
* 验签示例
*/
@Slf4j
public class CheckSignDemo {
public static final String SIGN_ALG_NAME = "SHA1withRSA";
public static final String KEY_ALG_NAME = "RSA";
public static void main(String[] args) {
String publicKey = "SHOPLINE 的公钥";
String signatureSourceString = "由 SHOPLINE 响应组装的待签名文本";
String signature = "SHOPLINE 返回的签名值";
boolean check = checkSignWithPublicKey(publicKey, signatureSourceString, signature);
System.out.println(check);
}
public static boolean checkSignWithPublicKey(String publicKey, String signSourceStr, String signedStr) {
try {
X509EncodedKeySpec bobPubKeySpec = new X509EncodedKeySpec(decodeBase64(publicKey));
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALG_NAME);
PublicKey pubKey = keyFactory.generatePublic(bobPubKeySpec);
byte[] signed = decodeBase64(signedStr);
Signature signetCheck = Signature.getInstance(SIGN_ALG_NAME);
signetCheck.initVerify(pubKey);
signetCheck.update(signSourceStr.getBytes(StandardCharsets.UTF_8));
return signetCheck.verify(signed);
} catch (Exception ex) {
log.warn("sign check fail, signSourceStr: {}, signedStr: {}", signSourceStr, signedStr, ex);
return false;
}
}
private static byte[] decodeBase64(String sourceStr) {
return Base64.decodeBase64(sourceStr.getBytes(StandardCharsets.UTF_8));
}
}

完整示例

Java 示例

import org.apache.commons.lang3.StringUtils;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;
public class SignatureDemo {
public static final String SIGN_ALG_NAME = "SHA1withRSA";
public static final String KEY_ALG_NAME = "RSA";
public static void main(String[] args) {
String privateKey = "你的私钥";
String publicKey = "你的公钥";
Map<String, Object> params = new HashMap<>();
params.put("key", "value");
String signatureSourceString = buildSignatureSourceString(params);
String signature = signature(privateKey, params);
boolean b = checkSignWithPublicKey(publicKey, signatureSourceString, signature);
System.out.println("b = " + b);
}
public static String signature(String privateKey, Map<String, Object> params) {
String signatureSource = buildSignatureSourceString(params);
return signWithPrivateKey(privateKey, signatureSource);
}
public static String buildSignatureSourceString(Map<String, Object> params) {
StringBuffer content = new StringBuffer();
append(content, params);
return content.toString();
}
private static void append(StringBuffer content, Map<String, Object> sourceObj) {
if (null == sourceObj || sourceObj.keySet().size() == 0) {
return;
}
List<String> keyList = new ArrayList<>(sourceObj.keySet());
Collections.sort(keyList);
for (String key : keyList) {
Object value = sourceObj.get(key);
if (value instanceof List) {
Object itemValue = ((List<?>) value).get(0);
if (isScalarValue(itemValue)) {
content.append(key);
content.append("=");
content.append(StringUtils.join(((List<?>) value).toArray(), ","));
} else {
for (int i = 0; i < ((List<?>) value).size(); ++i) {
Object item = ((List<?>) value).get(i);
if (item instanceof Map) {
append(content, (Map) item);
}
}
}
} else if (value instanceof Map) {
append(content, (Map) value);
} else if (isScalarValue(value)) {
if (content.length() > 0) {
content.append("&");
}
content.append(key);
content.append("=");
content.append(value);
}
}
}
private static boolean isScalarValue(Object value) {
return value instanceof String
|| value instanceof Float
|| value instanceof Double
|| value instanceof Integer
|| value instanceof Long
|| value instanceof BigDecimal
|| value instanceof Boolean;
}
private static String signWithPrivateKey(String privateKey, String signSourceStr) {
try {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(decodeBase64(privateKey));
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALG_NAME);
PrivateKey myPrivateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Signature signet = Signature.getInstance(SIGN_ALG_NAME);
signet.initSign(myPrivateKey);
signet.update(signSourceStr.getBytes(StandardCharsets.UTF_8));
byte[] signed = signet.sign();
return new String(org.apache.commons.codec.binary.Base64.encodeBase64(signed));
} catch (Exception ex) {
throw new RuntimeException(ex.getMessage(), ex);
}
}
public static boolean checkSignWithPublicKey(String publicKey, String signSourceStr, String signedStr) {
try {
X509EncodedKeySpec bobPubKeySpec = new X509EncodedKeySpec(decodeBase64(publicKey));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(bobPubKeySpec);
byte[] signed = decodeBase64(signedStr);
Signature signetCheck = Signature.getInstance(SIGN_ALG_NAME);
signetCheck.initVerify(pubKey);
signetCheck.update(signSourceStr.getBytes(StandardCharsets.UTF_8));
return signetCheck.verify(signed);
} catch (Exception ex) {
log.warn("sign check fail, signSourceStr: {}, signedStr: {}", signSourceStr, signedStr, ex);
return false;
}
}
private static byte[] decodeBase64(String sourceStr) {
return org.apache.commons.codec.binary.Base64.decodeBase64(sourceStr.getBytes(StandardCharsets.UTF_8));
}
}

PHP 示例

class SignatureUtilsBySHA1WithRSA {
public static function genSignatureSourceStr($params){
$signSourceStr = "";
$params = json_decode(json_encode($params));
return self::append($signSourceStr, $params);
}
public static function append(&$content, $params){
$emptyObj = false;
if(is_object($params) && count(get_object_vars($params))==0){
return ;
}
$params = (array)$params;
ksort($params);
foreach ($params as $key => $value) {
if(is_null($value)){
continue;
}
$prefix = empty($content) ? "" : "&";
if(is_scalar($value) || empty($value)){
$content .= $prefix.$key."=".(is_null($value) ? "" : $value);
}else{
if(is_array($value)){
if(empty($value)){
$content .= $key."=";
}elseif(is_scalar($value[0])){
$content .= $key."=".join(",", $value);
}else{
if(isset($value[0])){
foreach($value as $v){
self::append($content, $v);
}
}else{
self::append($content, $value);
}
}
}elseif(is_object($value)){
self::append($content, $value);
}else{
self::append($content, $value);
}
}
}
return $content;
}
public static function genSignature($toSign, $privateKey){
$privateKey = "-----BEGIN RSA PRIVATE KEY-----\n" .
wordwrap($privateKey, 64, "\n", true) .
"\n-----END RSA PRIVATE KEY-----";
$key = openssl_get_privatekey($privateKey);
openssl_sign($toSign, $signature, $key);
openssl_free_key($key);
$sign = base64_encode($signature);
return $sign;
}
public static function verifySignature($data, $sign, $pubKey){
$sign = base64_decode($sign);
$pubKey = "-----BEGIN PUBLIC KEY-----\n" .
wordwrap($pubKey, 64, "\n", true) .
"\n-----END PUBLIC KEY-----";
$key = openssl_pkey_get_public($pubKey);
$result = openssl_verify($data, $sign, $key, OPENSSL_ALGO_SHA1) === 1;
return $result;
}
}
这篇文章对你有帮助吗?