使用数字签名

使用 API 密钥对请求进行数字签名

根据您的使用情况,除了 API 密钥之外,您可能还需要使用数字签名来对请求进行身份验证。请参阅以下内容 文章:

数字签名的运作方式

您可以使用 Google Cloud 控制台中的网址签名密钥来生成数字签名。该密钥本质上是一种私钥,仅在您与 Google 之间共享,并且归您的项目独有。

签名流程使用一种加密算法将网址与您的共享密钥进行组合。我们的服务器会根据生成的唯一签名来进行验证,以确认使用您的 API 密钥生成请求的所有网站都获得了相应授权。

限制未签名的请求

为确保您的 API 密钥仅接受已签名的请求,请按以下步骤操作:

  1. 前往 Cloud 控制台中的 Google Maps Platform“配额”页面
  2. 点击项目下拉菜单,然后选择您为应用或网站创建 API 密钥时使用的同一项目。
  3. 从 API 下拉列表中选择 Street View Static API
  4. 展开未签名的请求部分。
  5. 配额名称表格中,点击您要修改的配额旁边的“修改”按钮。例如每日未签名请求数
  6. 修改配额限制窗格中更新配额限制
  7. 选择保存

对请求进行签名

对请求进行签名包括以下步骤:

第 1 步:获取您的网址签名密钥

如需获取项目的网址签名密钥,请按以下步骤操作:

  1. 前往 Cloud 控制台中的 Google Maps Platform“凭据”页面
  2. 选择项目下拉菜单,然后选择创建 API 密钥时使用的同一项目 适用于 Street View Static API。
  3. 向下滚动到密钥生成器卡片。当前密钥字段包含您当前的网址签名密钥。
  4. 该页面还提供了立即对网址进行签名 widget,可让您自动进行签名 使用您当前的签名密钥发送 Street View Static API 请求。 向下滚动到立即对网址进行签名卡片进行访问。

如需获取新的网址签名密钥,请选择重新生成密钥。旧密钥将在您生成新密钥的 24 小时后失效。包含旧密钥的请求也将在 24 小时后失效。

第 2 步:构建未签名的请求

必须对下表中未列出的字符进行网址编码:

有效网址字符汇总
字符集字符在网址中的用法
字母数字 a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9 文本字符串、在 scheme 中使用 (http)、端口 (8080) 等
非预留字符 - _ . ~ 文本字符串
预留字符 ! * ' ( ) ; : @ & = + $ , / ? % # [ ] 控制字符和/或文本字符串

此要求也适用于预留字符集中的所有字符(如果相应字符是在文本字符串内传递的)。如需了解详情,请参阅特殊字符

构建不带签名的未签名请求网址。如需了解相关说明,请参阅下面的开发者文档:

此外,还请务必在 key 参数中添加 API 密钥。例如:

https://maps.googleapis.com/maps/api/streetview?location=Z%C3%BCrich&size=400x400&key=YOUR_API_KEY

生成签名请求

针对一次性使用场景(例如在网页上托管简单的 Maps Static API 或 Street View Static API 图片,或是进行问题排查),您可以使用提供的立即对网址进行签名 widget 来自动生成数字签名。

而对于动态生成的请求,您需要在服务器端进行签名,此操作需要执行一些额外的中间步骤。

无论采用上述哪种方式,最终都应在您的请求网址末尾附加一个 signature 参数。例如:

https://maps.googleapis.com/maps/api/streetview?location=Z%C3%BCrich&size=400x400&key=YOUR_API_KEY
&signature=BASE64_SIGNATURE
使用“立即对网址进行签名”widget

如需通过 Google Cloud 控制台中的立即对网址进行签名 widget 使用 API 密钥来生成数字签名,请按以下步骤操作:

  1. 按照第 1 步:获取您的网址签名密钥中所述,找到立即对网址进行签名 widget。
  2. 网址字段中,粘贴您在第 2 步:构建未签名的请求中获取的未签名请求网址。
  3. 系统随即会显示您的已签名网址字段,其中包含经过数字签名的网址。请务必制作副本。
在服务器端生成数字签名

立即对网址进行签名 widget 相比,在服务器端生成数字签名时,您需要执行一些额外的操作:

  1. 去除网址的协议 scheme 和主机部分,只留下路径和查询:

  2. /maps/api/streetview?location=Z%C3%BCrich&size=400x400&key=YOUR_API_KEY
    
  3. 显示的网址签名密钥采用改良版网址 Base64 进行编码。

    由于大多数加密库都要求密钥采用原始字节格式,因此您可能需要先将网址签名密钥解码为其最初的原始格式,然后再进行签名。

  4. 使用 HMAC-SHA1 对上述执行了去除操作的请求进行签名。
  5. 由于大多数加密库都会生成采用原始字节格式的签名,因此您需要利用改良版网址 Base64,将生成的二进制签名转换成可在网址内传递的内容。

  6. 将 Base64 编码的签名附加到原始未签名请求网址的 signature 参数中。例如:

    https://maps.googleapis.com/maps/api/streetview?location=Z%C3%BCrich&size=400x400&key=YOUR_API_KEY
    &signature=BASE64_SIGNATURE

如需查看示例,了解如何使用服务器端代码实现网址签名,请参阅下面的网址签名示例代码

网址签名示例代码

以下各部分展示了使用服务器端代码实现网址签名的方法。应始终在服务器端对网址进行签名,以免将您的网址签名密钥暴露给用户。

Python

下例使用标准 Python 库对网址进行签名(下载代码)。

#!/usr/bin/python
# -*- coding: utf-8 -*-
""" Signs a URL using a URL signing secret """

import hashlib
import hmac
import base64
import urllib.parse as urlparse


def sign_url(input_url=None, secret=None):
    """ Sign a request URL with a URL signing secret.
      Usage:
      from urlsigner import sign_url
      signed_url = sign_url(input_url=my_url, secret=SECRET)
      Args:
      input_url - The URL to sign
      secret    - Your URL signing secret
      Returns:
      The signed request URL
  """

    if not input_url or not secret:
        raise Exception("Both input_url and secret are required")

    url = urlparse.urlparse(input_url)

    # We only need to sign the path+query part of the string
    url_to_sign = url.path + "?" + url.query

    # Decode the private key into its binary format
    # We need to decode the URL-encoded private key
    decoded_key = base64.urlsafe_b64decode(secret)

    # Create a signature using the private key and the URL-encoded
    # string using HMAC SHA1. This signature will be binary.
    signature = hmac.new(decoded_key, str.encode(url_to_sign), hashlib.sha1)

    # Encode the binary signature into base64 for use within a URL
    encoded_signature = base64.urlsafe_b64encode(signature.digest())

    original_url = url.scheme + "://" + url.netloc + url.path + "?" + url.query

    # Return signed URL
    return original_url + "&signature=" + encoded_signature.decode()


if __name__ == "__main__":
    input_url = input("URL to Sign: ")
    secret = input("URL signing secret: ")
    print("Signed URL: " + sign_url(input_url, secret))

Java

下例使用从 JDK 1.8 开始提供的 java.util.Base64 类,旧版本可能需要使用 Apache Commons 或类似工具(下载代码)。

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;  // JDK 1.8 only - older versions may need to use Apache Commons or similar.
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class UrlSigner {

  // Note: Generally, you should store your private key someplace safe
  // and read them into your code

  private static String keyString = "YOUR_PRIVATE_KEY";
  
  // The URL shown in these examples is a static URL which should already
  // be URL-encoded. In practice, you will likely have code
  // which assembles your URL from user or web service input
  // and plugs those values into its parameters.
  private static String urlString = "YOUR_URL_TO_SIGN";

  // This variable stores the binary key, which is computed from the string (Base64) key
  private static byte[] key;
  
  public static void main(String[] args) throws IOException,
    InvalidKeyException, NoSuchAlgorithmException, URISyntaxException {
    
    BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
    
    String inputUrl, inputKey = null;

    // For testing purposes, allow user input for the URL.
    // If no input is entered, use the static URL defined above.    
    System.out.println("Enter the URL (must be URL-encoded) to sign: ");
    inputUrl = input.readLine();
    if (inputUrl.equals("")) {
      inputUrl = urlString;
    }
    
    // Convert the string to a URL so we can parse it
    URL url = new URL(inputUrl);
 
    // For testing purposes, allow user input for the private key.
    // If no input is entered, use the static key defined above.   
    System.out.println("Enter the Private key to sign the URL: ");
    inputKey = input.readLine();
    if (inputKey.equals("")) {
      inputKey = keyString;
    }
    
    UrlSigner signer = new UrlSigner(inputKey);
    String request = signer.signRequest(url.getPath(),url.getQuery());
    
    System.out.println("Signed URL :" + url.getProtocol() + "://" + url.getHost() + request);
  }
  
  public UrlSigner(String keyString) throws IOException {
    // Convert the key from 'web safe' base 64 to binary
    keyString = keyString.replace('-', '+');
    keyString = keyString.replace('_', '/');
    System.out.println("Key: " + keyString);
    // Base64 is JDK 1.8 only - older versions may need to use Apache Commons or similar.
    this.key = Base64.getDecoder().decode(keyString);
  }

  public String signRequest(String path, String query) throws NoSuchAlgorithmException,
    InvalidKeyException, UnsupportedEncodingException, URISyntaxException {
    
    // Retrieve the proper URL components to sign
    String resource = path + '?' + query;
    
    // Get an HMAC-SHA1 signing key from the raw key bytes
    SecretKeySpec sha1Key = new SecretKeySpec(key, "HmacSHA1");

    // Get an HMAC-SHA1 Mac instance and initialize it with the HMAC-SHA1 key
    Mac mac = Mac.getInstance("HmacSHA1");
    mac.init(sha1Key);

    // compute the binary signature for the request
    byte[] sigBytes = mac.doFinal(resource.getBytes());

    // base 64 encode the binary signature
    // Base64 is JDK 1.8 only - older versions may need to use Apache Commons or similar.
    String signature = Base64.getEncoder().encodeToString(sigBytes);
    
    // convert the signature to 'web safe' base 64
    signature = signature.replace('+', '-');
    signature = signature.replace('/', '_');
    
    return resource + "&signature=" + signature;
  }
}

Node.js

下例使用原生节点模块对网址进行签名(下载代码)。

'use strict'

const crypto = require('crypto');
const url = require('url');

/**
 * Convert from 'web safe' base64 to true base64.
 *
 * @param  {string} safeEncodedString The code you want to translate
 *                                    from a web safe form.
 * @return {string}
 */
function removeWebSafe(safeEncodedString) {
  return safeEncodedString.replace(/-/g, '+').replace(/_/g, '/');
}

/**
 * Convert from true base64 to 'web safe' base64
 *
 * @param  {string} encodedString The code you want to translate to a
 *                                web safe form.
 * @return {string}
 */
function makeWebSafe(encodedString) {
  return encodedString.replace(/\+/g, '-').replace(/\//g, '_');
}

/**
 * Takes a base64 code and decodes it.
 *
 * @param  {string} code The encoded data.
 * @return {string}
 */
function decodeBase64Hash(code) {
  // "new Buffer(...)" is deprecated. Use Buffer.from if it exists.
  return Buffer.from ? Buffer.from(code, 'base64') : new Buffer(code, 'base64');
}

/**
 * Takes a key and signs the data with it.
 *
 * @param  {string} key  Your unique secret key.
 * @param  {string} data The url to sign.
 * @return {string}
 */
function encodeBase64Hash(key, data) {
  return crypto.createHmac('sha1', key).update(data).digest('base64');
}

/**
 * Sign a URL using a secret key.
 *
 * @param  {string} path   The url you want to sign.
 * @param  {string} secret Your unique secret key.
 * @return {string}
 */
function sign(path, secret) {
  const uri = url.parse(path);
  const safeSecret = decodeBase64Hash(removeWebSafe(secret));
  const hashedSignature = makeWebSafe(encodeBase64Hash(safeSecret, uri.path));
  return url.format(uri) + '&signature=' + hashedSignature;
}

C#

下例使用默认 System.Security.Cryptography 库对网址请求进行签名。请注意,我们需要转换默认 Base64 编码,才能实现网址安全版本(下载代码)。

using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;

namespace SignUrl {

  public struct GoogleSignedUrl {

    public static string Sign(string url, string keyString) {
      ASCIIEncoding encoding = new ASCIIEncoding();

      // converting key to bytes will throw an exception, need to replace '-' and '_' characters first.
      string usablePrivateKey = keyString.Replace("-", "+").Replace("_", "/");
      byte[] privateKeyBytes = Convert.FromBase64String(usablePrivateKey);

      Uri uri = new Uri(url);
      byte[] encodedPathAndQueryBytes = encoding.GetBytes(uri.LocalPath + uri.Query);

      // compute the hash
      HMACSHA1 algorithm = new HMACSHA1(privateKeyBytes);
      byte[] hash = algorithm.ComputeHash(encodedPathAndQueryBytes);

      // convert the bytes to string and make url-safe by replacing '+' and '/' characters
      string signature = Convert.ToBase64String(hash).Replace("+", "-").Replace("/", "_");
            
      // Add the signature to the existing URI.
      return uri.Scheme+"://"+uri.Host+uri.LocalPath + uri.Query +"&signature=" + signature;
    }
  }

  class Program {

    static void Main() {
    
      // Note: Generally, you should store your private key someplace safe
      // and read them into your code

      const string keyString = "YOUR_PRIVATE_KEY";
  
      // The URL shown in these examples is a static URL which should already
      // be URL-encoded. In practice, you will likely have code
      // which assembles your URL from user or web service input
      // and plugs those values into its parameters.
      const  string urlString = "YOUR_URL_TO_SIGN";
      
      string inputUrl = null;
      string inputKey = null;
    
      Console.WriteLine("Enter the URL (must be URL-encoded) to sign: ");
      inputUrl = Console.ReadLine();
      if (inputUrl.Length == 0) {
        inputUrl = urlString;
      }     
    
      Console.WriteLine("Enter the Private key to sign the URL: ");
      inputKey = Console.ReadLine();
      if (inputKey.Length == 0) {
        inputKey = keyString;
      }
      
      Console.WriteLine(GoogleSignedUrl.Sign(inputUrl,inputKey));
    }
  }
}

其他语言的示例

您可以在网址签名项目中查看涵盖更多语言的示例。

问题排查

如果请求包含的签名无效,则 API 会返回 HTTP 403 (Forbidden) 错误。如果使用的签名密钥未与传递的 API 密钥相关联,或者非 ASCII 输入在签名之前未进行网址编码,则最有可能发生此错误。

如需排查问题,请复制请求网址,去除 signature 查询参数,并按照以下说明重新生成有效签名:

如需通过 Google Cloud 控制台中的立即对网址进行签名 widget 使用 API 密钥来生成数字签名,请按以下步骤操作:

  1. 按照第 1 步:获取您的网址签名密钥中所述,找到立即对网址进行签名 widget。
  2. 网址字段中,粘贴您在第 2 步:构建未签名的请求中获取的未签名请求网址。
  3. 系统随即会显示您的已签名网址字段,其中包含经过数字签名的网址。请务必制作副本。