# API 调用说明
# 企微中台服务器出口 IP 地址
可通过接口 获取企微中台出口ip列表 获取
# 通用规则
如无特殊说明,企微中台接口遵循以下规则:
- 请求时必须在 header 中设置名为 orgcode 的参数。参数值是接入方注册企业信息时返回的企微中台租户代码
- 接口返回均为 json 格式,且必定带有 errcode 和 errmsg 参数,接入方提供的接口返回也必须按此实现:
参数 | 说明 |
---|---|
errcode | 返回码。0 表示成功 |
errmsg | 对返回码的文本描述内容 |
# 调用方式
接入方需通过 HTTP 调用企微中台对外开放 API,并满足鉴权要求。 企微中台调用接入方提供的 HTTP 接口时,接入方需实现本文约定的鉴权要求。
# 调用 IP 限制
接入方调用企微中台对外开放 API 时使用的 IP 需提前告知缪伟宁进行配置。
# 鉴权方式
# 算法描述
接入时,需找周勇申请 access-key 和 secret-key,然后在 HTTP 请求的 header 中带上鉴权所需的字段:
- Date:当前时间,GMT 格式,如
Tue, 19 Jan 2021 11:33:20 GMT
- X-Hmac-Access-Key:即 access-key
- X-Hmac-Algorithm:固定为
hmac-sha256
- 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¶ms1=aaa,bbb&a&c=&zoo=22,access-key 为 b5f6c8e5-e9b3-4a8a-9d36-0f47495eaec5,secret-key 为 v8xfn5xrf2cykkt5d3q2e823nekzhy7x
生成的 signing_string 如下:
GET
/url
a=&c=¶ms1=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¶ms1=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,
)
← 自建应用安全性升级指南 页面插件接入 →