HMAC 身份验证 - REST API 参考HMAC authentication - REST API reference

可以使用 HMAC-SHA256 身份验证方案对 HTTP 请求进行身份验证。You can authenticate HTTP requests by using the HMAC-SHA256 authentication scheme. (HMAC 是指基于哈希的消息身份验证代码。)必须通过 TLS 传输这些请求。(HMAC refers to hash-based message authentication code.) These requests must be transmitted over TLS.

必备条件Prerequisites

  • 凭据 - <Access Key ID>Credential - <Access Key ID>
  • 机密 - base64 解码的访问密钥值。Secret - base64 decoded Access Key Value. base64_decode(<Access Key Value>)

凭据(也称为 id)和机密(也称为 value)的值必须从 Azure 应用配置的实例获取。The values for credential (also called id) and secret (also called value) must be obtained from the instance of Azure App Configuration. 可以使用 Azure 门户Azure CLI 执行此操作。You can do this by using the Azure portal or the Azure CLI.

为每个请求提供身份验证所需的所有 HTTP 标头。Provide each request with all HTTP headers required for authentication. 至少需要以下标头:The minimum required are:

请求标头Request header 说明Description
主机Host Internet 主机和端口号。Internet host and port number. 有关详细信息,请参阅 3.2.2 部分。For more information, see section 3.2.2.
日期Date 发出请求的日期和时间。Date and time at which the request was originated. 它与当前的协调世界时(格林威治标准时间)相差不能超过 15 分钟。It can't be more than 15 minutes off from the current Coordinated Universal Time (Greenwich Mean Time). 此值为 HTTP 日期,如 3.3.1 部分所述。The value is an HTTP-date, as described in section 3.3.1.
x-ms-datex-ms-date 与上述 Date 相同。Same as Date above. 当代理无法直接访问 Date 请求头或代理对其进行修改时,可以改用它。You can use it instead when the agent can't directly access the Date request header, or a proxy modifies it. 如果同时提供了 x-ms-dateDate,则 x-ms-date 优先。If x-ms-date and Date are both provided, x-ms-date takes precedence.
x-ms-content-sha256x-ms-content-sha256 请求正文的 base64 编码的 SHA256 哈希。base64 encoded SHA256 hash of the request body. 即使没有正文,也必须提供它。It must be provided even if there is no body. base64_encode(SHA256(body))
授权Authorization HMAC-SHA256 方案所需的身份验证信息。Authentication information required by the HMAC-SHA256 scheme. 本文的后面部分会介绍格式和详细信息。Format and details are explained later in this article.

示例:Example:

Host: {myconfig}.azconfig.io
Date: Fri, 11 May 2018 18:48:36 GMT
x-ms-content-sha256: {SHA256 hash of the request body}
Authorization: HMAC-SHA256 Credential={Access Key ID}&SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature={Signature}

授权标头Authorization header

语法Syntax

AuthorizationHMAC-SHA256 Credential=<value>&SignedHeaders=<value>&Signature=<value>Authorization: HMAC-SHA256 Credential=<value>&SignedHeaders=<value>&Signature=<value>

参数Argument 描述Description
HMAC-SHA256HMAC-SHA256 授权方案。Authorization scheme. (必需)(required)
凭据Credential 用于计算签名的访问密钥的 ID。The ID of the access key used to compute the signature. (必需)(required)
SignedHeadersSignedHeaders 添加到签名的 HTTP 请求头。HTTP request headers added to the signature. (必需)(required)
SignatureSignature String-To-Sign 的 base64 编码的 HMACSHA256。base64 encoded HMACSHA256 of String-To-Sign. (必需)(required)

凭据Credential

用于计算签名的访问密钥的 ID。ID of the access key used to compute the signature.

签名标头Signed headers

为请求签名所需的 HTTP 请求头名称(以分号分隔)。HTTP request header names, separated by semicolons, required to sign the request. 这些 HTTP 标头必须与请求一起正确提供。These HTTP headers must be correctly provided with the request as well. 请不要使用空格。Don't use white spaces.

所需的 HTTP 请求标头Required HTTP request headers

x-ms-date[或 Date];host;x-ms-content-sha256x-ms-date[or Date];host;x-ms-content-sha256

任何其他 HTTP 请求标头也可添加到签名。Any other HTTP request headers can also be added to the signing. 只需将它们追加到 SignedHeaders 参数中即可。Just append them to the SignedHeaders argument.

示例:Example:

x-ms-date;host;x-ms-content-sha256;Content-Type;Acceptx-ms-date;host;x-ms-content-sha256;Content-Type;Accept

签名Signature

String-To-Sign 的 Base64 编码的 HMACSHA256 哈希。Base64 encoded HMACSHA256 hash of the String-To-Sign. 它使用由 Credential 标识的访问密钥。It uses the access key identified by Credential. base64_encode(HMACSHA256(String-To-Sign, Secret))

String-To-SignString-To-Sign

它是请求的规范表示形式:It is a canonical representation of the request:

String-To-Sign=String-To-Sign=

HTTP_METHOD + '\n' + path_and_query + '\n' + signed_headers_valuesHTTP_METHOD + '\n' + path_and_query + '\n' + signed_headers_values

参数Argument 描述Description
HTTP_METHODHTTP_METHOD 与请求一起使用的大写 HTTP 方法名称。Uppercase HTTP method name used with the request. 有关详细信息,请参阅第 9 部分For more information, see section 9.
path_and_querypath_and_query 串联在一起的请求绝对 URI 路径和查询字符串。Concatenation of request absolute URI path and query string. 有关详细信息,请参阅 3.3 部分For more information, see section 3.3.
signed_headers_valuessigned_headers_values SignedHeaders 中列出的所有 HTTP 请求头的值(以分号分隔)。Semicolon-separated values of all HTTP request headers listed in SignedHeaders. 该格式遵循 SignedHeaders 语义。The format follows SignedHeaders semantics.

示例:Example:

string-To-Sign=
            "GET" + '\n' +                                                                                  // VERB
            "/kv?fields=*&api-version=1.0" + '\n' +                                                         // path_and_query
            "Fri, 11 May 2018 18:48:36 GMT;{myconfig}.azconfig.io;{value of ms-content-sha256 header}"      // signed_headers_values

错误Errors

HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256, Bearer

原因: 未提供使用 HMAC-SHA256 方案的 Authorization 请求头。Reason: Authorization request header with HMAC-SHA256 scheme isn't provided.

解决方案: 提供有效的 Authorization HTTP 请求头。Solution: Provide a valid Authorization HTTP request header.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="The access token has expired", Bearer

原因:Datex-ms-date 请求头与当前的协调世界时(格林威治标准时间)相差超过 15 分钟。Reason: Date or x-ms-date request header is more than 15 minutes off from the current Coordinated Universal Time (Greenwich Mean Time).

解决方案: 提供正确的日期和时间。Solution: Provide the correct date and time.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Invalid access token date", Bearer

原因: Datex-ms-date 请求头缺失或无效。Reason: Missing or invalid Date or x-ms-date request header.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="[Credential][SignedHeaders][Signature] is required", Bearer

原因: Authorization 请求头缺少所需的参数。Reason: Missing a required parameter from the Authorization request header.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Invalid Credential", Bearer

原因: 找不到所提供的 [Host]/[访问密钥 ID]。Reason: The provided [Host]/[Access Key ID] isn't found.

解决方案: 检查 Authorization 请求头的 Credential 参数。Solution: Check the Credential parameter of the Authorization request header. 请确保它是有效的访问密钥 ID,并确保 Host 标头指向已注册的帐户。Make sure it's a valid Access Key ID, and make sure the Host header points to the registered account.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Invalid Signature", Bearer

原因:提供的 Signature 与服务器需要的内容不匹配。Reason: The Signature provided doesn't match what the server expects.

解决方案: 请确保 String-To-Sign 正确无误。Solution: Make sure the String-To-Sign is correct. 请确保 Secret 正确无误且其使用方式正确(在使用之前已进行 base64 解码)。Make sure the Secret is correct and properly used (base64 decoded prior to using).

HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="Signed request header 'xxx' is not provided", Bearer

原因: Authorization 标头中缺少 SignedHeaders 参数所需的请求头。Reason: Missing request header required by SignedHeaders parameter in the Authorization header.

解决方案: 提供具有正确值的所需标头。Solution: Provide the required header, with the correct value.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: HMAC-SHA256 error="invalid_token" error_description="XXX is required as a signed header", Bearer

原因:SignedHeaders 中缺少参数。Reason: Missing parameter in SignedHeaders.

解决方案: 查看“签名标头”最低要求。Solution: Check Signed Headers minimum requirements.

代码片段Code snippets

JavaScriptJavaScript

先决条件Crypto-JSPrerequisites: Crypto-JS

function signRequest(host, 
                     method,      // GET, PUT, POST, DELETE
                     url,         // path+query
                     body,        // request body (undefined of none)
                     credential,  // access key id
                     secret)      // access key value (base64 encoded)
{
        var verb = method.toUpperCase();
        var utcNow = new Date().toUTCString();
        var contentHash = CryptoJS.SHA256(body).toString(CryptoJS.enc.Base64);

        //
        // SignedHeaders
        var signedHeaders = "x-ms-date;host;x-ms-content-sha256"; // Semicolon separated header names

        //
        // String-To-Sign
        var stringToSign = 
            verb + '\n' +                              // VERB
            url + '\n' +                               // path_and_query
            utcNow + ';' + host + ';' + contentHash;   // Semicolon separated SignedHeaders values

        //
        // Signature
        var signature = CryptoJS.HmacSHA256(stringToSign, CryptoJS.enc.Base64.parse(secret)).toString(CryptoJS.enc.Base64);

        //
        // Result request headers
        return [
            { name: "x-ms-date", value: utcNow },
            { name: "x-ms-content-sha256", value: contentHash },
            { name: "Authorization", value: "HMAC-SHA256 Credential=" + credential + "&SignedHeaders=" + signedHeaders + "&Signature=" + signature }
        ];
}

C#C#

using (var client = new HttpClient())
{
    var request = new HttpRequestMessage()
    {
        RequestUri = new Uri("https://{config store name}.azconfig.io/kv?api-version=1.0"),
        Method = HttpMethod.Get
    };

    //
    // Sign the request
    request.Sign(<Credential>, <Secret>);

    await client.SendAsync(request);
}

static class HttpRequestMessageExtensions
{
    public static HttpRequestMessage Sign(this HttpRequestMessage request, string credential, byte[] secret)
    {
        string host = request.RequestUri.Authority;
        string verb = request.Method.ToString().ToUpper();
        DateTimeOffset utcNow = DateTimeOffset.UtcNow;
        string contentHash = Convert.ToBase64String(request.Content.ComputeSha256Hash());

        //
        // SignedHeaders
        string signedHeaders = "date;host;x-ms-content-sha256"; // Semicolon separated header names

        //
        // String-To-Sign
        var stringToSign = $"{verb}\n{request.RequestUri.PathAndQuery}\n{utcNow.ToString("r")};{host};{contentHash}";

        //
        // Signature
        string signature;

        using (var hmac = new HMACSHA256(secret))
        {
            signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.ASCII.GetBytes(stringToSign)));
        }

        //
        // Add headers
        request.Headers.Date = utcNow;
        request.Headers.Add("x-ms-content-sha256", contentHash);
        request.Headers.Authorization = new AuthenticationHeaderValue("HMAC-SHA256", $"Credential={credential}&SignedHeaders={signedHeaders}&Signature={signature}");

        return request;
    }
}

static class HttpContentExtensions
{
    public static byte[] ComputeSha256Hash(this HttpContent content)
    {
        using (var stream = new MemoryStream())
        {
            if (content != null)
            {
                content.CopyToAsync(stream).Wait();
                stream.Seek(0, SeekOrigin.Begin);
            }

            using (var alg = SHA256.Create())
            {
                return alg.ComputeHash(stream.ToArray());
            }
        }
    }
}

JavaJava

public CloseableHttpResponse signRequest(HttpUriRequest request, String credential, String secret)
        throws IOException, URISyntaxException {
    Map<String, String> authHeaders = generateHeader(request, credential, secret);
    authHeaders.forEach(request::setHeader);

    return httpClient.execute(request);
}

private static Map<String, String> generateHeader(HttpUriRequest request, String credential, String secret) 
        throws URISyntaxException, IOException {
    String requestTime = GMT_DATE_FORMAT.format(new Date());

    String contentHash = buildContentHash(request);
    // SignedHeaders
    String signedHeaders = "x-ms-date;host;x-ms-content-sha256";

    // Signature
    String methodName = request.getRequestLine().getMethod().toUpperCase();
    URIBuilder uri = new URIBuilder(request.getRequestLine().getUri());
    String scheme = uri.getScheme() + "://";
    String requestPath = uri.toString().substring(scheme.length()).substring(uri.getHost().length());
    String host = new URIBuilder(request.getRequestLine().getUri()).getHost();
    String toSign = String.format("%s\n%s\n%s;%s;%s", methodName, requestPath, requestTime, host, contentHash);

    byte[] decodedKey = Base64.getDecoder().decode(secret);
    String signature = Base64.getEncoder().encodeToString(new HmacUtils(HMAC_SHA_256, decodedKey).hmac(toSign));

    // Compose headers
    Map<String, String> headers = new HashMap<>();
    headers.put("x-ms-date", requestTime);
    headers.put("x-ms-content-sha256", contentHash);

    String authorization = String.format("HMAC-SHA256 Credential=%s, SignedHeaders=%s, Signature=%s",
            credential, signedHeaders, signature);
    headers.put("Authorization", authorization);

    return headers;
}

private static String buildContentHash(HttpUriRequest request) throws IOException {
    String content = "";
    if (request instanceof HttpEntityEnclosingRequest) {
        try {
            StringWriter writer = new StringWriter();
            IOUtils.copy(((HttpEntityEnclosingRequest) request).getEntity().getContent(), writer,
                    StandardCharsets.UTF_8);

            content = writer.toString();
        }
        finally {
            ((HttpEntityEnclosingRequest) request).getEntity().getContent().close();
        }
    }

    byte[] digest = new DigestUtils(SHA_256).digest(content);
    return Base64.getEncoder().encodeToString(digest);
}

GolangGolang

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "io/ioutil"
    "net/http"
    "strings"
    "time"
)

//SignRequest Setup the auth header for accessing Azure App Configuration service
func SignRequest(id string, secret string, req *http.Request) error {
    method := req.Method
    host := req.URL.Host
    pathAndQuery := req.URL.Path
    if req.URL.RawQuery != "" {
        pathAndQuery = pathAndQuery + "?" + req.URL.RawQuery
    }

    content, err := ioutil.ReadAll(req.Body)
    if err != nil {
        return err
    }
    req.Body = ioutil.NopCloser(bytes.NewBuffer(content))

    key, err := base64.StdEncoding.DecodeString(secret)
    if err != nil {
        return err
    }

    timestamp := time.Now().UTC().Format(http.TimeFormat)
    contentHash := getContentHashBase64(content)
    stringToSign := fmt.Sprintf("%s\n%s\n%s;%s;%s", strings.ToUpper(method), pathAndQuery, timestamp, host, contentHash)
    signature := getHmac(stringToSign, key)

    req.Header.Set("x-ms-content-sha256", contentHash)
    req.Header.Set("x-ms-date", timestamp)
    req.Header.Set("Authorization", "HMAC-SHA256 Credential="+id+", SignedHeaders=x-ms-date;host;x-ms-content-sha256, Signature="+signature)

    return nil
}

func getContentHashBase64(content []byte) string {
    hasher := sha256.New()
    hasher.Write(content)
    return base64.StdEncoding.EncodeToString(hasher.Sum(nil))
}

func getHmac(content string, key []byte) string {
    hmac := hmac.New(sha256.New, key)
    hmac.Write([]byte(content))
    return base64.StdEncoding.EncodeToString(hmac.Sum(nil))
}

PythonPython


import base64
import hashlib
import hmac
from datetime import datetime
import six

def sign_request(host,
                method,     # GET, PUT, POST, DELETE
                url,        # Path + Query
                body,       # Request body 
                credential, # Access Key ID
                secret):    # Access Key Value
    verb = method.upper()

    utc_now = str(datetime.utcnow().strftime("%b, %d %Y %H:%M:%S ")) + "GMT"

    if six.PY2:
        content_digest = hashlib.sha256(bytes(body)).digest()
    else:
        content_digest = hashlib.sha256(bytes(body, 'utf-8')).digest()

    content_hash = base64.b64encode(content_digest).decode('utf-8')

    # Signed Headers
    signed_headers = "x-ms-date;host;x-ms-content-sha256"  # Semicolon separated header names

    # String-To-Sign
    string_to_sign = verb + '\n' + \
                    url + '\n' + \
                    utc_now + ';' + host + ';' + content_hash  # Semicolon separated SignedHeaders values

    # Decode secret
    if six.PY2:
        decoded_secret = base64.b64decode(secret)
        digest = hmac.new(decoded_secret, bytes(
            string_to_sign), hashlib.sha256).digest()
    else:
        decoded_secret = base64.b64decode(secret, validate=True)
        digest = hmac.new(decoded_secret, bytes(
            string_to_sign, 'utf-8'), hashlib.sha256).digest()

    # Signature
    signature = base64.b64encode(digest).decode('utf-8')

    # Result request headers
    return {
        "x-ms-date": utc_now,
        "x-ms-content-sha256": content_hash,
        "Authorization": "HMAC-SHA256 Credential=" + credential + "&SignedHeaders=" + signed_headers + "&Signature=" + signature
    }

PowerShellPowerShell

function Sign-Request(
    [string] $hostname,
    [string] $method,      # GET, PUT, POST, DELETE
    [string] $url,         # path+query
    [string] $body,        # request body
    [string] $credential,  # access key id
    [string] $secret       # access key value (base64 encoded)
)
{  
    $verb = $method.ToUpperInvariant()
    $utcNow = (Get-Date).ToUniversalTime().ToString("R", [Globalization.DateTimeFormatInfo]::InvariantInfo)
    $contentHash = Compute-SHA256Hash $body

    $signedHeaders = "x-ms-date;host;x-ms-content-sha256";  # Semicolon separated header names

    $stringToSign = $verb + "`n" +
                    $url + "`n" +
                    $utcNow + ";" + $hostname + ";" + $contentHash  # Semicolon separated signedHeaders values

    $signature = Compute-HMACSHA256Hash $secret $stringToSign

    # Return request headers
    return @{
        "x-ms-date" = $utcNow;
        "x-ms-content-sha256" = $contentHash;
        "Authorization" = "HMAC-SHA256 Credential=" + $credential + "&SignedHeaders=" + $signedHeaders + "&Signature=" + $signature
    }
}

function Compute-SHA256Hash(
    [string] $content
)
{
    $sha256 = [System.Security.Cryptography.SHA256]::Create()
    try {
        return [Convert]::ToBase64String($sha256.ComputeHash([Text.Encoding]::ASCII.GetBytes($content)))
    }
    finally {
        $sha256.Dispose()
    }
}

function Compute-HMACSHA256Hash(
    [string] $secret,      # base64 encoded
    [string] $content
)
{
    $hmac = [System.Security.Cryptography.HMACSHA256]::new([Convert]::FromBase64String($secret))
    try {
        return [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::ASCII.GetBytes($content)))
    }
    finally {
        $hmac.Dispose()
    }
}

# Stop if any error occurs
$ErrorActionPreference = "Stop"

$uri = [System.Uri]::new("https://{myconfig}.azconfig.io/kv?api-version=1.0")
$method = "GET"
$body = $null
$credential = "<Credential>"
$secret = "<Secret>"

$headers = Sign-Request $uri.Authority $method $uri.PathAndQuery $body $credential $secret
Invoke-RestMethod -Uri $uri -Method $method -Headers $headers -Body $body

BashBash

先决条件Prerequisites:

先决条件Prerequisite 命令Command 经过测试的版本Versions Tested
BashBash bashbash 3.5.27、4.4.233.5.27, 4.4.23
coreutilscoreutils trtr 8.288.28
curlcurl curlcurl 7.55.1、7.58.07.55.1, 7.58.0
OpenSSLOpenSSL opensslopenssl 1.1.0g、1.1.1a1.1.0g, 1.1.1a
util-linuxutil-linux hexdumphexdump 2.14.1、2.31.12.14.1, 2.31.1
#!/bin/bash

sign_request () {
    local host="$1"
    local method="$2"      # GET, PUT, POST, DELETE
    local url="$3"         # path+query
    local body="$4"        # request body
    local credential="$5"  # access key id
    local secret="$6"      # access key value (base64 encoded)

    local verb=$(printf "$method" | tr '[:lower:]' '[:upper:]')
    local utc_now="$(date -u '+%a, %d %b %Y %H:%M:%S GMT')"
    local content_hash="$(printf "$body" | openssl sha256 -binary | base64)"

    local signed_headers="x-ms-date;host;x-ms-content-sha256"  # Semicolon separated header names
    local string_to_sign="$verb\n$url\n$utc_now;$host;$content_hash"  # Semicolon separated signed_headers values

    local decoded_secret="$(printf "$secret" | base64 -d | hexdump -v -e '/1 "%02x"')"
    local signature="$(printf "$string_to_sign" | openssl sha256 -mac HMAC -macopt hexkey:"$decoded_secret" -binary | base64)"

    # Output request headers
    printf '%s\n' \
           "x-ms-date: $utc_now" \
           "x-ms-content-sha256: $content_hash" \
           "Authorization: HMAC-SHA256 Credential=$credential&SignedHeaders=$signed_headers&Signature=$signature"
}

host="{config store name}.azconfig.io"
method="GET"
url="/kv?api-version=1.0"
body=""
credential="<Credential>"
secret="<Secret>"

headers=$(sign_request "$host" "$method" "$url" "$body" "$credential" "$secret")

while IFS= read -r line; do
    header_args+=("-H$line")
done <<< "$headers"
curl -X "$method" -d "$body" "${header_args[@]}" "https://$host$url"