签名机制

为保证API的安全调用,在调用API时阿里云会对每个API请求通过签名(Signature)进行身份验证。无论使用HTTP还是HTTPS协议提交请求,都需要在请求中包含签名信息。此文档描述了如何计算签名,提供了Java、Python、Go的代码示例。

概述

RPC API要按以下格式在API请求的Query中增加签名(Signature)。

https://Endpoint/?SignatureVersion=1.0&SignatureMethod=HMAC-SHA1&SignatureNonce=3ee8c1b8-83d3-44af-a94f-4e0ad82fd6cf&Signature=CT9X0VtwR86fNWSnsc6v8YGOjuE%3D&

其中

  • SignatureMethod:签名方式,目前支持HMAC-SHA1。

  • SignatureVersion:签名算法版本,目前版本是 1.0。

  • SignatureNonce:唯一随机数,用于防止网络重放攻击。用户在不同请求间要使用不同的随机数值,建议使用通用唯一识别码(Universally Unique Identifier, UUID)。

  • Signature:使用AccessKey Secret对请求进行对称加密后生成的签名。

签名算法遵循RFC2104 HMAC-SHA1规范,使用AccessSecret对编码、排序后的整个请求串计算HMAC值作为签名。签名的元素是请求自身的一些参数,由于每个API请求内容不同,所以签名的结果也不尽相同。可参考本文的操作步骤,计算签名值。(详细签名规则请参考签名机制说明

String signature = Base64(HMAC_SHA1(AccessSecret + "&", UTF_8_Encoding_Of(stringToSign)))

步骤一:构造待签名字符串

  1. 使用请求参数构造规范化的请求字符串(CanonicalizedQueryString)。

    1. 按照参数名称的字典顺序对请求中所有的请求参数(包括公共请求参数和接口的自定义参数,但不包括公共请求参数中的Signature参数)进行排序。

      说明

      这些参数是请求URI中的参数部分,即URI中“?”之后由“&”连接的部分。详见示例部分。

    2. 对排序之后的请求参数的名称和值分别用UTF-8字符集进行URL编码。编码规则请参考下表。

      字符

      编码方式

      A-Z、a-z和0-9以及“-”、“_”、“.”和“~”

      不编码

      其它字符

      编码成 %XY 的格式,其中 XY 是字符对应ASCII码的16进制表示。例如英文的双引号(””)对应的编码为 %22

      扩展的UTF-8字符

      编码成 %XY%ZA…的格式

      英文空格

      编码成 %20,而不是加号(+)。

      注:该编码方式和一般采用的 application/x-www-form-urlencoded MIME格式编码算法(例如Java标准库中的 java.net.URLEncoder的实现)存在区别。编码时可以先用标准库的方式进行编码,然后把编码后的字符串中的加号(+)替换成 %20,星号(*)替换成 %2A,%7E替换回波浪号(~),即可得到上述规则描述的编码字符串。本算法可以用下面的percentEncode方法来实现:

      private static String percentEncode(String value) throws UnsupportedEncodingException {
          return value != null ? URLEncoder.encode(value, "UTF-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~") : null;
      }
    3. 将编码后的参数名称和值用英文等号(=)进行连接。

    4. 将等号连接得到的参数组合按步骤 1 排好的顺序依次使用“&”符号连接,即得到规范化请求字符串。

  2. 将第一步构造的规范化字符串按照下面的规则构造成待签名的字符串。

    String StringToSign = HTTPMethod + "&" +
                          percentEncode(“/”) + "&" +
                          percentEncode(CanonicalizedQueryString)

    其中:

    1. HttpMethod 是提交请求用的HTTP方法,例如GET。注意:如果计算签名时使用GET方法,最后请求时也需要使用GET方法,否则会导致签名错误。

    2. percentEncode("/") 是对字符 “/” 进行编码得到的值,即 %2F。

    3. percentEncode(CanonicalizedQueryString) 是对步骤1中构造的规范化请求字符串(CanonicalizedQueryString)按照步骤1.b描述的编码规则编码后得到的字符串。

步骤二:计算签名值

  1. 根据RFC2104的定义,计算待签名字符串(StringToSign)的HMAC值。

    说明

    计算签名时使用的Key就是您持有的AccessKey Secret并加上一个 “&” 字符(ASCII:38), 使用的哈希算法是SHA1。

  2. 按照Base64编码规则把上面的HMAC值编码成字符串,即得到签名值(Signature)。

  3. 将得到的签名值作为Signature参数添加到请求参数中。

    说明

    得到的签名值在作为最后的请求参数值提交时,需要按照RFC3986的规则进行URL编码。

示例

DescribeRegions API 为例,假设使用的AccessKey Id 为 testid, AccessKey Secret为 testsecret。 签名前的请求URL如下:

http://ecs.aliyuncs.com/?Timestamp=2016-02-23T12:46:24Z&Format=XML&AccessKeyId=testid&Action=DescribeRegions&SignatureMethod=HMAC-SHA1&SignatureNonce=3ee8c1b8-83d3-44af-a94f-4e0ad82fd6cf&Version=2014-05-26&SignatureVersion=1.0

请求参数按照参数名称的字典顺序排序后为:

AccessKeyId=testid&Action=DescribeRegions&Format=XML&SignatureMethod=HMAC-SHA1&SignatureNonce=3ee8c1b8-83d3-44af-a94f-4e0ad82fd6cf&SignatureVersion=1.0&Timestamp=2016-02-23T12:46:24Z&Version=2014-05-26

根据步骤一第1部分描述,对排序之后的请求参数的名称和值分别用UTF-8字符集进行URL编码后为:

AccessKeyId=testid&Action=DescribeRegions&Format=XML&SignatureMethod=HMAC-SHA1&SignatureNonce=3ee8c1b8-83d3-44af-a94f-4e0ad82fd6cf&SignatureVersion=1.0&Timestamp=2016-02-23T12%3A46%3A24Z&Version=2014-05-26

根据步骤一第2部分描述,构造的待签名字符串(stringToSign) 为(使用GET方法):

GET&%2F&AccessKeyId%3Dtestid%26Action%3DDescribeRegions%26Format%3DXML%26SignatureMethod%3DHMAC-SHA1%26SignatureNonce%3D3ee8c1b8-83d3-44af-a94f-4e0ad82fd6cf%26SignatureVersion%3D1.0%26Timestamp%3D2016-02-23T12%253A46%253A24Z%26Version%3D2014-05-26

按照步骤二中的方法得到的签名(Signature) 为:

OLeaidS1JvxuMvnyHOwuJ+uX5qY=

最后按照RFC3986规则编码的签名(Signature)参数加入到URL请求中,得到的URL为:

http://ecs.aliyuncs.com/?Timestamp=2016-02-23T12:46:24Z&Format=XML&AccessKeyId=testid&Action=DescribeRegions&SignatureMethod=HMAC-SHA1&SignatureNonce=3ee8c1b8-83d3-44af-a94f-4e0ad82fd6cf&Version=2014-05-26&SignatureVersion=1.0&Signature=OLeaidS1JvxuMvnyHOwuJ%2BuX5qY%3D

代码示例

您可以参考下方的代码示例构造最终的请求链接(不需要安装第三方库)。

重要

下面的代码仅作为示例,请勿在生产环境中使用。

package demo;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.temporal.ChronoField;
import java.util.Arrays;
import java.util.Base64;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class Demo {

    private static String percentEncode(String value) {
        try {
            return value != null ? URLEncoder.encode(value, "UTF-8")
                    .replace("+", "%20")
                    .replace("*", "%2A")
                    .replace("%7E", "~") : null;
        } catch (UnsupportedEncodingException ignore) {
            return value;
        }
    }

    private static String urlDecode(String value) {
        try {
            return URLDecoder.decode(value, "UTF-8");
        } catch (UnsupportedEncodingException ignore) {
            return value;
        }
    }

    private static String readFromInputStream(InputStream source) {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(source))) {
            return reader.lines().collect(Collectors.joining("\n"));
        } catch (IOException ignore) {
            return "";
        }
    }

    private static String doPost(String url, String fileName) {
        HttpURLConnection urlConnection = null;
        try {
            URL postUrl = new URL(url);
            urlConnection = (HttpURLConnection) postUrl.openConnection();
            urlConnection.setDoOutput(true);
            urlConnection.setDoInput(true);
            urlConnection.setRequestMethod("POST");
            urlConnection.setUseCaches(false);
            urlConnection.setRequestProperty("Content-Type", "application/octet-stream");
            Path filePath = Paths.get(fileName);
            if (!filePath.toFile().isFile()) {
                throw new FileNotFoundException(String.format("file:%s not found", fileName));
            }
            Files.copy(filePath, urlConnection.getOutputStream());
            return readFromInputStream(urlConnection.getInputStream());
        } catch (FileNotFoundException e) {
            return e.getMessage();
        } catch (IOException e) {
            if (urlConnection != null) {
                return readFromInputStream(urlConnection.getErrorStream());
            }
            return e.getMessage();
        }
    }

    private static String doGet(String url) {
        HttpURLConnection urlConnection = null;
        try {
            URL postUrl = new URL(url);
            urlConnection = (HttpURLConnection) postUrl.openConnection();
            urlConnection.setDoOutput(true);
            urlConnection.setDoInput(true);
            urlConnection.setRequestMethod("GET");
            urlConnection.setUseCaches(false);
            return readFromInputStream(urlConnection.getInputStream());
        } catch (IOException e) {
            if (urlConnection != null) {
                return readFromInputStream(urlConnection.getErrorStream());
            }
            return e.getMessage();
        }

    }

    public static String getSignature(String url, String secret, String httpMethod) throws Exception {
        // 解析url中的参数部分
        URL u = new URL(url);
        String query = u.getQuery();
        // 获取范化的请求字符串
        String canonicalString = Arrays.stream(query.split("&"))
                .map(s -> s.split("="))
                // 去掉不合法的空参数(例如: https://example?Url=&AccessKeyId=)
                .filter(arr -> arr != null && arr.length > 1)
                // 根据请求参数的字典顺序排序
                .sorted(Comparator.comparing(arr -> arr[0]))
                // 按照 RFC3986 规则编码参数名称、参数值
                .map(arr -> String.format("%s=%s", percentEncode(arr[0]), percentEncode(urlDecode(arr[1]))))
                // 用"&"拼接起来编码后的参数
                .reduce((s1, s2) -> s1 + "&" + s2)
                .orElse("");
        // 将规范化字符串拼接成待签名的字符串
        String stringToSign = httpMethod + "&" + percentEncode("/") + "&" + percentEncode(canonicalString);
        // 把 AccessKeySecret加上"&"构成 HMAC-SHA1 算法的key
        secret += "&";
        // HMAC-SHA1 编码后的bytes
        SecretKey secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA1");
        Mac mac = Mac.getInstance("HmacSHA1");
        mac.init(secretKey);
        byte[] hashBytes = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
        // 按照 base64 编码规则生成最后的签名字符串
        String signature = Base64.getEncoder().encodeToString(hashBytes);
        return signature;
    }

    /**
     * 获取公共请求参数。公共请求参数详见文档:https://help.aliyun.com/document_detail/145074.html
     *
     * @param accessKeyId 您的AccessKeyId。如何获取AccessKeyId请参考:https://help.aliyun.com/document_detail/116401.htm?spm=a2c4g.11186623.0.0.6e6027b5fXEVz0
     * @return 公共请求参数组成的字典
     */
    private static Map<String, String> getCommonParameters(String accessKeyId) {
        return new HashMap<String, String>() {
            {
                put("Action", "RecognizeGeneral"); // 调用的接口名称,此处以 RecognizeGeneral 为例
                put("Version", "2021-07-07"); // API版本。OCR的固定值:2021-07-07
                put("Format", "JSON"); // 指定接口返回数据的格式,可以选择 JSON 或者 XML
                put("AccessKeyId", accessKeyId); // 您的AccessKeyId
                put("SignatureNonce", UUID.randomUUID().toString()); // 签名唯一随机数,每次调用不可重复
                put("Timestamp", Instant.now().with(ChronoField.NANO_OF_SECOND, 0).toString()); // 需要Java8及以上版本。如果您需要使用更低版本Java,请更换获取时间戳的方法
                put("SignatureMethod", "HMAC-SHA1"); // 签名方式。目前为固定值 HMAC-SHA1
                put("SignatureVersion", "1.0"); // 签名方式。目前为固定值 1.0
            }
        };
    }

    /**
     * 使用传图片URL方法请求接口示例。以 RecognizeGeneral 接口为例。
     */
    private static void getDemo() throws Exception {
        String endpoint = "ocr-api.cn-hangzhou.aliyuncs.com";
        // 如何获取AccessKeyId、AccessKeySecret请参考文档:https://help.aliyun.com/document_detail/116401.htm?spm=a2c4g.11186623.0.0.6e6027b5GvMJ25
        String accessKeyId = "";
        String accessKeySecret = "";
        // 获取公共请求参数
        Map<String, String> parametersMap = getCommonParameters(accessKeyId);
        // 添加业务参数(不同的接口参数有差异,此处以RecognizeGeneral为例,Url参数为图片链接)
        parametersMap.put("Url", "https://example.png");
        // 初始化请求URL
        StringBuilder urlBuilder = new StringBuilder("https://" + endpoint + "/?");
        // 把业务参数拼接到请求链接中
        for (Map.Entry<String, String> entry : parametersMap.entrySet()) {
            // entry.getValue() 可能出现"&"等符号。 需要encode
            urlBuilder.append(String.format("%s=%s", entry.getKey(), URLEncoder.encode(entry.getValue(), "UTF-8")))
                    .append('&');
        }
        // 去掉最后的"&"
        String url = urlBuilder.substring(0, urlBuilder.length() - 1);
        // 获取签名
        String signature = getSignature(url, accessKeySecret, "GET");
        // 按照 RFC3986 规则编码签名,并添加到最终的请求链接上
        url += String.format("&Signature=%s", percentEncode(signature));
        // 通过GET方式请求接口并打印识别结果,以 RecognizeGeneral 为例
        String result = doGet(url);
        System.out.println(result);
    }

    /**
     * 识别本地文件代码示例。以 RecognizeGeneral 接口为例。
     */
    private static void postDemo() throws Exception {
        String endpoint = "ocr-api.cn-hangzhou.aliyuncs.com";
        // 如何获取AccessKeyId、AccessKeySecret请参考文档:https://help.aliyun.com/document_detail/116401.htm?spm=a2c4g.11186623.0.0.6e6027b5GvMJ25
        String accessKeyId = "";
        String accessKeySecret = "";
        // 获取公共请求参数
        Map<String, String> parametersMap = getCommonParameters(accessKeyId);
        // 初始化请求URL
        StringBuilder urlBuilder = new StringBuilder("https://" + endpoint + "/?");
        // 把业务参数拼接到请求链接中
        for (Map.Entry<String, String> entry : parametersMap.entrySet()) {
            // entry.getValue() 可能出现"&"等符号。 需要encode
            urlBuilder.append(String.format("%s=%s", entry.getKey(), URLEncoder.encode(entry.getValue(), "UTF-8")))
                    .append('&');
        }
        // 去掉最后的"&"
        String url = urlBuilder.substring(0, urlBuilder.length() - 1);
        // 获取签名
        String signature = getSignature(url, accessKeySecret, "POST");
        // 按照 RFC3986 规则编码签名,并添加到最终的请求链接上
        url += String.format("&Signature=%s", percentEncode(signature));
        // 本地文件
        String fileName = "/home/example.png";
        // 通过POST请求接口并打印识别结果,以 RecognizeGeneral 为例
        String result = doPost(url, fileName);
        System.out.println(result);
    }

    public static void main(String[] args) throws Exception {
        // getDemo();
        // postDemo();
    }
}
# encoding=utf-8
try:
    from urllib.parse import quote, urlparse, parse_qs
except:
    from urllib import quote
    from urlparse import urlparse, parse_qs
import hmac
import base64
import hashlib
import uuid
from datetime import datetime, tzinfo, timedelta


class UTC(tzinfo):
    """表示UTC时间
    """

    def tzname(self, dt):
        return "UTC"

    def utcoffset(self, dt):
        return timedelta(0)

    def dst(self, dt):
        return timedelta(0)


def percentEncode(s):
    return quote(s.encode('utf-8'), safe='~')


def get_signature(url, secret, http_method):
    # 解析url中的参数部分
    queries = parse_qs(urlparse(url).query)
    # 按照请求参数字典顺序排序
    keys = sorted(queries.keys())
    # 初始化规范化的请求字符串
    canonicalized_query_string = ""
    # 生成 规范化的请求字符串
    for k in keys:
        # 按照 RFC3986 规则编码参数名称
        quoted_param = percentEncode(k)
        # 按照 RFC3986 规则编码参数值
        quoted_value = percentEncode(queries[k][0])
        # 把编码后的参数名称和值用英文等号(=)拼接起来,然后用 "&" 连接
        canonicalized_query_string += '&%s=%s' % (quoted_param, quoted_value)
    # 去掉开头的 "&"
    canonicalized_query_string = canonicalized_query_string[1:]
    # 将规范化字符串拼接成待签名的字符串
    string_to_sign = http_method + '&' + percentEncode('/') + '&' + percentEncode(canonicalized_query_string)
    # 把 AccessKeySecret加上"&"构成 HMAC-SHA1 算法的key
    secret += '&'
    # HMAC-SHA1 编码后的bytes
    hash_bytes = hmac.new(secret.encode('utf-8'), string_to_sign.encode('utf-8'), digestmod=hashlib.sha1).digest()
    # 按照 base64 编码规则生成最后的签名字符串
    signature = base64.b64encode(hash_bytes).decode('utf-8')
    return signature


def get_common_parameters(access_key_id):
    """获取公共请求参数。公共请求参数详见文档:https://help.aliyun.com/document_detail/145074.html
    Args:
        您的AccessKeyId。如何获取AccessKeyId请参考:https://help.aliyun.com/document_detail/116401.htm?spm=a2c4g.11186623.0.0.6e6027b5fXEVz0
    Returns:
        公共请求参数组成的字典
    """
    return {
        "Action": "RecognizeGeneral",    # 调用的接口名称,此处以 RecognizeGeneral 为例
        "Version": "2021-07-07",    # API版本。OCR的固定值:2021-07-07
        "Format": "JSON",    # 指定接口返回数据的格式,可以选择 JSON 或者 XML
        "AccessKeyId": access_key_id,    # 您的AccessKeyId
        "SignatureNonce": uuid.uuid4(),    # 签名唯一随机数
        "Timestamp": datetime.utcnow().replace(tzinfo=UTC()).strftime('%Y-%m-%dT%H:%M:%SZ'),    # 请求的时间戳。按照ISO8601标准表示,并需要使用UTC时间,格式为yyyy-MM-ddTHH:mm:ssZ
        "SignatureMethod": "HMAC-SHA1",    # 签名方式。目前为固定值 HMAC-SHA1
        "SignatureVersion": "1.0"    # 签名方式。目前为固定值 1.0
    }


def get_request_url():
    """获取完整的请求URL。
    """
    endpoint = "ocr-api.cn-hangzhou.aliyuncs.com"
    # 如何获取AccessKeyId、AccessKeySecret请参考文档:https://help.aliyun.com/document_detail/116401.htm?spm=a2c4g.11186623.0.0.6e6027b5GvMJ25
    access_key_id = ""        # 您的AccessKeyId
    access_key_secret = ""    # 您的AccessKeySecret
    # 获取公共请求参数
    parameters = get_common_parameters(access_key_id)
    # 添加业务参数(不同的接口参数有差异,此处以RecognizeGeneral为例,Url参数为图片链接)
    parameters['Url'] = "https://example.png"
    # 把业务参数拼接到请求链接中
    url = "https://%s/?%s" % (endpoint, '&'.join('%s=%s' % (k, v) for k, v in parameters.items()))
    # 获取签名(GET方法)
    signature = get_signature(url, access_key_secret, 'GET')
    # 按照 RFC3986 规则编码签名,并添加到最终的请求链接上
    url += "&Signature=" + percentEncode(signature)
    return url


if __name__ == '__main__':
    request_url = get_request_url()
    print(request_url)
package main

import (
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha1"
	"encoding/base64"
	"fmt"
	"net/url"
	"sort"
	"strings"
	"time"
)

// uuid 生成随机字符串,作为 SignatureNonce
func uuid() string {
	b := make([]byte, 16)
	_, err := rand.Read(b)
	if err != nil {
		panic(err)
	}
	return fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}

func percentEncode(s string) string {
	s = url.QueryEscape(s)
	s = strings.ReplaceAll(s, "+", "%20")
	s = strings.ReplaceAll(s, "*", "%2A")
	s = strings.ReplaceAll(s, "%7E", "~")
	return s
}

// getCommonParameters 获取公共请求参数。公共请求参数详见文档:https://help.aliyun.com/document_detail/145074.html
func getCommonParameters(accessKeyId string) map[string]string {
	return map[string]string{
		"Action":           "RecognizeGeneral",                                  // 调用的接口名称,此处以 RecognizeGeneral 为例
		"Version":          "2021-07-07",                                        // API版本。OCR的固定值:2021-07-07
		"Format":           "JSON",                                              // 指定接口返回数据的格式,可以选择 JSON 或者 XML
		"AccessKeyId":      accessKeyId,                                         // 您的AccessKeyId
		"SignatureNonce":   uuid(),                                              // 签名唯一随机数
		"Timestamp":        time.Now().UTC().Format("2006-01-02T15:04:05.000Z"), //请求的时间戳。按照ISO8601标准表示,并需要使用UTC时间,格式为yyyy-MM-ddTHH:mm:ssZ。示例:2018-01-01T12:00:00Z表示北京时间2018年01月01日20点00分00秒。
		"SignatureMethod":  "HMAC-SHA1",                                         // 签名方式。目前为固定值 HMAC-SHA1
		"SignatureVersion": "1.0",                                               // 签名方式。目前为固定值 1.0
	}
}

// getSignature 获取签名
func getSignature(urlString, secret, httpMethod string) string {
	// 解析url中的参数部分
	u, err := url.Parse(urlString)
	if err != nil {
		panic(err)
	}
	rawQuery := u.RawQuery
	// 把url的请求参数名称记录到 slice 中
	queryMap, err := url.ParseQuery(rawQuery)
	if err != nil {
		panic(err)
	}
	keys := make([]string, 0)
	for k := range queryMap {
		keys = append(keys, k)
	}
	// 按照请求参数字典顺序排序
	sort.Strings(keys)
	// 初始化规范化的请求字符串
	canonicalString := ""
	for i, k := range keys {
		canonicalString += fmt.Sprintf("%s=%s", percentEncode(k), percentEncode(queryMap[k][0]))
		if i < len(keys)-1 {
			canonicalString += "&"
		}
	}
	// 将规范化字符串拼接成待签名的字符串
	stringToSign := httpMethod + "&" + percentEncode("/") + "&" + percentEncode(canonicalString)
	// 把 AccessKeySecret加上"&"构成 HMAC-SHA1 算法的key
	secret += "&"
	// HMAC-SHA1 编码后的bytes
	h := hmac.New(sha1.New, []byte(secret))
	_, err = h.Write([]byte(stringToSign))
	if err != nil {
		panic(err)
	}
	b := h.Sum(nil)
	// 按照 base64 编码规则生成最后的签名字符串
	signature := base64.StdEncoding.EncodeToString(b)
	return signature
}

// getRequestUrl 获取完整的请求URL
func getRequestUrl() string {
	endpoint := "ocr-api.cn-hangzhou.aliyuncs.com"
	// 如何获取AccessKeyId、AccessKeySecret请参考文档:https://help.aliyun.com/document_detail/116401.htm?spm=a2c4g.11186623.0.0.6e6027b5GvMJ25
	accessKeyId := ""     // 您的AccessKeyId
	accessKeySecret := "" // 您的AccessKeySecret
	// 获取公共请求参数
	parameters := getCommonParameters(accessKeyId)
	// 添加业务参数(不同的接口参数有差异,此处以RecognizeGeneral为例,Url参数为图片链接)
	parameters["Url"] = "https://example.png"
	// 把业务参数拼接到请求链接中
	url := "https://" + endpoint + "/?"
	for k, v := range parameters {
		url += fmt.Sprintf("%s=%s&", k, v)
	}
  // 去掉url最后的"&"
  url = url[:len(url)-1]
	// 获取签名
	signature := getSignature(url, accessKeySecret, "GET")
	// 按照 RFC3986 规则编码签名,并添加到最终的请求链接上
	url += "&Signature=" + percentEncode(signature)
	return url
}

func main() {
	url := getRequestUrl()
	fmt.Println(url)
}
const url = require("url");
const crypto = require("crypto");

const percentEncode = (s) => {
  return encodeURIComponent(s)
    .replace(/\+/g, "%20")
    .replace(/\*/g, "%2A")
    .replace(/%7E/g, "~");
};

// 获取公共请求参数。公共请求参数详见文档:https://help.aliyun.com/document_detail/145074.html
const getCommonParameters = (accessKeyId) => {
  return {
    Action: "RecognizeGeneral", // 调用的接口名称,此处以 RecognizeGeneral 为例
    Version: "2021-07-07", // API版本。OCR的固定值:2021-07-07
    Format: "JSON", // 指定接口返回数据的格式,可以选择 JSON 或者 XML
    AccessKeyId: accessKeyId, // 您的AccessKeyId
    SignatureNonce: crypto.randomBytes(16).toString("hex"), // 签名唯一随机数
    Timestamp: new Date().toISOString(), // 请求的时间戳。按照ISO8601标准表示,并需要使用UTC时间,格式为yyyy-MM-ddTHH:mm:ssZ
    SignatureMethod: "HMAC-SHA1", // 签名方式。目前为固定值 HMAC-SHA1
    SignatureVersion: "1.0", // 签名方式。目前为固定值 1.0
  };
};

// 获取签名
const getSignature = (urlString, secret, httpMethod) => {
  // 解析url中的参数部分
  const query = url.parse(urlString, true).query;
  // 按照请求参数字典顺序排序
  keys = Object.keys(query).sort();
  // 初始化规范化的请求字符串
  canonicalString = "";
  for (const k of keys) {
    // 按照 RFC3986 规则编码参数名称
    let encodedParam = percentEncode(k);
    // 按照 RFC3986 规则编码参数值
    let encodedValue = percentEncode(query[k]);
    // 把编码后的参数名称和值用英文等号(=)拼接起来,然后用 "&" 连接
    canonicalString += `&${encodedParam}=${encodedValue}`;
  }
  // 去掉开头的 "&"
  canonicalString = canonicalString.substring(1);
  // 将规范化字符串拼接成待签名的字符串
  let stringToSign =
    httpMethod +
    "&" +
    percentEncode("/") +
    "&" +
    percentEncode(canonicalString);
  // 把 AccessKeySecret加上"&"构成 HMAC-SHA1 算法的key
  secret += "&";
  // HMAC-SHA1 编码后的bytes
  let res = crypto.createHmac("sha1", secret).update(stringToSign).digest();
  // 按照 base64 编码规则生成最后的签名字符串
  let signature = res.toString("base64");
  return signature;
};

// 获取完整的请求URL
const getRequestUrl = () => {
  const endpoint = "ocr-api.cn-hangzhou.aliyuncs.com";
  // 如何获取AccessKeyId、AccessKeySecret请参考文档:https://help.aliyun.com/document_detail/116401.htm?spm=a2c4g.11186623.0.0.6e6027b5GvMJ25
  const accessKeyId = "";        // 您的AccessKeyId
  const accessKeySecret = "";    // 您的AccessKeySecret
  // 获取公共请求参数
  let parameters = getCommonParameters(accessKeyId);
  // 添加业务参数(不同的接口参数有差异,此处以RecognizeGeneral为例,Url参数为图片链接)
  parameters["Url"] = "https://example.png";
  // 把业务参数拼接到请求链接中
  let requestUrl = `https://${endpoint}/?`;
  for (const key in parameters) {
    requestUrl += `${key}=${parameters[key]}&`;
  }
  requestUrl = requestUrl.substring(0, requestUrl.length - 1);
  // 获取签名
  const signature = getSignature(requestUrl, accessKeySecret, "GET");
  // 按照 RFC3986 规则编码签名,并添加到最终的请求链接上
  requestUrl += `&Signature=${percentEncode(signature)}`;
  return requestUrl;
};

const res = getRequestUrl();
console.log(res);