使用流程
- 获取鉴权码:从讯飞开放平台申请appid,并添加webapi听写(流式接口)获取接口密钥APIKey 和 APISecret
- 集成websocket接口: 通用接口 + 参数说明
- 参照集成demo实现
websocket使用注意事项:
- 服务端支持的websocket-version 为13,请确保客户端使用的框架支持该版本。
- 服务端返回的所有的帧类型均为TextMessage,对应于原生websocket的协议帧中opcode=1,请确保客户端解析到的帧类型一定为该类型,如果不是,请尝试升级客户端框架版本,或者更换技术框架。
- 如果出现分帧问题,即一个json数据包分多帧返回给了客户端,导致客户端解析json失败。出现这种问题大部分情况是客户端的框架对websocket协议解析存在问题,如果出现请先尝试升级框架版本,或者更换技术框架。
- 客户端回话结束后如果需要关闭连接,尽量保证传给服务端的websocket错误码为1000(如果客户端框架没有提供关闭时传错误码的接口。则无需关注本条)。
鉴权指南
请求地址
//视具体的ai能力而定
wss://ws-api.xfyun.cn/v2/***
握手&鉴权
鉴权方式
客户端需要将请求的相关参数使用 hmac-sha256 算法计算摘要生成signature,构建 authorization 参数。服务端会解析authorization 参数,并按照同样的方式计算singature, 并比signature要是否相等。相等则鉴权通过。
示例url:
wss://ws-api.xfyun.cn/v2/ivw?authorization=aG1hYyB1c2VybmFtZT0iMTAwSU1FIiwgYWxnb3JpdGhtPSJobWFjLXNoYTI1NiIsIGhlYWRlcnM9Imhvc3QgZGF0ZSByZXF1ZXN0LWxpbmUiLCBzaWduYXR1cmU9IlVSbnk4M3o1elJsNWF1ODl1YXhUL1dGdUtWejZVNkdkWDdDV25SMGdueWc9Ig%3D%3D&date=Tue%2C+18+Dec+2018+09%3A08%3A49+UTC&host=10.1.87.70%3A8000
鉴权参数
参数 | 类型 | 必须 | 说明 | 示例 |
---|---|---|---|---|
host | string | 是 | 请求主机 | iat-api.xfyun.cn |
date | string | 是 | 当前时间戳,RFC1123格式(Mon, 02 Jan 2006 15:04:05 GMT) | Fri, 18 Jan 2019 07:21:29 UTC |
authorization | string | 是 | 使用base64编码的签名相关信息(签名基于hmac-sha256计算) | aG1hYyB1c2VybmFtZT0iNDNiMThiMWU4MGNmMzlmODFiMGEyMjkxNjE5YTU1OWUiLCBhbGdvcml0aG09ImhtYWMtc2hhMjU2IiwgaGVhZGVycz0iaG9zdCBkYXRlIHJlcXVlc3QtbGluZSIsIHNpZ25hdHVyZT0iYW4wQ2xYdGRQTG92ZDdFT1phL1p2VzNXT0ZRUlBiU0NqMTViRzZNcXFSZz0i |
签名生成公式
api_key="${api_key}",algorithm="hmac-sha256",headers="host date
request-line",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
- host: 请求的主机头 例如。iat-api.xfyun.cn
- date: 当前的时间戳,格式为 Wed, 08 Jun 2022 08:12:15 UTC
- method: 请求方法 支持 GET POST DELETE PATCH PUT 需要和当前的请求的http method 一致
- path: 请求的path。不需要包含query string 部分('?'及后面的部分),例如url 为 '/v2/iat? a=b&c=d' 时,只需要取 '/v2/iat' 部分作为path 的值。
注意:':' 后面有一个空格, '\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_raw 参数
authorization_raw = api_key="${api_key}",algorithm="hmac-
sha256",headers="host date request-line",signature="${signature}"
4. 构建authoirzation 参数
authorization = base64(authorization_raw)
5. 拼接请求url
url = ${api_url}?
authorization=${authorization}&host=${host}&date=${date}
签名生成示例:
假如有以下的请求, 请求地址为 ws://iat-api.xfyun.cn/v2/iat 请求方法为 GET ,用户的 api_key = 5ccdf2b4d1b5cdf81846697bf8bcd05d ,api_secret = B00TFRS9KDCfTrdX5JQwhVSXaFoHLy34 , 当前的时间为:Wed, 08 Jun 2022 09:00:06 UTC 1 那么signature_origin_str应该拼接成这样
host: iat-api.xfyun.cn
date: Wed, 08 Jun 2022 09:00:06 UTC
GET /v2/iat HTTP/1.1
2 使用signature_origin_str计算后的 signature =
VhEap7PkvX7ujjx8DjBtkRZFwQDIEOc62EM+M9N+pf8=
3 则authorization_raw 为
api_key="5ccdf2b4d1b5cdf81846697bf8bcd05d",algorithm="hmac-
sha256",headers="host date request-
line",signature="VhEap7PkvX7ujjx8DjBtkRZFwQDIEOc62EM+M9N+pf8="
4 autorization 参数为authorization_raw 进行base64 编码后的结果
YXBpX2tleT0iNWNjZGYyYjRkMWI1Y2RmODE4NDY2OTdiZjhiY2QwNWQiLGFsZ29ya
XRobT0iaG1hYy1zaGEyNTYiLGhlYWRlcnM9Imhvc3QgZGF0ZSByZXF1ZXN0LWxpbm
UiLHNpZ25hdHVyZT0iSk9CYTBTV3d0WFVYTjU4ZWlsSENKa21oZnVQVHpvWXcrSHp
6UXpKa3ExST0i
5 最终的请求url 为
ws://iat-api.xfyun.cn/v2/iat?
authorization=YXBpX2tleT0iNWNjZGYyYjRkMWI1Y2RmODE4NDY2OTdiZjhiY2Q
wNWQiLGFsZ29yaXRobT0iaG1hYy1zaGEyNTYiLGhlYWRlcnM9Imhvc3QgZGF0ZSBy
ZXF1ZXN0LWxpbmUiLHNpZ25hdHVyZT0iVmhFYXA3UGt2WDd1amp4OERqQnRrUlpGd
1FESUVPYzYyRU0rTTlOK3BmOD0i&date=Wed%2C+08+Jun+2022+09%3A00%3A06+
UTC&host=iat-api.xfyun.cn
用户可以使用该示例来验证自己的签名算法实现的是否正确
关键函数说明
函数名 称 | 说明 |
---|---|
hmac-256 | 一种的标准的签名算法,提供密钥和待签名数据,就可以计算出签名摘要,计 sha256 算的结果是原始的字节,不能经过编码。 |
base64 | 一种标准的将字节编码成可见字符串的编码方法,注意使用标准的base64 而不 是base64_url |
握手失败返回示例:
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用的对应参数是否一致
生成鉴权url示例代码
golang
//@hosturl : like wss://ws-api.xfyun.cn/v2/iat
//@apikey : apiKey
//@apiSecret : apiSecret
//@method: request method , GET 、POST ...
func assembleAuthUrl(hosturl string,method, apiKey, apiSecret string) string {
ul, err := url.Parse(hosturl)
if err != nil {
fmt.Println(err)
}
//签名时间
date := time.Now().UTC().Format(time.RFC1123)
//参与签名的字段 host ,date, request-line
signString := []string{"host: " + ul.Host, "date: " + date, method + " " + ul.Path + " HTTP/1.1"}
//拼接签名字符串
sgin := strings.Join(signString, "\n")
//签名结果
sha := HmacWithShaTobase64("hmac-sha256", sgin, apiSecret)
//构建请求参数 此时不需要urlencoding
authUrl := fmt.Sprintf("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey,
"hmac-sha256", "host date request-line", sha)
//将请求参数使用base64编码
authorization:= base64.StdEncoding.EncodeToString([]byte(authUrl))
v := url.Values{}
v.Add("host", ul.Host)
v.Add("date", date)
v.Add("authorization", authorization)
//将编码后的字符串url encode后添加到url后面
callurl := hosturl + "?" + v.Encode()
return callurl
}
func HmacWithShaTobase64(algorithm, data, key string) string {
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte(data))
encodeData := mac.Sum(nil)
return base64.StdEncoding.EncodeToString(encodeData)
}
java:
package com.iflytek.webgatews.wsclient;
import okhttp3.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* @Author:sjliu7
* 鉴权使用
* @Date:2019/7/31 15:23
*/
public class AuthUtils {
/**
* 生成用于鉴权的URL,websocket 接口
* @param requestUrl
* @param apiKey
* @param apiSecret
* @return final requestUrl
*/
public static String assembleRequestUrl(String requestUrl,String method, String apiKey, String apiSecret) {
URL url = null;
String httpRequestUrl = requestUrl.replace("ws://", "http://").replace("wss://","https://" );
try {
url = new URL(httpRequestUrl);
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());
// date = "Thu, 19 Dec 2024 07:47:57 GMT";
String host = url.getHost();
StringBuilder builder = new StringBuilder("host: ").append(host).append("\n").//
append("date: ").append(date).append("\n").//
append(method).append(" ").
append(url.getPath()).append(" HTTP/1.1");
Charset charset = Charset.forName("UTF-8");
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);
String authorization = String.format("hmac username=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey, "hmac-sha256", "host date request-line", sha);
String authBase = Base64.getEncoder().encodeToString(authorization.getBytes(charset));
return String.format("%s?authorization=%s&host=%s&date=%s", requestUrl, URLEncoder.encode(authBase), URLEncoder.encode(host), URLEncoder.encode(date));
} catch (Exception e) {
throw new RuntimeException("assemble requestUrl error:"+e.getMessage());
}
}
}
javascript:
function assembleRequestUrl(host,path,apiKey,apiSecret) {
var url = "wss://"+host+path
var date = new Date().toGMTString()
var algorithm = 'hmac-sha256'
var headers = 'host date request-line'
var signatureOrigin = `host: ${host}\ndate: ${date}\nGET ${path} HTTP/1.1`
var signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret)
var signature = CryptoJS.enc.Base64.stringify(signatureSha)
var authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`
var authorization = btoa(authorizationOrigin)
url = `${url}?authorization=${authorization}&date=${date}&host=${host}`
return url
}
*python
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 urlencode
import os
import traceback
import json
class AssembleHeaderException(Exception):
def __init__(self, msg):
self.message = msg
class Url:
def __init__(this, host, path, schema):
this.host = host
this.path = path
this.schema = schema
pass
# 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
def parse_url(requset_url):
stidx = requset_url.index("://")
host = requset_url[stidx + 3:]
schema = requset_url[:stidx + 3]
edidx = host.index("/")
if edidx <= 0:
raise AssembleHeaderException("invalid request url:" + requset_url)
path = host[edidx:]
host = host[:edidx]
u = Url(host, path, schema)
return u
# build websocket auth request url
def assemble_ws_auth_url(requset_url, method="GET", api_key="", api_secret=""):
u = parse_url(requset_url)
host = u.host
path = u.path
now = datetime.now()
date = format_date_time(mktime(now.timetuple()))
print(date)
# date = "Thu, 12 Dec 2019 01:57:27 GMT"
signature_origin = "host: {}\ndate: {}\n{} {} HTTP/1.1".format(host, date, method, path)
# 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_origin = "api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"" % (
api_key, "hmac-sha256", "host date request-line", signature_sha)
authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8')
# print(authorization_origin)
values = {
"host": host,
"date": date,
"authorization": authorization
}
return requset_url + "?" + urlencode(values)
公共请求参数
- 请求参数说明:
参数名 | 类型 | 必传 | 描述 |
---|---|---|---|
common | object | 是 | 公共参数,仅在握手成功后首帧请求时上传 |
business | object | 是 | 业务参数,仅在握手成功后首帧请求时上传,详细参数见具体AI能力 |
data | object | 是 | 业务数据流参数,在握手成功后的所有请求中都需要上传,详细参数见具体AI能力 |
- common参数说明:
参数名 | 类型 | 必传 | 描述 |
---|---|---|---|
context_id | string | 否 | 当客户端发生重新连接行为时,传入上一次连接时中返回的context_id。服务端会恢复上次会话,如果开启了角色分离功能,则会恢复上次会话中的角色分离声纹信息。 |
app_id | string | 是 | 在平台申请的app id信息 |
uid | string | 否 | 请求用户服务返回的uid,用户及设备级别个性化功能依赖此参数,最长32个字节 |
device_id | string | 否 | 请求方确保唯一的设备标志,设备级别个性化功能依赖此参数,uid没传的情况下建议上传这个参数,如果没传,服务端会依次从device.imei、device.imsi、device.mac、device.other中选取不为空的参数作为设备唯一标志 |
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应用的版本号 |
did | string | 否 | 用户设备唯一标识,用户保证唯一性 |
- 公共请求参数示例
{ "common":{ "app_id":"", "uid":"", "did":"", "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": "",
"data": {},
"sid":""
}