디지털 서명 사용하기

API 키로 요청에 디지털 서명

사용량에 따라 요청을 인증하는 데 API 키 외에 디지털 서명이 필요할 수 있습니다. 다음을 참고하세요. 도움말:

디지털 서명 작동 방식

디지털 서명은 Google Cloud 콘솔에서 제공되는 URL 서명 비밀번호를 사용하여 생성됩니다. 이 비밀번호는 기본적으로 사용자와 Google 사이에만 공유되는 비공개 키이며 프로젝트별로 고유합니다.

서명 과정에서는 암호화 알고리즘을 사용하여 URL과 공유 비밀번호를 결합합니다. 결과로 얻은 고유 서명을 사용하면 서버에서 API 키를 사용하여 요청을 생성하는 사이트에 그러한 권한이 있는지 확인할 수 있습니다.

서명되지 않은 요청 제한

API 키가 서명된 요청만 수락하도록 하기 위한 방법은 다음과 같습니다.

  1. Cloud 콘솔에서 Google Maps Platform 할당량 페이지로 이동합니다.
  2. 프로젝트 드롭다운을 클릭하고 애플리케이션 또는 사이트에 대한 API 키를 만들 때 사용한 것과 동일한 프로젝트를 선택합니다.
  3. API 드롭다운에서 Maps Static API를 선택합니다.
  4. 서명되지 않은 요청 섹션을 펼칩니다.
  5. 할당량 이름 표에서 수정할 할당량 옆에 있는 수정 버튼을 클릭합니다. 예를 들면, 하루에 서명되지 않은 요청 수입니다.
  6. 할당량 한도 수정 창에서 할당량 한도를 업데이트합니다.
  7. 저장을 선택합니다.

시작하기 전에

시작하기 전에 만들 수 있습니다. 이 URL을 통해 디지털 서명이 진행됩니다.

요청 서명

요청에 서명하는 단계는 다음과 같습니다.

1단계: URL 서명 비밀번호 가져오기

프로젝트 URL 서명 비밀번호를 가져오는 방법은 다음과 같습니다.

  1. Cloud 콘솔에서 Google Maps Platform 사용자 인증 정보 페이지로 이동합니다.
  2. 프로젝트 드롭다운을 선택하고 API 키를 만들 때 사용한 것과 동일한 프로젝트를 선택합니다. (Maps Static API용)
  3. 비밀번호 생성기 카드까지 아래로 스크롤합니다. 현재 비밀번호 입력란에 현재 URL 서명 비밀번호가 표시되어 있습니다.
  4. 이 페이지에는 지금 URL 서명하기 위젯도 포함되어 있어 Maps Static API 요청을 보낼 수 있습니다. 지금 URL 서명하기 카드까지 아래로 스크롤하여 액세스합니다.

새 URL 서명 비밀번호를 가져오려면 비밀번호 재생성을 선택합니다. 이전의 비밀번호는 새 비밀번호를 생성한 후 24시간이 지나면 만료됩니다. 24시간이 지나면 이전 비밀번호를 포함한 요청은 더 이상 효력이 없게 됩니다.

2단계: 서명되지 않은 요청 구성

아래 표에 나열되지 않은 문자는 URL로 인코딩해야 합니다.

적절한 URL 문자 요약
세트문자URL 사용
영숫자 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) 등
예약되지 않음 - _ . ~ 텍스트 문자열
예약됨 ! * ' ( ) ; : @ & = + $ , / ? % # [ ] 제어 문자 또는 텍스트 문자열

예약됨 세트에 있는 문자가 텍스트 문자열 내에 전달되는 경우에도 마찬가지입니다. 자세한 내용은 특수문자를 참고하세요.

서명되지 않은 요청 URL을 서명 없이 구성합니다. 자세한 내용은 다음 개발자 문서를 참고하세요.

API 키를 key 매개변수에도 포함해야 합니다. 예를 들면 다음과 같습니다.

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 이미지를 호스팅하거나 문제 해결을 목적으로 하는 일회성 사용 사례의 경우 사용 가능한 지금 URL 서명하기 위젯을 사용하여 디지털 서명을 자동으로 생성할 수 있습니다.

동적으로 생성된 요청의 경우 서버 측 서명이 필요한데 이를 위해서는 중간 단계를 더 거쳐야 합니다.

어떤 방법을 사용하든 끝에 signature 매개변수가 추가된 요청 URL이 생성됩니다. 예를 들면 다음과 같습니다.

https://maps.googleapis.com/maps/api/staticmap?center=Z%C3%BCrich&zoom=12&size=400x400&key=YOUR_API_KEY
&signature=BASE64_SIGNATURE
지금 URL 서명하기 위젯 사용

Google Cloud 콘솔의 지금 URL 서명하기 위젯을 사용하여 API 키로 디지털 서명을 생성하려면 다음 단계를 따르세요.

  1. 1단계: URL 서명 비밀번호 가져오기에 설명된 대로 지금 URL 서명하기 위젯을 찾습니다.
  2. URL 입력란에 2단계: 서명되지 않은 요청 구성의 서명되지 않은 요청 URL을 붙여넣습니다.
  3. 서명한 URL 입력란이 나타나고 디지털 서명된 URL이 표시됩니다. 반드시 사본을 만드세요.
서버 측 디지털 서명 생성

지금 URL 서명하기 위젯과 달리 서버 측에서 디지털 서명을 생성할 때는 추가 작업이 필요합니다.

  1. URL에서 프로토콜 스킴과 호스트 부분을 삭제하고 경로와 쿼리만 남겨둡니다.

  2. /maps/api/staticmap?center=Z%C3%BCrich&zoom=12&size=400x400&key=YOUR_API_KEY
    
  3. 표시된 URL 서명 비밀번호는 URL의 수정된 Base64로 인코딩됩니다.

    대부분의 암호화 라이브러리는 키가 원시 바이트 형식이어야 하므로, 서명하기 전에 URL 서명 비밀번호를 기존 원시 형식으로 디코딩해야 할 수 있습니다.

  4. HMAC-SHA1을 사용하여 위의 삭제된 요청에 서명합니다.
  5. 대부분의 암호화 라이브러리는 원시 바이트 형식의 서명을 생성하므로, 결과로 얻은 바이너리 서명을 URL 내에서 전달할 수 있는 형식으로 변환하려면 URL의 수정된 Base64를 사용하여 변환해야 합니다.

  6. Base64로 인코딩된 서명을 signature 매개변수의 서명되지 않은 기존 요청 URL에 추가합니다. 예를 들면 다음과 같습니다.

    https://maps.googleapis.com/maps/api/staticmap?center=Z%C3%BCrich&zoom=12&size=400x400&key=YOUR_API_KEY
    &signature=BASE64_SIGNATURE

서버 측 코드를 사용하여 URL 서명을 구현하는 방법은 아래의 URL 서명 샘플 코드를 참고하세요.

URL 서명 샘플 코드

다음 섹션에서는 서버 측 코드를 사용하여 URL 서명을 구현하는 방법을 보여줍니다. URL 서명 비밀번호가 사용자에게 노출되지 않도록 하려면 항상 서버 측에서 URL에 서명해야 합니다.

Python

아래의 예에서는 표준 Python 라이브러리를 사용하여 URL에 서명합니다. (코드 다운로드)

#!/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 모듈을 사용하여 URL에 서명합니다. (코드 다운로드)

'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 라이브러리를 사용하여 URL 요청에 서명합니다. URL 안전 버전을 구현하도록 기본 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));
    }
  }
}

다른 언어로 구현된 예시

다른 언어가 포함된 예는 url-signing 프로젝트를 참고하세요.

문제 해결

요청에 잘못된 서명이 포함된 경우 API에서 HTTP 403 (Forbidden) 오류를 반환합니다. 이 오류는 사용된 서명 보안이 전달된 API 키에 연결되지 않았거나 서명하기 전에 ASCII가 아닌 입력이 URL로 인코딩되지 않은 경우에 발생할 가능성이 높습니다.

문제를 해결하려면 요청 URL을 복사한 후 signature 쿼리 매개변수를 제거한 다음 아래 안내에 따라 유효한 서명을 다시 생성합니다.

Google Cloud 콘솔의 지금 URL 서명하기 위젯을 사용하여 API 키로 디지털 서명을 생성하려면 다음 단계를 따르세요.

  1. 1단계: URL 서명 비밀번호 가져오기에 설명된 대로 지금 URL 서명하기 위젯을 찾습니다.
  2. URL 입력란에 2단계: 서명되지 않은 요청 구성의 서명되지 않은 요청 URL을 붙여넣습니다.
  3. 서명한 URL 입력란이 나타나고 디지털 서명된 URL이 표시됩니다. 반드시 사본을 만드세요.