使用數位簽章

透過數位方式以 API 金鑰簽署要求

驗證要求時,除了 API 金鑰,您可能還需要提供數位簽章,視用量而定。請參閱下列文章:

數位簽章的運作方式

您必須使用 Google Cloud 控制台提供的「網址簽署密鑰」,才能產生數位簽章。這個密鑰基本上是一種私密金鑰,只限您與 Google 共用,且專屬於您的專案。

簽署程序會使用加密演算法將網址與共用密鑰結合。我們的伺服器會根據產生的專屬簽章進行驗證,確認使用您 API 金鑰產生要求的所有網站都已獲得授權。

限制未簽署的要求

如何確保您的 API 金鑰只接受已簽署的要求:

  1. 在 Cloud 控制台中,前往 Google 地圖平台配額頁面
  2. 按一下專案下拉式選單,選取您建立應用程式或網站的 API 金鑰時,使用的同一個專案。
  3. 從「API」下拉式選單中選取「Maps Static API」
  4. 展開「Unsigned requests」(未簽署要求) 部分。
  5. 在「Quota Name」(配額名稱) 資料表中,找出您要編輯的配額,按一下旁邊的編輯按鈕,例如「Unsigned requests per day」(每日未簽署要求)。
  6. 在「Edit Quota Limit」(編輯配額限制) 窗格中,更新「Quota limit」(配額限制)
  7. 選取「儲存」

事前準備

開始前,請先找出含有靜態地圖的網頁網址。這個網址是用來數位簽署的網址。

簽署要求

簽署要求包含下列步驟:

步驟 1:取得網址簽署密鑰

如何取得專案網址簽署密鑰:

  1. 在 Cloud 控制台中,前往 Google 地圖平台憑證頁面
  2. 選取專案下拉式選單,然後選取您建立 Maps Static API 的 API 金鑰時,使用的同一個專案。
  3. 向下捲動至「Secret Generator」(密鑰產生器) 資訊卡。「Current secret」(目前的密鑰) 欄位含有您目前的網址簽署密鑰。
  4. 該頁面也提供「立即簽署網址」小工具,可讓您使用目前的簽署密鑰,自動簽署 Maps Static API 要求。向下捲動至「Sign a URL now」(立即簽署網址) 資訊卡即可取得。

如要取得新的網址簽署密鑰,請選取「Regenerate Secret」(重新產生密鑰)。新密鑰產生 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/staticmap?center=Z%C3%BCrich&zoom=12&size=400x400&key=YOUR_API_KEY

產生已簽署的要求

如果是一次性的用途 (例如在網頁上代管簡單的 Maps Static API 或 Street View Static API 圖片,或是進行疑難排解),您可以使用我們提供的「立即簽署網址小工具自動產生數位簽章。

如果是動態產生的要求,您必須進行伺服器端簽署,而這需要一些額外的中間步驟。

不論是哪一種方式,最後都需要建立結尾附有 signature 參數的要求網址。舉例來說:

https://maps.googleapis.com/maps/api/staticmap?center=Z%C3%BCrich&zoom=12&size=400x400&key=YOUR_API_KEY
&signature=BASE64_SIGNATURE
使用「立即簽署網址」小工具

如要在 Google Cloud 控制台中透過「立即簽署網址」小工具,使用 API 金鑰產生數位簽章,請按照下列步驟操作:

  1. 按照「步驟 1:取得網址簽署密鑰」所述的方式,找出「立即簽署網址」小工具。
  2. 在「URL」(網址) 欄位中,貼上「步驟 2:建立未簽署的要求」的未簽署要求網址。
  3. 隨後顯示的「Your Signed URL」(已簽署的網址) 欄位就會包含經過數位簽署的網址,請務必複製該網址。
在伺服器端產生數位簽章

相較於「立即簽署網址」小工具,在伺服器端產生數位簽章時,您必須另外完成幾項操作。

  1. 去除網址的通訊協定架構和主機部分,僅保留路徑和查詢:

  2. /maps/api/staticmap?center=Z%C3%BCrich&zoom=12&size=400x400&key=YOUR_API_KEY
    
  3. 畫面上顯示的網址簽署密鑰,已經過網址適用的調整版 Base64 編碼。

    大部分的加密編譯程式庫都要求金鑰採用原始位元組格式,因此在簽署前,您可能需要將網址簽署密鑰解碼為最初的原始格式。

  4. 使用 HMAC-SHA1 簽署上述去除格式的要求。
  5. 大部分的加密編譯程式庫會以原始位元組格式產生簽章,因此請務必使用經調整的 Base64 編碼網址,將產生的二進位簽章轉換成可在網址內傳送的內容。

  6. 將經過 Base64 編碼的簽章附加到 signature 參數中未簽署的原始要求網址。舉例來說:

    https://maps.googleapis.com/maps/api/staticmap?center=Z%C3%BCrich&zoom=12&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

以下範例使用原生 Node 模組簽署網址 (下載程式碼)。

'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 控制台中透過「立即簽署網址」小工具,使用 API 金鑰產生數位簽章,請按照下列步驟操作:

  1. 按照「步驟 1:取得網址簽署密鑰」所述的方式,找出「立即簽署網址」小工具。
  2. 在「URL」(網址) 欄位中,貼上「步驟 2:建立未簽署的要求」的未簽署要求網址。
  3. 隨後顯示的「Your Signed URL」(已簽署的網址) 欄位就會包含經過數位簽署的網址,請務必複製該網址。