服务端授权

更新时间:

本文档介绍了口语评测、作文批改和AI老师的鉴权所需的接口和实例代码。

重要

安全和维护便捷性考虑强烈建议在服务端集成此步骤

语音评测授权与请求流程

image

  1. 评测设备发起授权申请

    • 评测设备通过向商户服务端发送POST请求来进行评测前的授权申请。

  2. 商户服务端验证并申请授权

    • 商户服务端在接收到评测设备的请求后,首先验证用户的有效性。

    • 验证通过后,商户服务端接着向语音评测授权服务提交授权申请。

  3. 获取授权ID

    • 语音评测授权服务处理完申请后,会返回一个授权ID (warrant_id) 给商户服务端。这个授权ID可以在有效期内多次使用。

  4. 授权ID传递给评测设备

    • 商户服务端收到授权ID后,将其返回给最初的评测设备。

  5. 组装并发送评测请求

    • 评测设备根据评测请求接口的要求,组织好JSON格式的数据包。

    • 使用获得的授权ID等信息,评测设备向评测服务器发起正式的评测请求。

  6. 接收评测结果

    • 评测服务器对接收到的请求进行处理,并以JSON数据格式返回评测结果给评测设备。

  7. 业务呈现

    • 最后,评测设备基于从评测服务器获取到的结果,执行相应的业务逻辑或用户界面更新,完成整个流程。

鉴权接口请求

重要

安全和维护便捷性考虑强烈建议商户在服务端集成此步骤

请求地址

  测试环境 
    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,或检查获取warrantIduserId是否与评测的一致

41035

coretype不支持

使用了没开放权限的coretype

与售前确认coretype是否已开放

41036

客户并发数超限

评测并发量已经达到客户购买的额度

与售前确认购买的并发量额度是否够用

42003

客户端发送请求的顺序出错

未按正常流程调用sdk

查看sdk是否正确使用

430005

appid无效

使用了错误的appid

确认appid是否有效或者appid是否与环境匹配(测试账号需要使用测试环境)

51000

初始化内核出错

传文本参数给内核时,内核返回出错

确认参数传递格式正确,联系技术支持

53000

内核进程崩溃

由于内核代码问题或sdk传参问题导致内核进程崩溃

再确认参数格式正确的情况下,联系技术支持。

70001

SDK版本需要升级

该版本SDK被禁止使用或者有限支持

联系技术支持获取SDK支持