使用方式
签名算法
RSA
待签名参数
获取所有请求参数(防止新增参数导致签名失败),除去>防止新增参数导致签名失败),除去 sign
,其他请求参数(包含空值的参数,即 key=""
)都要参加验签。
详细步骤
STEP1:获取验签公钥
STEP2:参数拼接,获取 $source
字符串
- 对所有待签名参数按照字段名的ASCII码从小到大排序;
- 使用键值对的格式(
key1=value1&key2=value2
)连接参数,拼接成字符串; - 对拼接完参数的字符串进行
urlencode
编码,获得$source
字符串;
其中,value 的定义如下:
类型 | 定义 |
---|---|
string | 直接拼接字符串内容即可,没有引号,如:abc |
number | 需要转成字符串后拼接,如:123 、1.03 |
bool | 需要转成 true /false 后拼接 |
空(null) | 需要转成空字符串("" )后拼接 |
list | 一般会转为 string 的方式来使用,不会直接使用 list 结构本身 |
object | 一般会转为 string 的方式来使用,不会直接使用 object 结构本身 |
STEP3:利用密钥校验请求签名参数
- 将请求传递过去的sign值进行base64解码后,进行openssl校验 (参考:RFC 8017 - PKCS #1: RSA Cryptography Specifications Version 2.2)
- 哈希算法使用 SHA1,签名算法使用 PKCS#1 v1.5
使用举例
假设
验签公钥
为-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCniSL6NpV9INgXlsV3GVGXmXq9
blKezBBPWGbhQuuDF5VK8iduKmXE8uInF1JwOu/wG2rCWCVxuvB5JDQFZQkigZFo
WR4/zIrTMz0qMXJeqdb3ITXzRrF9Vrt6cGMZUB3rTYSsCWSgDoiCGXkqtescPb0W
eP2Ja76KxWKVt7fUPQIDAQAB
-----END PUBLIC KEY-----
sign
为NGfJ7gJdiLuEbq6FInSh+7PoXVbAV7YG8MoFZY74viojPtlPKQMII0LFKZDL41fotp5OyKpI5A7lfCf51z2J0TFbaRl0nppwARpzv4t7Zdyt54B5jfst7LZvxAHFN+svej00ggAyxt3l+GCdE/ZQj+9gvHtWoneZqWzghcah+5A=
请求参数为:
- b = 1
- a = "飞鱼"
- d = 0.1
- c =
null
- x =
true
- y =
false
实际计算步骤如下:
- 通过步骤2的排序和连接,可以得到:
a=飞鱼&b=1&c=&d=0.1&x=true&y=false
- 通过步骤2的
urlencode
编码,可以得到a%3D%E9%A3%9E%E9%B1%BC%26b%3D1%26c%3D%26d%3D0.1%26x%3Dtrue%26y%3Dfalse
- 通过步骤3进行签名校验
签名样例
Golang
func verifySign(params map[string]interface{}, pubKey, sign string) (bool, error) {
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
// 对key进行排序
sort.Strings(keys)
var kvPairs []string
for _, k := range keys {
if k == "sign" {
continue
}
v := params[k]
strValue := ""
if v != nil {
switch v := v.(type) {
case float32, float64:
// 浮点类型需要特殊处理,不然转换后的字符串会变成科学计数法的形式导致签名url不正确
strValue = strconv.FormatFloat(v.(float64), 'f', -1, 64)
default:
strValue = fmt.Sprintf("%v", v)
}
}
kvPairs = append(kvPairs, k+"="+strValue)
}
// 拼接参数
preStr := strings.Join(kvPairs, "&")
// 进行urlencode编码
preStr = strings.Replace(url.QueryEscape(preStr), "+", "%20", -1)
// 计算hash
signature, err := base64.StdEncoding.DecodeString(sign)
if err != nil {
return false, err
}
h := crypto.SHA1.New()
h.Write([]byte(preStr))
hashed := h.Sum(nil)
// 验证签名
pk, err := getRSAPubKey([]byte(pubKey))
if err != nil {
return false, err
}
err = rsa.VerifyPKCS1v15(pk, crypto.SHA1, hashed, signature)
if err != nil {
return false, err
}
return true, nil
}
func getRSAPubKey(pkey []byte) (*rsa.PublicKey, error) {
block, _ := pem.Decode(pkey)
if block == nil {
return nil, errors.New("no PEM data is found")
}
public, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
return public.(*rsa.PublicKey), nil
}
PHP
function verifySign($params, $rsapublickey, $sign = '')
{
ksort($params);//1 按照健值从小到大排序
$signParams = array();//2 键值对按照'='拼接,排除sign参数
foreach ($params as $key => $val) {
if ($key == 'sign'){
continue;
}
switch (gettype($val)) {
case 'NULL':
$val = '';
break;
case 'boolean':
$val = $val === true ? 'true' : 'false';
break;
default:
$val = (string) $val;
break;
}
array_push($signParams, $key . '=' . $val);
}
$signStr = join('&', $signParams);// 将上述项目按照&拼接
$source = rawurlencode($signStr);// 将拼接结果用rawurlencode编码
$res = openssl_get_publickey($rsapublickey);
if (!$res) {
return false;
}
$result = (bool)openssl_verify($source, base64_decode($sign), $res);//使用base64_decode进行解码验证签名
openssl_free_key($res);//释放资源
return $result;
}