使用数字签名

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

您可能需要使用数字签名以及 API 密钥,以对请求进行身份验证,具体取决于您的使用情况。请参阅以下文章:

数字签名的运行方式

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

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

限制未签名的请求

若要确保您的 API 密钥仅接受已签名的请求,请执行以下操作:

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

对请求进行签名

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

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

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

  1. 在 Cloud Console 中,转到 Google Maps Platform“凭据”页面
  2. 选择项目下拉菜单,然后选择您为 Street View Static API 创建 API 密钥时使用的同一项目。
  3. 向下滚动到“Secret Generator”卡片。当前 Secret 字段包含您当前的网址签名密钥。
  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 文本字符串、架构用法 (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
使用“立即对网址进行签名”微件

如需使用 Google Cloud Console 中的立即对网址进行签名 widget 生成包含 API 密钥的数字签名,请执行以下操作:

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

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

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

  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 Console 中的立即对网址进行签名 widget 生成包含 API 密钥的数字签名,请执行以下操作:

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