Skip to content

WPS-4签名主要用于外部三方调用时接口签名。

签名算法说明

接口签名时,只签uri字段(不包含/o/{cid}部分),不签域名,如:/o/docmini/api/xxx,只签/api/xxx部分 Header中需要携带以下字段:

参数参数类型是否必须说明
Content-Typestring默认为"application/json",跟实际请求的ContentType保持一致
Wps-Docs-Datestring取当前时间,格式:"Wed, 23 Jan 2013 06:43:08 GMT"
Wps-Docs-Authorizationstring签名值

特别说明:当Content-Type中不包含application/jsonapplication/x-www-form-urlencoded类型时,Body不计入签名计算,此外参与计算的Content-Type需要跟实际请求的ContentType保持一致

  • 示例①:请求Header 为 Content-Type: multipart/form-data; boundary=fdb1369bd955e779bdf793ff7e2f3764,则计算签名时,Content-Type="multipart/form-data; boundary=fdb1369bd955e779bdf793ff7e2f3764", Body=""
  • 示例②:请求Header 为 Content-Type: application/json 或者 Content-Type: application/json; charset=utf-8,则计算签名时,Content-Type="application/json" 或者 "application/json; charset=utf-8", Body计入签名计算

计算方法如下:

Wps-Docs-Authorization:"WPS-4 $AppId:$Signature"
Signature:hmac-sha256(AppKey, Ver + HttpMethod + URI + Content-Type + Wps-Docs-Date + sha256(HttpBody))

Ver + HttpMethod + URI + Content-Type + Wps-Docs-Date + sha256(HttpBody) 示例: WPS-4POST/callback/path/demoapplication/jsonWed, 20 Apr 2022 01:33:07 GMTfc005f51a6e75586d2d5d078b657dxxxdf9c1dfa6a7c0c0ba38c715daeb6ede9

sha256 与 hmac-sha256 均取小写十六进制字符串。

  • Ver: WPS-4,表示算法版本,后续算法有更新,则变更该字段。
  • HttpMethod:表示HTTP 请求的Method的字符串,如PUT、GET、POST、HEAD、DELETE等。
  • URI:不带域名,如:"/api_url?app_id=aaaa"。
  • Content-Type:表示请求内容的类型,如:"application/json"。
  • Wps-Docs-Date:自行对签名时限进行验证。
  • HttpBody:如果为空,则sha256(body)部分取空串。

Go代码示例

go
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "net/http"
    "net/url"
    "strings"
    "time"
)

type Wps4Auth struct {
	AccessKey  string
	SecretKey string
}

func NewWps4Auth(accessKey, secretKey string) *Wps4Auth {
	auth := &Wps4Auth{}
	auth.AccessKey = accessKey
	auth.SecretKey = secretKey
	return auth
}

func (a *Wps4Auth)BuildWps4Headers(method string, url *url.URL, data []byte, contentType string) *http.Header {
    if strings.TrimSpace(contentType) == "" {
        contentType = "application/json"
    }

    header := http.Header{}
    auth, date := a.prepare(method, url, data, contentType)
    header.Set("Wps-Docs-Authorization", auth)
    header.Set("Content-Type", contentType)
    header.Set("Wps-Docs-Date", date)
    return &header
}

func (a *Wps4Auth) prepare(method string, url *url.URL, data []byte, contentType string) (auth, date string) {
	path := url.Path
	if url.RawQuery != "" {
		path += fmt.Sprintf("?%s", url.RawQuery)
	}

	var content []byte
	if data != nil {
		content = data
	}

	date = time.Now().UTC().Format(http.TimeFormat)
	sig := a.sign(method, path, contentType, date, content)
	auth = fmt.Sprintf("WPS-4 %s:%s", a.AccessKey, sig)

	return
}

func (a *Wps4Auth) sign(method, uri, contentType, date string, content []byte) (sign string) {
    bodySha := ""
    if content != nil {
	s := sha256.New()
	s.Write(content)
	bodySha = hex.EncodeToString(s.Sum(nil))
    }

    mac := hmac.New(sha256.New, []byte(a.SecretKey))
    mac.Write([]byte("WPS-4"))
    mac.Write([]byte(method))
    mac.Write([]byte(uri))
    mac.Write([]byte(contentType))
    mac.Write([]byte(date))
    mac.Write([]byte(bodySha))

    return hex.EncodeToString(mac.Sum(nil))
}

测试方法

go
import (
	"encoding/json"
	"net/url"
	"testing"
)

type TestWps4Body struct {
	MsgType string `json:"msg_type" web:"msg_type,required"`
	MsgData string `json:"msg_data,omitempty" web:"msg_data"`
}

func TestWps4Sign(t *testing.T) {
	accessKey := "****************"
	secretKey := "********************************"

	wps4Auth := NewWps4Auth(accessKey, secretKey)

	method := "GET"
	path := "/kopen/pay/v1/wx_adapter/mp/subscribe/wps_docer?access_token=********************************'"
	header := wps4Auth.BuildWps4Headers(method, &url.URL{Path: path}, nil, "application/json")
	t.Log(header)

	method = "POST"
	path = "/kopen/pay/v1/msg/apipush?access_token=********************************'"
	body := &TestWps4Body{MsgType: "wps_docer_attent_reward", MsgData: "{'account': 'Zyssnh事实上&abc=123kk//dsa?d'}"}
	reqData, err := json.Marshal(body)
	if err != nil {
		t.Error(err)
	}
	header = wps4Auth.BuildWps4Headers(method, &url.URL{Path: path}, reqData, "application/json")
	t.Log(header)
}

Python代码示例

python
import json
import requests
import hashlib
import time
# 应用信息
def _wps4_sig(method, url, date, body):
    if body is None:
        bodySha = ""
    else:
        bodySha = hashlib.sha256(body.encode('utf-8')).hexdigest()

    content = "WPS-4" + method + url + "application/json" + date + bodySha

    signature = hmac.new(secret_key.encode('utf-8'), content.encode('utf-8'), hashlib.sha256).hexdigest()

    return "WPS-4 %s:%s" % (access_key, signature)


def wps4_request(method, host, uri, body=None, cookie=None, headers=None):
    requests.packages.urllib3.disable_warnings()

    if body is not None:
        body = json.dumps(body)

    date = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime())
    # date = "Wed, 02 Jun 2021 12:15:40 GMT"

    # print date
    header = {"Content-type": "application/json"}
    header['Wps-Docs-Date'] = date
    header['Wps-Docs-Authorization'] = _wps4_sig(method, uri, date, body)

    if headers != None:
        # header = {}
        for key, value in headers.items():
            header[key] = value

    url = "%s%s" % (host, uri)
    r = requests.request(method, url, data=body,
                         headers=header, cookies=cookie, verify=False)

    return r.status_code, r.text

java代码示例

java
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
        throws IOException {
    //获取uri路径
    String path = request.getURI().getPath() ;
    if(request.getURI().getRawQuery() != null && !"".equals(request.getURI().getRawQuery())) {
        path = path + "?" + request.getURI().getRawQuery();
    }

    //获取Content-type,“application/json"
    String contentType = request.getHeaders().getContentType().toString();

    //日期格式化
    DateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);
    dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
    String date = dateFormat.format(new Date());

    //open不参与签名,做替换处理
    if (path.startsWith("/open")) {
        path = path.replace("/open", "");
    }

    String sha256body;

    //body为空则为空,否则返回sha256(body)
    if (body != null && body.length > 0) {
        sha256body = HMacUtils.getSHA256StrJava(body);
    } else {
        sha256body = "";
    }

    //hmac-sha256(secret_key, Ver + HttpMethod + URI + Content-Type + Wps-Date + sha256(HttpBody))
    String signature = null;
    try {
        signature = HMacUtils.HMACSHA256("WPS-4" + request.getMethod() + path + contentType + date + sha256body,secretKey);
    } catch (Exception e) {
        e.printStackTrace();
    }

    request.getHeaders().add("Wps-Docs-Date",date);
    request.getHeaders().add("Wps-Docs-Authorization", String.format("WPS-4 %s:%s", accessKey, signature));
    return execution.execute(request, body);
}

sha256和hmac工具类代码:

java
package demo.utils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class HMacUtils {

    public static String HMACSHA256(String data, String key) throws Exception {
        Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
        SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
        sha256_HMAC.init(secret_key);
        byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString();
    }


    /**
     * 利用java原生的摘要实现SHA256加密
     * @param str 加密后的报文
     * @return
     */
    public static String getSHA256StrJava(byte[] str){
        MessageDigest messageDigest;
        String encodeStr = "";
        try {
            messageDigest = MessageDigest.getInstance("SHA-256");
            messageDigest.update(str);
            encodeStr = byte2Hex(messageDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return encodeStr;
    }
    /**
     * 将byte转为16进制
     * @param bytes
     * @return
     */
    private static String byte2Hex(byte[] bytes){
        StringBuffer stringBuffer = new StringBuffer();
        String temp = null;
        for (int i=0;i<bytes.length;i++){
            temp = Integer.toHexString(bytes[i] & 0xFF);
            if (temp.length()==1){
                //1得到一位的进行补0操作
                stringBuffer.append("0");
            }
            stringBuffer.append(temp);
        }
        return stringBuffer.toString();
    }
}

验证用例

拿下面请求参数签名

  • appId: ****************
  • appKey:********************************
  • url:/manage/v1/application/config/info
  • method:GET
  • Content-Type: application/json
  • Wps-Docs-Date: Sat, 07 Oct 2023 02:53:09 GMT
  • body:空

签名结果如下:

  • Content-Type:application/json
  • Wps-Docs-Date:Sat, 07 Oct 2023 02:53:09 GMT
  • Wps-Docs-Authorization:WPS-4 ****************:********************************

常见签名报错问题

根据返回结果中的debug参数信息,对比下本地签名的参数是否与之匹配,常见出错点:

  1. SK不一致(SK不能写死,因为不同环境SK 不同,需要从环境变量中获取)
  2. 实际签名的URI或Body不一致
  3. 签名使用的ContentType与实际请求的不一致