U+平台OpenAPI
  1. API调用说明
U+平台OpenAPI
  • API调用说明
    • 如何调用API
    • 发起请求
    • 验证请求
  • 课程服务API
    • 我管理的课程
      GET
    • 根据id获取课程详情
      GET
    • 获取课程下的教学班列表
      GET
    • 获取教学班下的学生
      GET
    • 根据教学班ID获取已发布的作业列表
      GET
    • 根据作业ID获取作业习题列表
      GET
    • 学生作业提交/批阅情况
      GET
    • 学生答题情况
      GET
    • 给学生单个题目打分
      POST
    • 上传学生答案
      POST
    • 给单个学生多个题目打分
      POST
    • 提交学生作业
      POST
  1. API调用说明

验证请求

U+平台的开放接口都可以使用 HTTP Authorization 标头携带签名信息来进行身份认证,格式为——

Authorization: UPIv2 AccessKey:Nonce:Signature

其中 UPIv2 为身份认证服务版本,当前版本 UPIv2 使用 HmacSHA256 算法进行签名。

AccessKey 为密钥中的 Access Key。

Nonce 是一个唯一的随机字符串,每次请求应使用不同的 Nonce 值,最大长度为 32。

Signature 为本次请求的签名。

Authorization 计算方法

Authorization = "UPIv2 " + AccessKey + ":" + RandomNonce + ":" + Signature

RandomNonce = 随机字符串、UUID、时间戳等均可

Signature = base64(HmacSHA256(AccessSecret,
					AccessKey + "\n"
					+ Date + "\n"
					+ RandomNonce + "\n"
					+ Verb + "\n"
					+ CanonicalPathAndParameters + "\n"
					+ Content-Type + "\n"
					+ Content-MD5
			))

Date

生成请求的时间,该时间为 RFC1123 格式,如 Mon, 10 Jul 2023 13:07:29 GMT ,需要同时在请求标头 Date 中携带该时间。

Verb

指接口操作的方法,对于 REST 接口而言即为 HTTP 请求方法,如 GET、POST、PUT、DELETE 等,需大写。

CanonicalPathAndParameters

该字段包含请求 Path、Query 和 Form 中的所有参数,组织形式如下——

Path + "?" + Key1 + "=" + Value1 + "&" + Key2 + "=" + Value2 + ... "&" + KeyN + "=" + ValueN

参数拼接需要以字典顺序排序。

Path、Key、Value 均需要进行百分号编码(URL编码)后再拼接。

编码遵循 RFC3986 规范——

  • 不编码的字符:A~Z a~z 0~9 - _ . ~
  • 其它字符编码为 %XY 格式(其中 XY 为字符的 ASCII 码的十六进制)
  • 扩展 UTF-8 字符编码为 %XY%ZA... 格式
  • 空格编码为 %20 而非 + 号
  • * 号编码为 %2A

拼接过程需遵循——

  • Query 和 Form 参数对需要按 Key 的字典顺序排序后拼接(URL 编码后);
  • 若没有任何 Query 和 Form 参数,则直接使用 Path,不需要添加 ?;
  • 若参数的 Value 为空,也需要在 Key 之后添加等号;
  • 若参数值存在数组时,需将值以半角逗号拼接为字符串,逗号前后没有空格。

Content-Type

请求中 Content-Type 标头的值,可以为空,为空时使用空字符串。

注:当因客户端一些底层逻辑(如微信小程序)导致 Content-Type 异常时,可以增加一个 X-Ca-Signed-Content-Type: application/json 自定义头,使用该值进行签名。

Content-MD5

按照 RFC1864 规范计算请求体的 MD5 摘要,然后经过 Base64 编码后得到的字符串,需要同时在请求标头 Content-MD5 中携带该字符串,可以为空,为空时使用空字符串。

只有存在请求体,且请求体不是 Form 形式时才进行 Content-MD5 的计算。

计算方式参考——

ContentMD5 = Base64(MD5(content.getBytes(UTF-8)))

签名排错

当签名校验失败时,会将服务端的签名字符串(StringToSign)放到响应头 X-Ca-Error-Message 中返回给客户端,用户可以将其和本地的签名字符串进行比较查找问题。

由于 HTTP 标头中无法表示换行,因此 StringToSign 中的换行符将替换为 # ,例如——

Invalid Signature, Server StringToSign: `MDLhiMQPw0wlNHWorLIiyXiGzHylrcMS#Mon, 10 Jul 2023 13:07:29 GMT#4abb2e885aaf4b0e9db446dac23a3819#GET#/app/v1/courses?name=TEST##`

说明服务端的签名字符串为——

MDLhiMQPw0wlNHWorLIiyXiGzHylrcMS                   # AccessKey
Mon, 10 Jul 2023 13:07:29 GMT                      # Date
4abb2e885aaf4b0e9db446dac23a3819                   # Nonce
GET                                                # Verb
/app/v1/courses?name=TEST                          # CanonicalPathAndParameters
                                                   # Content-Type,该请求没有请求体,所以Content-Type使用空串
                                                   # Content-MD5,该请求没有请求体,所以Content-MD5为空串

计算示例

假设——

  • AccessKey 为 UhH3QfuFW0O0JAkmi2IFU5m95VI0Kziv
  • AccessSecret 为 69589UwjICw7k9gjuyIY6IgajTHxEHR5MaYFawS8YlLEwaQpzN2HBYRtx0fyakvI
  • 主机域名为 api.example.com
  • 请求方法为 POST
  • 请求接口为 /api/v1/courses
  • Query参数为
    • region: Prov.11
    • nature: Senior
    • tags: Java, Spring, MySQL
    • feature:<无值>
  • 请求体内容为
    • {"metadata":{"grade":"2023","version":"1.0"},"code":"ABC","author":"Tom","name":"Spring增删改查"}

Java

public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {  
    String accessKey = "UhH3QfuFW0O0JAkmi2IFU5m95VI0Kziv";  
    String accessSecret = "69589UwjICw7k9gjuyIY6IgajTHxEHR5MaYFawS8YlLEwaQpzN2HBYRtx0fyakvI";  
  
    // 请求时间:Date  
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US).withZone(ZoneId.of("GMT"));  
    String date = dtf.format(Instant.now());  
  
    // 随机字符串:Nonce  
    String nonce = UUID.randomUUID().toString().replace("-", "");  
  
    // Verb  
    String method = "POST";  
  
    // CanonicalPathAndParameters  
    String canonicalPath = percentEncode("/api/v1/courses");  
    TreeMap<String, String> queries = new TreeMap<>(String::compareTo);  
    queries.put(percentEncode("region"), percentEncode("Prov.11"));  
    queries.put(percentEncode("nature"), percentEncode("Senior"));  
    queries.put(percentEncode("tags"), percentEncode("Java,Spring,MySQL"));  
    queries.put(percentEncode("feature"), percentEncode(""));
    StringJoiner canonicalParams = new StringJoiner("&");  
    for (Map.Entry<String, String> entry : queries.entrySet()) {  
        canonicalParams.add(entry.getKey() + "=" + entry.getValue());  
    }  
    String canonicalPathAndParameters = canonicalPath + "?" + canonicalParams;  
  
    // Content-Type  
    String contentType = MediaType.APPLICATION_JSON_VALUE;  
  
    // Content-MD5  
    // 构造原始请求体  
    Map<String, Object> course = new HashMap<>();  
    course.put("name", "Spring增删改查");  
    course.put("code", "ABC");  
    course.put("author", "Tom");  
    Map<String, Object> metadata = new HashMap<>();  
    metadata.put("version", "1.0");  
    metadata.put("grade", "2023");  
    course.put("metadata", metadata);  
    String content = new Gson().toJson(course);  
    // 生成请求体 MD5 摘要,并进行 Base64 编码  
    MessageDigest md = MessageDigest.getInstance("MD5");  
    md.update(content.getBytes(StandardCharsets.UTF_8));  
    String contentMD5 = Base64.getEncoder().encodeToString(md.digest());  
  
    // 组装签名字符串  
    String stringToSign = accessKey + "\n"  
                        + date + "\n"  
                        + nonce + "\n"  
                        + method + "\n"  
                        + canonicalPathAndParameters + "\n"  
                        + contentType + "\n"  
                        + contentMD5;  
  
    System.out.printf("### string to sign:\n%s\n\n", stringToSign);  
  
    // 生成签名  
    SecretKey keySpec = new SecretKeySpec(accessSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");  
    Mac mac = Mac.getInstance("HmacSHA256");  
    mac.init(keySpec);  
    String signature = Base64.getEncoder().encodeToString(mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)));  
  
    System.out.printf("### signature:\n%s\n\n", signature);  
  
    // 拼装 Authorization 请求头的值  
    String authorization = "UPIv2 " + accessKey + ":" + nonce + ":" + signature;  
  
    System.out.printf("### authorization:\n%s\n\n", authorization);  
  
}   

/** 辅助方法:用于对请求路径和参数进行 URL 编码 */
public static String percentEncode(String value) {  
    if (value == null) {  
        return null;  
    }  
    /*  
        URLEncoder.encode 并非 RFC3986 规范,编码后需要额外处理——  
            空格会被编码为 + 号,替换为 %20
            ~ 会被编码为 %7E,替换回 ~
            * 不会被编码,需要替换为 %2A
	*/
    return URLEncoder.encode(value, StandardCharsets.UTF_8)  
            .replace("+", "%20")  
            .replace("*", "%2A")  
            .replace("%7E", "~");  
}
修改于 2023-08-17 07:21:46
上一页
发起请求
下一页
我管理的课程
Built with