服务端授权
本文档介绍了口语评测、作文批改和AI老师的鉴权所需的接口和实例代码。
安全和维护便捷性考虑强烈建议在服务端集成此步骤
语音评测授权与请求流程

评测设备发起授权申请
评测设备通过向商户服务端发送
POST请求来进行评测前的授权申请。
商户服务端验证并申请授权
商户服务端在接收到评测设备的请求后,首先验证用户的有效性。
验证通过后,商户服务端接着向语音评测授权服务提交授权申请。
获取授权ID
语音评测授权服务处理完申请后,会返回一个授权ID (
warrant_id) 给商户服务端。这个授权ID可以在有效期内多次使用。
授权ID传递给评测设备
商户服务端收到授权ID后,将其返回给最初的评测设备。
组装并发送评测请求
评测设备根据评测请求接口的要求,组织好JSON格式的数据包。
使用获得的授权ID等信息,评测设备向评测服务器发起正式的评测请求。
接收评测结果
评测服务器对接收到的请求进行处理,并以JSON数据格式返回评测结果给评测设备。
业务呈现
最后,评测设备基于从评测服务器获取到的结果,执行相应的业务逻辑或用户界面更新,完成整个流程。
鉴权接口请求
安全和维护便捷性考虑强烈建议商户在服务端集成此步骤
请求地址
测试环境
https://ginger-trial.api.cloud.ssapi.cn/auth/authorize
通用正式环境
https://api.cloud.ssapi.cn/auth/authorize
https://gate-01.api.cloud.ssapi.cn/auth/authorize
https://gate-02.api.cloud.ssapi.cn/auth/authorize
https://gate-03.api.cloud.ssapi.cn/auth/authorize
钉钉云正式环境(非钉钉云请勿使用)
https://dingtalk-ginger.api.cloud.ssapi.cn/auth/authorize
请求方法
通过Form表单POST的方式发起请求
请求参数
参数名称 | 类型 | 是否必须 | 默认值 | 描述 |
appid | string | Y | None | 商户分配的 appid,作文批改独立的授权接口可以传入app_id |
timestamp | string | Y | None | 商户发起授权请求时的时间戳(以秒为单位的10位时间戳) |
user_id | string | Y | string | 商户用户ID |
user_client_ip | ip string | Y | None | 客户端设备公网IP |
request_sign | string | Y | None | 授权接口参数签名 |
warrant_available | integer | N | 7200 | 授权ID有效期,单位:秒 |
签名(request_sign)生成规则
将 appid、timestamp、user_id、user_client_ip、app_secret按照参数名升序排序之后拼接为queryString,然后进行MD5。示例请见下方
appid、app_secret非控制台下单app_id、app_secret在控制台上获取。售前服务人员提供。控制台下单直接在控制台上获取。
app_secret 是应用的密钥信息,务必放在可靠的运行环境中,且在请求授权时不要传递
返回参数
参数名称 | 类型 | 是否必须 | 默认值 | 描述 |
code | integer | Y | 0 | 状态码:0: 成功 |
msg | string | Y | success | 状态描述:success: 成功 |
data.warrant_id | string | Y | string | 平台返回的授权ID |
data.expire_at | timestamp | Y | time()+7200 | 授权ID过期时间,默认2小时 |
错误码
错误代码 | 位置 | 错误描述 |
430001 | 平台 | 商户请求平台时,未传递 任何 参数 |
430002 | 平台 | 商户请求平台时,未传递 timestamp 参数 |
430003 | 平台 | 商户请求平台时,未传递 request_sign 参数 |
430004 | 平台 | 商户请求平台时,未传递 appid 参数 |
430005 | 平台 | 商户请求平台时,appid 不合法(不是我平台授权的商户) |
430006 | 平台 | 商户请求平台时,未传递 user_id 参数 |
430007 | 平台 | 商户请求平台时,未传递 user_client_ip 参数 |
430008 | 平台 | 商户请求平台时,request_sign 未通过验证,签名错误;如果请求的是测试环境,请依据返回的信息对照修改 |
430009 | 平台 | 平台认证服务器cache宕机 |
示例
请求授权
curl
curl -X POST \
https://api.cloud.ssapi.cn/auth/authorize \
-F "appid=your_appid" \
-F "timestamp=$(date +%s)" \
-F "user_id=your_user_id" \
-F "user_client_ip=your_client_ip" \
-F "request_sign=your_request_sign" \
-F "warrant_available=7200"请注意替换上面的参数为实际值,$(date +%s)为获取实时的时间戳。
授权响应
{
"code": 0,
"message": "success",
"data": {
"warrant_id": "5aec3959f4130352bf067fd21f019",
"expire_at": 1525430617,
"timestamp": "1525423417",
"user_data": {
"user_id": "HCWAS"
}
}
}请求授权时的签名逻辑示例
PHP
$data = ['appid' => $app_id, 'timestamp' => $timestamp, 'user_id' => $user_id, 'user_client_ip' => $user_client_ip, 'app_secret' => $app_secret];
ksort($data);
$data_kv = [ ];
foreach ($data as $k => $v) {
array_push($data_kv, $k.'='.$v);
}
$sign_string = implode('&', $data_kv);
$new_sign = md5($sign_string);JAVA
import java.security.MessageDigest;
import java.util.*;
// Java版本的签名生成代码
Map<String, Object> data = new HashMap<>();
data.put("appid", app_id);
data.put("timestamp", timestamp);
data.put("user_id", user_id);
data.put("user_client_ip", user_client_ip);
data.put("app_secret", app_secret);
List<Map.Entry<String, Object>> list = new ArrayList<>(data.entrySet());
Collections.sort(list, Map.Entry.comparingByKey());
StringBuilder signString = new StringBuilder();
for (Map.Entry<String, Object> entry : list) {
signString.append(entry.getKey()).append("=").append(entry.getValue().toString()).append("&");
}
String sign = signString.toString().substring(0, signString.length() - 1); // 移除最后一个多余的"&"
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[ ] digest = md.digest(sign.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : digest) {
hexString.append(String.format("%02x", b));
}
String newSign = hexString.toString(); // MD5后的签名
} catch (Exception e) {
e.printStackTrace();
}Python
from hashlib import md5
def generate_signature(app_id, timestamp, user_id, user_client_ip, app_secret):
"""
根据给定的参数生成签名字符串。
"""
params = {'appid': app_id, 'timestamp': str(timestamp),'user_id': user_id, 'user_client_ip': user_client_ip,'app_secret': app_secret}
sorted_params = dict(sorted(params.items()))
query_string = '&'.join([f"{k}={v}" for k, v in sorted_params.items()])
signature = md5(query_string.encode()).hexdigest()
return signature
# 示例
app_id = 'a111'
timestamp = 1603885321
user_id = 'w9egtDf3PMAOaxZVGSlQUip12no6WCvu'
user_client_ip = '111.111.XXX.XXX'
app_secret = 'wHkC1SMmDLrVO86vcydG2ax4oPYuqiIh'
print(generate_signature(app_id, timestamp, user_id, user_client_ip, app_secret))```完整版本示例
包含从环境变量读取密钥、地址轮询、签名、获取到授权等整套逻辑
GO 语言
package main
import (
"bytes"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"sort"
"time"
)
func main() {
appId := os.Getenv("APP_ID")
appSecret := os.Getenv("APP_SECRET")
timestamp := fmt.Sprintf("%d", time.Now().Unix())
userClientIp := "172.26.1.61" // 替换为实际客户端的公网 IP 地址
userID := "dummyUserId" // 替换为实际用户 ID
signature := generateSignature(appId, timestamp, userID, userClientIp, appSecret)
fmt.Printf("signature=%s\n\n", signature)
urls := [ ]string{
//测试环境
"http://ginger-trial.api.cloud.ssapi.cn:8080/auth/authorize",
//正式环境
//"https://api.cloud.ssapi.cn/auth/authorize",
//"https://gate-01.api.cloud.ssapi.cn/auth/authorize",
//"https://gate-02.api.cloud.ssapi.cn/auth/authorize",
//"https://gate-03.api.cloud.ssapi.cn/auth/authorize",
}
for _, url := range urls {
respBody, err := requestAuthorization(url, appId, timestamp, userID, userClientIp, signature)
if err == nil {
fmt.Println("Authorization Done")
// 构建包含授权ID的 JSON 字符串
var result struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
WarrantId string `json:"warrant_id"`
ExpireAt int64 `json:"expire_at"`
} `json:"data"`
}
// 解析 JSON 数据
if err := json.NewDecoder(bytes.NewBuffer([ ]byte(respBody))).Decode(&result); err == nil {
// 打印解析后的数据
fmt.Printf("状态码: %d\n", result.Code)
fmt.Printf("状态描述: %s\n", result.Msg)
fmt.Printf("授权ID: %s\n", result.Data.WarrantId)
fmt.Printf("授权ID过期时间: %d\n", result.Data.ExpireAt)
} else {
// 处理解析错误
fmt.Printf("解析JSON失败: %v\n", err)
// 输出 resp.Body 内容作为字符串
fmt.Printf("响应体内容:%s;;;\n", respBody)
}
break
}
time.Sleep(time.Second * 2) // 轮询间隔时间
}
}
func generateSignature(appId, timestamp, userId, userClientIp, appSecret string) string {
data := map[string]string{
"appid": appId,
"timestamp": timestamp,
"user_id": userId,
"user_client_ip": userClientIp,
"app_secret": appSecret,
}
var keys [ ]string
for key := range data {
keys = append(keys, key)
}
sort.Strings(keys)
var buf bytes.Buffer
for _, key := range keys {
buf.WriteString(key + "=" + data[key] + "&")
}
buf.Truncate(buf.Len() - 1) // 删除最后一个多余的"&"
hash := md5.New()
hash.Write([ ]byte(buf.String()))
return hex.EncodeToString(hash.Sum(nil))
}
func requestAuthorization(url, appId, timestamp, userID, clientIp, signature string) (string, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
_ = writer.WriteField("appid", appId)
_ = writer.WriteField("timestamp", timestamp)
_ = writer.WriteField("user_id", userID)
_ = writer.WriteField("user_client_ip", clientIp)
_ = writer.WriteField("request_sign", signature)
_ = writer.WriteField("warrant_available", "7200")
err := writer.Close()
if err != nil {
return "", err
}
req, _ := http.NewRequest("POST", url, body)
req.Header.Set("Content-Type", writer.FormDataContentType())
client := &http.Client{}
resp, err := client.Do(req)
if err != nil || resp.StatusCode != http.StatusOK {
return "", err
}
bodyBytes, _ := io.ReadAll(resp.Body)
fmt.Printf("响应体内容:%s;;;\n", string(bodyBytes))
return string(bodyBytes), err
}
Python语言
import os
import time
import requests
from hashlib import md5
# 获取环境变量中的敏感信息
app_id = os.getenv('APP_ID')
app_secret = os.getenv('APP_SECRET')
# 其他必要参数
timestamp = int(time.time())
user_id = 'your_user_id'
user_client_ip = '192.168.0.1'
# 签名生成函数
def generate_signature(app_id, timestamp, user_id, user_client_ip, app_secret):
params = {
'appid': app_id,
'timestamp': str(timestamp),
'user_id': user_id,
'user_client_ip': user_client_ip,
'app_secret': app_secret
}
sorted_params = dict(sorted(params.items()))
query_string = '&'.join(f'{k}={v}' for k, v in sorted_params.items())
return md5(query_string.encode()).hexdigest()
signature = generate_signature(app_id, timestamp, user_id, user_client_ip, app_secret)
# 请求URL列表
urls = [
#
'http://trial.cloud.ssapi.cn:8080/auth/authorize',
# 正式环境
# 'https://api.cloud.ssapi.cn/auth/authorize',
# 'https://gate-01.api.cloud.ssapi.cn/auth/authorize',
# 'https://gate-02.api.cloud.ssapi.cn/auth/authorize',
# 'https://gate-03.api.cloud.ssapi.cn/auth/authorize'
]
# 发送请求并处理响应
for url in urls:
response = requests.post(
url=url,
data={
'appid': app_id,
'timestamp': str(timestamp),
'user_id': user_id,
'user_client_ip': user_client_ip,
'request_sign': signature,
'warrant_available': '7200'
},
headers={'Content-Type': 'application/x-www-form-urlencoded'}
)
if response.status_code == 200:
print("原始结果:" + str(response.content))
result = response.json()
if result['code'] == 0:
print(f"warrant_id: {result['data']['warrant_id']}")
print(f"过期时间: {result['data']['expire_at']}")
break
JAVA语言
请先引入以下Maven依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>package com.alibaba.edu.efec.demo;
import com.alibaba.fastjson.JSONObject;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;
public class TestGetWarrant {
public static void main(String[ ] args) throws IOException {
// 获取环境变量中的敏感信息
String app_id = System.getenv("APP_ID");
String app_secret = System.getenv("APP_SECRET");
// 其他必要参数
long timestamp = System.currentTimeMillis() / 1000L;
String user_id = "your_user_id";
String user_client_ip = "192.168.0.1";
// 签名生成函数
String signature = generateSignature(app_id, timestamp, user_id, user_client_ip, app_secret);
// 请求URL列表
List<String> urls = Arrays.asList(
"http://trial.cloud.ssapi.cn:8080/auth/authorize"
// 正式环境
// "https://api.cloud.ssapi.cn/auth/authorize",
// "https://gate-01.api.cloud.ssapi.cn/auth/authorize",
// "https://gate-02.api.cloud.ssapi.cn/auth/authorize",
// "https://gate-03.api.cloud.ssapi.cn/auth/authorize"
);
// 发送请求并处理响应
for (String urlStr : urls) {
try {
HttpURLConnection connection = (HttpURLConnection) new URL(urlStr).openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
Map<String, Object> params = new HashMap<>();
params.put("appid", app_id);
params.put("timestamp", timestamp);
params.put("user_id", user_id);
params.put("user_client_ip", user_client_ip);
params.put("request_sign", signature);
params.put("warrant_available", "7200");
StringBuilder postData = new StringBuilder();
for (Map.Entry<String, Object> param : params.entrySet()) {
if (postData.length() != 0) postData.append('&');
postData.append(param.getKey());
postData.append('=');
postData.append(URLEncoder.encode(String.valueOf(param.getValue()), StandardCharsets.UTF_8.toString()));
}
byte[ ] postDataBytes = postData.toString().getBytes(StandardCharsets.UTF_8);
connection.getOutputStream().write(postDataBytes);
int status = connection.getResponseCode();
if (status == 200) {
Scanner scanner = new Scanner(connection.getInputStream(), String.valueOf(StandardCharsets.UTF_8));
StringBuilder body = new StringBuilder();
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println(line);
body.append(line);
}
scanner.close();
// 解析JSON响应
JSONObject json = JSONObject.parseObject(body.toString());
if (json.getInteger("code") == 0) {
System.out.println("warrant_id: " + json.getJSONObject("data").getString("warrant_id"));
System.out.println("过期时间: " + json.getJSONObject("data").getString("expire_at"));
break;
}
} else {
throw new RuntimeException("Failed : HTTP error code : " + status);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static String generateSignature(String app_id, long timestamp, String user_id, String user_client_ip, String app_secret) {
Map<String, String> params = new TreeMap<>();
params.put("appid", app_id);
params.put("timestamp", Long.toString(timestamp));
params.put("user_id", user_id);
params.put("user_client_ip", user_client_ip);
params.put("app_secret", app_secret);
StringBuilder queryString = new StringBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
if (queryString.length() > 0) {
queryString.append("&");
}
queryString.append(entry.getKey()).append("=").append(entry.getValue());
}
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
byte[ ] hash = digest.digest(queryString.toString().getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
错误码
errorId | 描述 | 原因 | 给应用层的建议 |
40092 | 传输的音频时长超限 | 服务端对每次请求的录音时长有限制 | 确定录音时长是否在规定范围内 |
41008 | 音频格式不支持 | 传输的音频格式不支持 | 确认音频格式支持的范畴 |
41010 | 音频信息不合法 | 音频信息中的channel不存在或不支持(只支持1) | 音频的channel参数不存在或不支持 |
41016 | sdk信息为空 | connect.param内缺少sdk字段 | 检查connect包是否完整 |
41017 | sdk中缺少字段version | connect.param.sdk内缺少version字段 | 检查connect包是否完整 |
41018 | sdk中缺少字段source | connect.param.sdk内缺少source字段 | 检查connect包是否完整 |
41019 | sdk中缺少字段protocol | connect.param.sdk内缺少protocol字段 | 检查connect包是否完整 |
41030 | 鉴权失败 | warrantId缺失或者warrantId错误 | 检查是否传了warrantId,或检查获取warrantId的userId是否与评测的一致 |
41035 | coretype不支持 | 使用了没开放权限的coretype | 与售前确认coretype是否已开放 |
41036 | 客户并发数超限 | 评测并发量已经达到客户购买的额度 | 与售前确认购买的并发量额度是否够用 |
42003 | 客户端发送请求的顺序出错 | 未按正常流程调用sdk | 查看sdk是否正确使用 |
430005 | appid无效 | 使用了错误的appid | 确认appid是否有效或者appid是否与环境匹配(测试账号需要使用测试环境) |
51000 | 初始化内核出错 | 传文本参数给内核时,内核返回出错 | 确认参数传递格式正确,联系技术支持 |
53000 | 内核进程崩溃 | 由于内核代码问题或sdk传参问题导致内核进程崩溃 | 再确认参数格式正确的情况下,联系技术支持。 |
70001 | SDK版本需要升级 | 该版本SDK被禁止使用或者有限支持 | 联系技术支持获取SDK支持 |