# API 调用说明

# 企微中台服务器出口 IP 地址

可通过接口 获取企微中台出口ip列表 获取

# 通用规则

如无特殊说明,企微中台接口遵循以下规则:

  1. 请求时必须在 header 中设置名为 orgcode 的参数。参数值是接入方注册企业信息时返回的企微中台租户代码
  2. 接口返回均为 json 格式,且必定带有 errcode 和 errmsg 参数,接入方提供的接口返回也必须按此实现:
参数 说明
errcode 返回码。0 表示成功
errmsg 对返回码的文本描述内容

# 调用方式

接入方需通过 HTTP 调用企微中台对外开放 API,并满足鉴权要求。 企微中台调用接入方提供的 HTTP 接口时,接入方需实现本文约定的鉴权要求。

# 调用 IP 限制

接入方调用企微中台对外开放 API 时使用的 IP 需提前告知缪伟宁进行配置。

# 鉴权方式

# 算法描述

image-20210810181312534

接入时,需找周勇申请 access-key 和 secret-key,然后在 HTTP 请求的 header 中带上鉴权所需的字段:

  1. Date:当前时间,GMT 格式,如 Tue, 19 Jan 2021 11:33:20 GMT
  2. X-Hmac-Access-Key:即 access-key
  3. X-Hmac-Algorithm:固定为 hmac-sha256
  4. X-Hmac-Signature:签名值,计算方式如下:
signature = HMAC-SHAx-HEX(secret-key, signing_string)

signing_string = HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key + \n + Date + \n

HTTP URI 不能为空,必须以"/"开头

canonical_query_string 生成步骤如下:
  1. 提取 URL 中的 query 项,即 URL 中 ? 后面的 key1=valve1&key2=valve2 字符串;
  2. 将 query 根据&分隔符拆开成若干项,每一项是 key=value 或者只有 key 的形式;
  3. 对拆开后的每一项进行编码处理,分以下两种情况:
    a. 当该项只有 key 时,转换公式为 url_encode(key) + "=" 的形式;
    b. 当该项是 key=value 的形式时,转换公式为 url_encode(key) + "=" + url_encode(value) 的形式,value 可以是空字符串;
  4. 将每一项转换后,以 key 按照字典顺序(ASCII 码由小到大)排序,并使用 & 符号连接起来。

# 示例

GET 请求 http://127.0.0.1:9080/url?zoo=333&params1=aaa,bbb&a&c=&zoo=22,access-key 为 b5f6c8e5-e9b3-4a8a-9d36-0f47495eaec5,secret-key 为 v8xfn5xrf2cykkt5d3q2e823nekzhy7x

生成的 signing_string 如下:

GET
/url
a=&c=&params1=aaa%2Cbbb&zoo=333&zoo=22
b5f6c8e5-e9b3-4a8a-9d36-0f47495eaec5
Thu, 29 Jul 2021 11:51:11 GMT

header 中传递的鉴权信息如下:

Date:Thu, 29 Jul 2021 11:51:11 GMT
X-Hmac-Access-Key:b5f6c8e5-e9b3-4a8a-9d36-0f47495eaec5
X-Hmac-Algorithm:hmac-sha256
X-Hmac-Signature:cRkXoqdv4i9FZfClGhowuGcysEq0wh6/w3KJqKriA1Q=

注意:鉴权失败时的状态码为 401,body 为 {"message":"鉴权失败的原因"}。

# Go 语言实现参考

package hmac_auth

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"hash"
	"net/http"
	"net/url"
	"sort"
	"strings"
	"time"
)

const (
	AlgorithmHmacSha1   = "hmac-sha1"
	AlgorithmHmacSha256 = "hmac-sha256"
	AlgorithmHmacSha512 = "hmac-sha512"
)

type HeaderKeyConfig struct {
	Signature     string
	Algorithm     string
	Date          string
	Access        string
	SignedHeaders string
}

type HmacAuth struct {
	AccessKey       string
	SecretKey       string
	Algorithm       string
	EncodeUriParam  bool
	SignedHeaders   []string
	HeaderKeyConfig *HeaderKeyConfig
	HashFunc        func() hash.Hash
}

func NewHmacAuth(accessKey, secretKey string) *HmacAuth {
	return &HmacAuth{
		AccessKey:      accessKey,
		SecretKey:      secretKey,
		EncodeUriParam: true,
		SignedHeaders:  []string{},
		HashFunc:       sha256.New,
		Algorithm:      AlgorithmHmacSha256,
		HeaderKeyConfig: &HeaderKeyConfig{
			Signature:     "X-HMAC-SIGNATURE",
			Algorithm:     "X-HMAC-ALGORITHM",
			Date:          "Date",
			Access:        "X-HMAC-ACCESS-KEY",
			SignedHeaders: "X-HMAC-SIGNED-HEADERS",
		},
	}
}

func (h *HmacAuth) Attach(req *http.Request, t time.Time) {
	date := t.UTC().Format(http.TimeFormat)
	req.Header.Set(h.HeaderKeyConfig.Access, h.AccessKey)
	if 0 < len(h.SignedHeaders) {
		req.Header.Set(h.HeaderKeyConfig.SignedHeaders, h.getSignedHeadersString(req.Header))
	}
	req.Header.Set(h.HeaderKeyConfig.Algorithm, h.Algorithm)
	req.Header.Set(h.HeaderKeyConfig.Signature, h.generateSignature(req, date))
	req.Header.Set(h.HeaderKeyConfig.Date, date)
}

func (h *HmacAuth) generateSignature(req *http.Request, date string) string {
	path := req.URL.Path
	if path == "" {
		path = "/"
	}

	var b bytes.Buffer
	b.WriteString(req.Method)
	b.WriteString("\n")
	b.WriteString(path)
	b.WriteString("\n")
	b.WriteString(h.generateCanonicalQueryString(req.URL.Query()))
	b.WriteString("\n")
	b.WriteString(h.AccessKey)
	b.WriteString("\n")
	b.WriteString(date)
	b.WriteString("\n")

	canonicalHeader := h.generateCanonicalHeader(req.Header)
	if canonicalHeader != "" {
		b.WriteString(h.generateCanonicalHeader(req.Header))
	}

	signingStr := b.String()

	handle := hmac.New(h.HashFunc, []byte(h.SecretKey))
	handle.Write([]byte(signingStr))
	return base64.StdEncoding.EncodeToString(handle.Sum(nil))
}

func (h *HmacAuth) generateCanonicalQueryString(params url.Values) string {
	length := len(params)
	if length <= 0 {
		return ""
	}

	keys := make([]string, length)
	i := 0
	for key := range params {
		keys[i] = key
		i++
	}
	sort.Strings(keys)

	items := make([]string, 0, length*2)
	for _, key := range keys {
		if h.EncodeUriParam {
			for _, v := range params[key] {
				r := url.QueryEscape(key) + "=" + url.QueryEscape(v)

				// 网关 hmac-auth 插件是 lua 实现的
				// lua 会将空格处理为 %20,但 go 的 QueryEscape 将空格处理为 +,造成鉴权不通过
				// 这里需要替换一下,保证和网关实现一致
				r = strings.ReplaceAll(r, "+", "%20")
        // go 的 QueryEscape 会将 * 转义为 %2A,但 lua 不会,所以计算鉴权时需转换回 *
				r = strings.ReplaceAll(r, "%2A", "*")

				items = append(items, r)
			}
		} else {
			for _, v := range params[key] {
				items = append(items, key+"="+v)
			}
		}
	}

	return strings.Join(items, "&")
}

func (h *HmacAuth) generateCanonicalHeader(header http.Header) string {
	var buffer bytes.Buffer

	for _, key := range h.SignedHeaders {
		value := header.Get(key)
		if value == "" {
			continue
		}

		buffer.WriteString(key)
		buffer.WriteString(":")
		buffer.WriteString(value)
		buffer.WriteString("\n")
	}

	return buffer.String()
}

func (h *HmacAuth) getSignedHeadersString(header http.Header) string {
	var existSignedHeaders []string

	for _, key := range h.SignedHeaders {
		value := header.Get(key)
		if value != "" {
			existSignedHeaders = append(existSignedHeaders, key)
		}
	}

	return strings.Join(existSignedHeaders, ";")
}

# Python 语言实现参考

感谢云沃享团队 刘畅 贡献

import base64
import datetime
import hmac
import urllib.parse
from hashlib import sha256
from urllib.parse import unquote, urlparse


def cal_encrypt_sign(
    method: str, url: str, access_key: str, secret_key: str, dt_str=None
):
    """计算签名

    Args:
        method (str): [请求方式]
        url (str): [请求url]
        access_key (str): [access_key]
        secret_key ([type]): [secret_key]

    Returns:
        [type]: [description]
    """
    url = unquote(url)
    o = urlparse(url)
    url_encode = urllib.parse.quote
    # params
    pair_list = (
        list(
            [*pair.split("="), idx] if len(pair.split("=")) == 2 else [pair, None, idx]
            for idx, pair in enumerate(o.query.split("&"))
        )
        if o.query
        else ""
    )
    sorted_quote = []
    # key 按照字典顺序(ASCII 码由小到大)排序(另外,go中按key排序后同名按原次序排序, python为与中台服务保持一致,另加一个同名按次序排序
    sorted_pair_list = sorted(pair_list, key=lambda x: (x[0], x[2]))
    for item in sorted_pair_list:
        if item[1] == None:
            # a. 当该项只有 key 时,转换公式为 url_encode(key) + "=" 的形式;
            sorted_quote.append(url_encode(item[0]) + "=")
        else:
            # b. 当该项是 key=value 的形式时,转换公式为 url_encode(key) + "=" + url_encode(value) 的形式,value 可以是空字符串;
            sorted_quote.append(url_encode(item[0]) + "=" + url_encode(item[1]))

    canonical_query_string = "&".join(sorted_quote)
    print("canonical_query_string", canonical_query_string)
    # fmt 时间
    date_fmt_str = (
        datetime.datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT")
        if not dt_str
        else dt_str
    )
    # date_fmt_str = "Thu, 29 Jul 2021 11:51:11 GMT"
    signing_string = (
        f"{method}\n{o.path}\n{canonical_query_string}\n{access_key}\n{date_fmt_str}\n"
    )
    print(signing_string)

    sign = base64.b64encode(
        hmac.new(
            secret_key.encode("utf-8"), signing_string.encode("utf-8"), digestmod=sha256
        ).digest()
    )
    sign_string = str(sign, "utf-8")

    return {
        "Date": date_fmt_str,
        "X-Hmac-Access-Key": access_key,
        "X-Hmac-Algorithm": "hmac-sha256",
        "X-Hmac-Signature": sign_string,
    }


if __name__ == "__main__":
    url = "http://127.0.0.1:9080/url?zoo=333&params1=aaa,bbb&a&c=&zoo=22"
    access_key = "b5f6c8e5-e9b3-4a8a-9d36-0f47495eaec5"
    secret_key = "v8xfn5xrf2cykkt5d3q2e823nekzhy7x"
    headers = cal_encrypt_sign("GET", url, access_key, secret_key)
    print(headers)
    from app import app
    from config import Config

    app.app_context().push()

    cal_encrypt_sign(
        "GET",
        url,
        Config.MIDDLE_SERVICE_ACCESS_KEY,
        Config.MIDDLE_SERVICE_SECRET_KEY,
    )