鉴权指南
请求地址
//视具体的ai能力而定
https://rest-api.xfyun.cn/v2/***
签名及鉴权
- 调用API须对HTTP请求进行签名,服务端通过签名来识别用户并验证其合法性,
- 用户向服务器申请生成一个凭证,凭证是key/secret的密钥对。客户端对Method、Accept、Date以及其他Header字段和Url进行规定方式拼接后,通过哈希算法(如HMAC-SHA256)和用户的secret对请求进行签名。最后将key、使用的算法、参与签名的头部字段以及计算后的签名放入头部字段"Authorization"中
请求头示例:
Content-Type:application/json Accept:application/json,version=1.0 Date : Tue, 26 Jun 2018 12:27:03 UTC Host:"your host" Digest:SHA-256=xxxxxxxxxxxxxxxxxxxxxxxx Authorization: hmac api_key="your_key", algorithm="hmac-sha256", headers="host date request-line", signature="base64_digest"
鉴权方式
客户端需要将请求的相关参数使用 hmac-sha256 算法计算摘要生成signature,构建 Authorization header。服务端会解析Authorization header,并按照同样的方式计算singature, 并比signature要是否相等。相等则鉴权通过。
Header 详细描述
签名参数 | 描述 |
---|---|
Date | 请求日期,utc 时区。格式如 : Tue, 26 Jun 2018 12:27:03 UTC |
Host | 请求主机,计算签名时需要该header |
Authorization | 鉴权参数,具体构建方法如下 |
Digest | body 摘要,计算方法为 "SHA256="+sha256(${body}) |
签名生成公式
api_key="${api_key}",algorithm="hmac-sha256",headers="host date
request-line digest",signature="${signature}"
解释: api_key: 开放平台申请的apikey algorithm: 签名计算算法,支持hmac-sha256 headers: 需要参与签名的参数,需要最少有 host date request-line 这三个 signature: 计算的签名
鉴权详细流程
1. 构建待签名字符串 signature_origin_str
signature_origin_str格式为:
host: ${host}\ndate: ${date}\n${method} ${path} HTTP/1.1\ndigest: ${digest}
- host: 请求的主机头 例如。iat-api.xfyun.cn ,需要放到请求header中,并且计算签名中使用的host 需要和http 报文中的header 保持完全一致。
- date: 当前的时间戳,格式为 Wed, 08 Jun 2022 08:12:15 UTC, 需要放到请求header 中。
- method: 请求方法 支持 GET POST DELETE PATCH PUT 需要和当前的请求的http method 一致
- path: 请求的path。不需要包含query string 部分('?'及后面的部分),例如url 为 '/v2/iat? a=b&c=d' 时,只需要取 '/v2/iat' 部分作为path 的值。
- digest: 请求body 摘要部分,计算方法为 "SHA256="+sha256(${body})
注意:':' 后面有一个空格, '\n' 为换行符,HTTP/1.1 为http 协议版本号,如果客户端使用 HTTP/1.0 协议,则值应该改为 HTTP/1.0
2. 计算signature
计算方法为: 使用 hmac-sha256 算法计算signature_origin_str 的签名,并做base64 编 码。
signature = base64(hmac-
sha256(${signature_origin_str},${api_secret}))
其中api_secret 为平台侧获取的密钥对中的密钥部分,signature_origin_str 为上一步拼接的 参数
3. 拼接authorization Header
authorization_raw = api_key="${api_key}",algorithm="hmac-
sha256",headers="host date request-line digest",signature="${signature}"
签名生成示例:
假如有以下的请求, 请求地址为 http://iat-api.xfyun.cn/v2/iat 请求方法为 POST ,用户的 api_key = 5ccdf2b4d1b5cdf81846697bf8bcd05d ,api_secret = B00TFRS9KDCfTrdX5JQwhVSXaFoHLy34 , 当前的时间为:Wed, 08 Jun 2022 09:00:06 UTC,请求的body内容为:
hello world
1 那么signature_origin_str应该拼接成这样
host: iat-api.xfyun.cn
date: Wed, 08 Jun 2022 09:00:06 UTC
POST HTTP/1.1
digest: SHA256=uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=
2 使用signature_origin_str计算后的 signature =
rRU2FA174RdsqpdxGzrLmJ6C1CPk5GgfP7bUQToxQIw=
3 则authorization header 为
api_key="5ccdf2b4d1b5cdf81846697bf8bcd05d",algorithm="hmac-
sha256",headers="host date request-
line",signature="rRU2FA174RdsqpdxGzrLmJ6C1CPk5GgfP7bUQToxQIw="
4 最终的请求header为
Authorization:api_key="5ccdf2b4d1b5cdf81846697bf8bcd05d",algorithm="hmac-
sha256",headers="host date request-
line",signature="VhEap7PkvX7ujjx8DjBtkRZFwQDIEOc62EM+M9N+pf8="
Host: iat-api.xfyun.cn
Date: Wed, 08 Jun 2022 08:12:15 UTC
Digest: SHA256=uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=
用户可以使用该示例来验证自己的签名算法实现的是否正确
关键函数说明
函数名 称 | 说明 |
---|---|
hmac-256 | 一种的标准的签名算法,提供密钥和待签名数据,就可以计算出签名摘要,计 sha256 算的结果是原始的字节,不能经过编码。 |
base64 | 一种标准的将字节编码成可见字符串的编码方法,注意使用标准的base64 而不 是base64_url |
sha256 | sha256 签名算法 |
鉴权失败返回示例:
HTTP/1.1 403 Forbidden
Date: Thu, 06 Dec 2018 07:55:16 GMT
Content-Length: 116
Content-Type: text/plain; charset=utf-8
{
"message": "HMAC signature does not match"
}
鉴权不过的可能原因分析:
用户可以通过服务端返回的httpCode 和响应body部分的 message字段 来判断 是鉴权哪里 出了问题。
- httpCode = 401 message = Unauthorized 该错误发生的原因为用户没有传 Authorization header
- httpCode = 401 message = HMAC signature cannot be verified,fail to retrieve credential 该错误发生的原因为服务端无法查询到api_key 检查apikey 是否正确
- httpCode = 401 message = HMAC signature cannot be verified,enforce header 'host' not used for HMAC Authentication 该错误发生的原因为服务端,解析'Authorization' header 失败,检查 'Authorization' header的格式是否满足文档要求。
- httpCode = 403 message = HMAC signature cannot be verified, a valid date or x-date header is required for HMAC Authentication 该错误发生的原因为Date header格式和文档要求不一致或者和服务端时间偏差超 过300s。检查客户端机器的时间戳是否和互联网同步,或者是时区是否正确。
- httpCode= 403 message = not found 该错误发生的原因为找不到请求地址,检查请求地址是否正确
- httpCode= 401 message = HMAC signature does not match 该错误发生的原因是服务端计算的签名和客户端计算的签名值不匹配,可能的原因有多种, 可以尝试以下方法解决。
- 检查 api_secret 是否正确
- 检查 signature_origin_str 格式是否拼接正确,正确的格式文档上文部分有描述。 可以打印参数出来进行对比
- 检查hmac-sha256 计算的签名signature长度是否是44。如果是88 ,则计算 base64 时使用的字符串是已经经过16进制编码的了,需要使用原始没有经过编码 的字节流做base64 编码。
- 检查是否使用了nginx 代理请求,nginx 默认使用http 1.0 代理请求,会导致服务 端获取的http 版本号 为 HTTP/1.0 造成签名计算不匹配。需要设置nginx 使用 HTTP/1.1 协议请求服务端
- 如果以上方法都没有解决,可以尝试使用抓包工具来抓包分析报文,某些框架可 能发出去的http 报文可能代码中写的有些许不一致的地方,比如 host,path,和 HTTP 版本号部分可能和预想的不一致。可以重点看报文中这些参数和代码中拼 接signature_origin_str用的对应参数是否一致
伪代码
func assembleRequestHeader(requestUrl,method,body,apikey,apisecret){
url = urlparse(requestUrl)
host = url.host
path = url.path
date = now().format('Tue, 26 Jun 2018 12:27:03 UTC')
request-line = "$method $path HTTP/1.1"
signature_headers = "host date request-line"
signature_strs = "host: $host\ndate: $date\n$request-line"
digest = ""
if body != nil:
signature_headers = "host date request-line digest"
digest = "SHA-256="+base64(sha256(body))
signature_strs = "host: $host\ndate: $date\n$request-line\ndigest: $digest"
else:
signature_strs = "host: $host\ndate: $date\n$request-line"
signature = base64(hmac-sha256(signature_strs,apisecret))
authorization = 'api_key="$apikey", algorithm="hmac-sha256" ,headers="$signature_headers", signature="$signature"'
return {
"Host":host,
"Date":date,
"Digest":digest,
"Authorization":authorization,
}
}
生成签名示例代码
golang
package iflyauth
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"time"
)
// 构建鉴权headers
//@requestUrl: like http://api.xfyun.cn
//@method: GET. POST. etc....
//@body : 请求body
func NewAuthHeaders(requestUrl, method string, apiKey, apiSecret string, body []byte) map[string]string {
bodySign := ""
if body == nil {
bodySign = sha256Base64([]byte(nil))
} else {
bodySign = sha256Base64(body)
}
bodySign = "SHA256=" + bodySign
u, err := url.Parse(requestUrl)
if err != nil {
panic("parse url error" + err.Error())
}
host := u.Host
date := time.Now().UTC().Format(time.RFC1123)
if u.Path == ""{
u.Path = "/"
}
requestLine := method + " " + u.Path + " HTTP/1.1"
signUrl := fmt.Sprintf("host: %s\ndate: %s\n%s\ndigest: %s", host, date, requestLine, bodySign)
signature := hmacSha256Base64([]byte(apiSecret), []byte(signUrl))
authorization := fmt.Sprintf(`api_key="%s", algorithm="hmac-sha256", headers="host date request-line digest", signature="%s"`, apiKey, signature)
return map[string]string{
"host": host,
"date": date,
"authorization": authorization,
"digest": bodySign,
}
}
func sha256Base64(b []byte) string {
h := sha256.New()
h.Write(b)
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func hmacSha256Base64(secret []byte, data []byte) string {
h := hmac.New(sha256.New, secret)
h.Write(data)
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
java
/**
* 计算签名所需要的header参数 (http 接口)
* @param requestUrl like 'http://rest-api.xfyun.cn/v2/iat'
* @param apiKey
* @param apiSecret
* @method request method POST/GET/PATCH/DELETE etc....
* @param body http request body
* @return header map ,contains all headers should be set when access api
*/
public static Map<String ,String> assembleRequestHeader(String requestUrl, String apiKey, String apiSecret,String method,byte[] body){
URL url = null;
try {
url = new URL(requestUrl);
// 获取日期
SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
String date = format.format(new Date());
//计算body 摘要(SHA256)
MessageDigest instance = MessageDigest.getInstance("SHA-256");
instance.update(body);
String digest = "SHA256="+ Base64.getEncoder().encodeToString(instance.digest());
//date = "Thu, 19 Dec 2024 07:47:57 GMT";
String host = url.getHost();
int port = url.getPort(); // port >0 说明url 中带有port
if (port > 0){
host = host +":"+port;
}
String path = url.getPath();
if ("".equals(path) || path == null){
path = "/";
}
//构建签名计算所需参数
StringBuilder builder = new StringBuilder().
append("host: ").append(host).append("\n").//
append("date: ").append(date).append("\n").//
append(method).append(" ").append(path).append(" HTTP/1.1").append("\n").
append("digest: ").append(digest);
Charset charset = Charset.forName("UTF-8");
System.out.println(builder.toString());
//使用hmac-sha256计算签名
Mac mac = Mac.getInstance("hmacsha256");
//System.out.println(builder.toString());
SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(charset), "hmacsha256");
mac.init(spec);
byte[] hexDigits = mac.doFinal(builder.toString().getBytes(charset));
String sha = Base64.getEncoder().encodeToString(hexDigits);
// 构建header
String authorization = String.format("hmac-auth api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey, "hmac-sha256", "host date request-line digest", sha);
Map<String,String > header = new HashMap<String ,String>();
header.put("authorization",authorization);
header.put("host",host);
header.put("date",date);
header.put("digest",digest);
System.out.println("header " + header.toString());
return header;
} catch (Exception e) {
throw new RuntimeException("assemble requestHeader error:"+e.getMessage());
}
}
javascript
function assembleRequestHeader(host,path,method,apiKey,apiSecret,body) {
var date = new Date().toGMTString()
var algorithm = 'hmac-sha256'
var headers = 'host date request-line digest'
var digest = "SHA256="+CryptoJS.enc.Base64.stringify(CryptoJS.SHA256(body))
var signatureOrigin = `host: ${host}\ndate: ${date}\n${method} ${path} HTTP/1.1\ndigest: ${digest}`
var signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret)
var signature = CryptoJS.enc.Base64.stringify(signatureSha)
var authorization = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`
return {
// 'Host':host,
'X-Date':date, //js 中可能无法设置Date header,使用X-Date
'Authorization':authorization,
'Digest':digest,
}
}
var headers = assembleRequestHeader('rest-api-gz.xfyun.cn','/v2/tts','POST','xxxxxxxxxxxxxxx','xxxxxxxxxxxxxxx','')
python3
from datetime import datetime
from wsgiref.handlers import format_date_time
from time import mktime
import hashlib
import base64
import hmac
from urllib.parse import urlparse
import sys
# calculate sha256 and encode to base64
def sha256base64(data):
sha256 = hashlib.sha256()
sha256.update(data)
digest = base64.b64encode(sha256.digest()).decode(encoding='utf-8')
return digest
# build auth request url
def assemble_auth_header(requset_url, method="GET", api_key="", api_secret="", body=""):
u = urlparse(requset_url)
# u = parse_url(requset_url)
host = u.hostname
path = u.path
now = datetime.now()
date = format_date_time(mktime(now.timetuple()))
digest = "SHA256=" + sha256base64(body.encode())
# date = "Thu, 12 Dec 2019 01:57:27 GMT"
signature_origin = "host: {}\ndate: {}\n{} {} HTTP/1.1\ndigest: {}".format(host, date, method, path, digest)
# print(signature_origin)
signature_sha = hmac.new(api_secret.encode('utf-8'), signature_origin.encode('utf-8'),
digestmod=hashlib.sha256).digest()
signature_sha = base64.b64encode(signature_sha).decode(encoding='utf-8')
authorization = "api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"" % (
api_key, "hmac-sha256", "host date request-line digest", signature_sha)
# print(authorization_origin)
headers = {
"host": host,
"date": date,
"authorization": authorization,
"digest": digest,
}
return headers
requrl = "http://rest-api.xfyun.cn/v2/itr"
import requests
import json
import time
body = {
"common": {},
"business": {},
"data": {}
}
now = time.time()
bds = json.dumps(body)
headers = assemble_auth_header(requrl, method="POST", api_key="xxxxxxxxx",
api_secret="xxxxxxxxxxxxx", body=bds)
resp = requests.post(requrl, headers=headers, data=bds)
print(resp.status_code, resp.text)
php
class http_test {
function tocurl($url, $header, $content){
$ch = curl_init();
if(substr($url,0,5)=='https'){
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 跳过证书检查
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // 从证书中检查SSL加密算法是否存在
curl_setopt($ch, CURLOPT_SSLVERSION, 1);
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL, $url);
if (is_array($header)) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
}
curl_setopt($ch, CURLOPT_POST, true);
if (!empty($content)) {
if (is_array($content)) {
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($content));
} else if (is_string($content)) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $content);
}
}
$response = curl_exec($ch);
$error=curl_error($ch);
//var_dump($error);
if($error){
die($error);
}
$header = curl_getinfo($ch);
curl_close($ch);
$data = array('header' => $header,'body' => $response);
return $data;
}
function xfyun() {
$app_id = "XXXXX";
$api_sec = "XXXXXXXXX";
$api_key = "XXXXXXXXX";
$resource = "resource/xx";
$url = "xx";
$host="xx.xxx.cn" #请求host
$path = "/v2/xxx" #请求path
//body组装
$body = json_encode($this->getBody($app_id, $resource));
// 组装http请求头
//$date = gmstrftime("%a, %d %b %Y %H:%M:%S %Z", time());
$date =gmdate('D, d M Y H:i:s') . ' GMT';
$digestBase64 = "SHA-256=".base64_encode(hash("sha256", $body, true));
$builder = sprintf("host: %s\ndate: %s\nPOST %s HTTP/1.1\ndigest: %s", $host, $date,$path, $digestBase64);
$sha = base64_encode(hash_hmac("sha256", $builder, $api_sec, true));
$authorization = sprintf("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", $api_key, "hmac-sha256", "host date request-line digest", $sha);
$header = [
"Authorization: ".$authorization,
'Content-Type: application/json',
'Accept: application/json,version=1.0',
'Host: rest-api.xfyun.cn', //这里要替换成域名真实的host
'Date: '.$date,
'Digest: '.$digestBase64
];
$response = $this->tocurl($url, $header, $body);
var_dump($response['body']);
}
}
公共请求参数
- 请求参数说明
参数名 | 类型 | 必传 | 描述 |
---|---|---|---|
common | object | 是 | 公共参数,请求时放在body中上传 |
business | object | 是 | 业务参数,请求时放在body中上传,详细参数见具体AI能力 |
data | object | 是 | 业务数据流参数,请求时放在body中上传,详细参数见具体AI能力 |
- common参数说明:
参数名 | 类型 | 必传 | 描述 |
---|---|---|---|
app_id | string | 是 | 在平台申请的app id信息 |
uid | string | 否 | 请求用户服务返回的uid,用户及设备级别个性化功能依赖此参数 |
device_id | string | 否 | 请求方确保唯一的设备标志,设备级别个性化功能依赖此参数,uid没传的情况下建议上传这个参数,如果没传,服务端会依次从device.imei、device.imsi、device.mac、device.other中选取不为空的参数作为设备唯一标志 |
request_id | string | 否 | 请求唯一标识 |
device.imei | string | 否 | 设备imei信息 |
device.imsi | string | 否 | 设备imei信息 |
device.mac | string | 否 | 设备imei信息 |
device.other | string | 否 | 设备imei信息 |
net.type | string | 否 | 网络类型,可选值为wifi、2G、3G、4G、5G |
net.isp | string | 否 | 运营商信息,可选值为CMCC、CUCC、CTCC、other |
app.ver | string | 否 | 集成此API应用的版本号 |
- 公共请求参数示例
{ "common":{ "app_id":"", "uid":"", "device_id":"", "device.imei":"", "device.imsi":"", "device.mac":[], "device.other":"", "auth.auth_id":"", "auth.auth_source":"", "net.type":"", "net.isp":"", "app.ver":"" }, "business":{ 业务参数 }, "data":{ 业务数据流 } }
公共返回参数
- 返回参数说明:
参数名 | 类型 | 描述 |
---|---|---|
code | int | 返回码,0表示成功,其它表示异常,详情请参考错误码。 |
message | string | 描述信息 |
data | object | 参考各AI能力协议中的详细定义内容 |
sid | string | 本次会话的id |
- 返回参数示例
{ "code": 0, "message": "success", "data": {}, "sid": "xxxd7f74204@gz1673f6ae3458410510" }