限制对 Google API 的 Android 密钥的使用

Posted

技术标签:

【中文标题】限制对 Google API 的 Android 密钥的使用【英文标题】:Restricting usage for an Android key for a Google API 【发布时间】:2016-03-03 12:37:04 【问题描述】:

我的问题是关于如何在 Google Developers Console 中正确设置包名称和 SHA-1 证书指纹,以限制我的 android API 密钥对我的应用的使用。

如果我没有在“限制使用您的 Android 应用”部分中设置任何内容,我对 Google Translate API 的请求可以正常工作。 API 正常响应状态码 200 和我的预期结果。

但是,当我使用 Developers Console 为我的应用程序指定包名称和 SHA-1 证书指纹时,我始终收到 403 Forbidden 响应,如下所示:

HTTP/1.1 403 Forbidden
Vary: Origin
Vary: X-Origin
Content-Type: application/json; charset=UTF-8
Date: Sun, 29 Nov 2015 21:01:39 GMT
Expires: Sun, 29 Nov 2015 21:01:39 GMT
Cache-Control: private, max-age=0
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Server: GSE
Alternate-Protocol: 443:quic,p=1
Alt-Svc: quic=":443"; ma=604800; v="30,29,28,27,26,25"
Content-Length: 729


 "error": 
  "errors": [
   
    "domain": "usageLimits",
    "reason": "ipRefererBlocked",
    "message": "There is a per-IP or per-Referer restriction configured on your API key and the request does not match these restrictions. Please use the Google Developers Console to update your API key configuration if request from this IP or referer should be allowed.",
    "extendedHelp": "https://console.developers.google.com"
   
  ],
  "code": 403,
  "message": "There is a per-IP or per-Referer restriction configured on your API key and the request does not match these restrictions. Please use the Google Developers Console to update your API key configuration if request from this IP or referer should be allowed."
 

请求如下所示。请注意,请求中没有 referer 标头:

GET https://www.googleapis.com/language/translate/v2?key=XXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXX&source=en&target=es&q=test HTTP/1.1
User-Agent: Dalvik/2.1.0 (Linux; U; Android 5.1.1; Nexus 6 Build/LVY48H)
Host: www.googleapis.com
Connection: Keep-Alive
Accept-Encoding: gzip

我假设错误消息表示包名称或 SHA-1 指纹问题,尽管它的消息是关于“per-IP or per-Referer 限制”。虽然浏览器键允许设置 per-referer 限制,但我使用的是 Android 键,无法设置 per-IP 或 per-Referer 限制。

我确定我已在 Google Developers Console 中正确输入了包名称。我正在从我的 Android 清单文件中 manifest 标记上的 package 属性中读取包名称。

我还确定我在 Google Developers Console 中正确设置了 SHA-1 指纹。我正在使用命令keytool -list -v -keystore /path/to/my/keystore 从我的密钥库中读取这个值。当我使用 keytool -list -printcert -jarfile myAppName.apk 从 APK 文件中读取它时,我得到了相同的值。我正在使用 adb 安装相同的 APK 文件。

这是我在开发者控制台中看到的:

我已经在多台运行原生 Android 的设备上对此进行了测试。无论我是否代理流量,我都会在 wifi 和蜂窝网络上收到错误响应。

当我从开发者控制台中移除限制后,应用程序再次正常运行。

我在这里做错了什么?

注意:Severalsimilarquestionshavebeenaskedbefore,before,butwithno@987653333331@98765333@2.我不想使用浏览器密钥或完全删除限制。我想让使用限制正常工作。

【问题讨论】:

只是一个快速的健全性检查 - 您正在使用发布密钥进行测试,而不是使用调试证书? 是的——这是一个释放密钥。 您使用的是 Rest API 还是 Java 库? 我是直接使用API​​,没有使用Java库。 我刚刚注册您正在使用纯 REST API。为此,您需要使用GoogleAuthUtil,它将为特定用户和包/指纹生成令牌。但是,这需要GET_ACCOUNTS 权限,聪明的用户对此感到厌烦。您也可以使用 AccountManager 的getAuthToken() 方法,但这不仅需要GET_ACCOUNTS 权限,还需要USE_CREDENTIALS。您最好使用 API 密钥并稍微隐藏一下。 【参考方案1】:

您在 Google 开发者控制台上为限制您的 Android 应用程序使用 api 密钥所做的一切都正常。受限制后,此 API 密钥将仅接受来自您的应用的请求,并指定了包名和 SHA-1 证书指纹。

那么 google 是如何知道该请求是从您的 ANDROID 应用程序发送的?您必须在每个请求的标头中添加您的应用程序包名称和 SHA-1(显然)。而且你不需要GoogleAuthUtilGET_ACCOUNTS 权限。

首先,获取您的应用 SHA 签名(您将需要 Guava 库):

/**
 * Gets the SHA1 signature, hex encoded for inclusion with Google Cloud Platform API requests
 *
 * @param packageName Identifies the APK whose signature should be extracted.
 * @return a lowercase, hex-encoded
 */
public static String getSignature(@NonNull PackageManager pm, @NonNull String packageName) 
    try 
        PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
        if (packageInfo == null
                || packageInfo.signatures == null
                || packageInfo.signatures.length == 0
                || packageInfo.signatures[0] == null) 
            return null;
        
        return signatureDigest(packageInfo.signatures[0]);
     catch (PackageManager.NameNotFoundException e) 
        return null;
    


private static String signatureDigest(Signature sig) 
    byte[] signature = sig.toByteArray();
    try 
        MessageDigest md = MessageDigest.getInstance("SHA1");
        byte[] digest = md.digest(signature);
        return BaseEncoding.base16().lowerCase().encode(digest);
     catch (NoSuchAlgorithmException e) 
        return null;
    

然后,将包名和SHA证书签名添加到请求头中:

java.net.URL url = new URL(REQUEST_URL);
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
try 
    connection.setDoInput(true);
    connection.setDoOutput(true);

    connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
    connection.setRequestProperty("Accept", "application/json");

    // add package name to request header
    String packageName = mActivity.getPackageName();
    connection.setRequestProperty("X-Android-Package", packageName);
    // add SHA certificate to request header
    String sig = getSignature(mActivity.getPackageManager(), packageName);
    connection.setRequestProperty("X-Android-Cert", sig);
    connection.setRequestMethod("POST");

    // ADD YOUR REQUEST BODY HERE
    // ....................
 catch (Exception e) 
    e.printStackTrace();
 finally 
    connection.disconnect();

另外,如果您使用的是 Google Vision API,您可以使用 VisionRequestInitializer 构建您的请求:

try 
    HttpTransport httpTransport = AndroidHttp.newCompatibleTransport();
    JsonFactory jsonFactory = GsonFactory.getDefaultInstance();

    VisionRequestInitializer requestInitializer =
    new VisionRequestInitializer(CLOUD_VISION_API_KEY) 
    /**
         * We override this so we can inject important identifying fields into the HTTP
         * headers. This enables use of a restricted cloud platform API key.
         */
        @Override
        protected void initializeVisionRequest(VisionRequest<?> visionRequest)
            throws IOException 
            super.initializeVisionRequest(visionRequest);

            String packageName = mActivity.getPackageName();
            visionRequest.getRequestHeaders().set("X-Android-Package", packageName);

            String sig = getSignature(mActivity.getPackageManager(), packageName);
            visionRequest.getRequestHeaders().set("X-Android-Cert", sig);
        
    ;

    Vision.Builder builder = new Vision.Builder(httpTransport, jsonFactory, null);
    builder.setVisionRequestInitializer(requestInitializer);

    Vision vision = builder.build();

    BatchAnnotateImagesRequest batchAnnotateImagesRequest =
    new BatchAnnotateImagesRequest();
    batchAnnotateImagesRequest.setRequests(new ArrayList<AnnotateImageRequest>() 
    AnnotateImageRequest annotateImageRequest = new AnnotateImageRequest();

    // Add the image
    Image base64EncodedImage = new Image();
    // Convert the bitmap to a JPEG
    // Just in case it's a format that Android understands but Cloud Vision
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    requestImage.compress(Bitmap.CompressFormat.JPEG, IMAGE_JPEG_QUALITY, byteArrayOutputStream);
    byte[] imageBytes = byteArrayOutputStream.toByteArray();

    // Base64 encode the JPEG
    base64EncodedImage.encodeContent(imageBytes);
    annotateImageRequest.setImage(base64EncodedImage);

    // add the features we want
    annotateImageRequest.setFeatures(new ArrayList<Feature>() 
    Feature labelDetection = new Feature();
    labelDetection.setType(TYPE_TEXT_DETECTION);
    add(labelDetection);
    );

    // Add the list of one thing to the request
    add(annotateImageRequest);
    );

    Vision.Images.Annotate annotateRequest =
    vision.images().annotate(batchAnnotateImagesRequest);
    // Due to a bug: requests to Vision API containing large images fail when GZipped.
    annotateRequest.setDisableGZipContent(true);
    Log.d("TAG_SERVER", "created Cloud Vision request object, sending request");

    BatchAnnotateImagesResponse response = annotateRequest.execute();
        return convertResponseToString(response);
     catch (GoogleJsonResponseException e) 
        Log.d("TAG_SERVER", "failed to make API request because " + e.getContent());
     catch (IOException e) 
        Log.d("TAG_SERVER", "failed to make API request because of other IOException " +
        e.getMessage());

将以下依赖项添加到您的 gradle:

compile 'com.google.apis:google-api-services-vision:v1-rev2-1.21.0'
compile 'com.google.api-client:google-api-client-android:1.20.0' exclude module: 'httpclient'
compile 'com.google.http-client:google-http-client-gson:1.20.0' exclude module: 'httpclient'

希望有帮助:)

【讨论】:

我尝试将它与 API 密钥受限的 Cloud Endpoints(v2) 一起使用。不工作。 “来自此 Android 客户端应用程序 的请求被阻止。” 嗯,这不会真正增加任何安全性,对吧?它只是添加标题。这与 API 密钥一样安全。它必须以某种方式使用只有正确签名的应用才能获得的签名。 我认为没有完全安全的方法,你应该从代码中隐藏你的api密钥,并使用SSL/TLS作为安全请求头,所以我建议你阅读2篇文章:androidauthority.com/how-to-hide-your-api-key-in-android-600583和@ 987654324@ 我可以对典型的 GET 请求 https://maps.googleapis.com/maps/api/js?key= 做同样的事情吗?【参考方案2】:

使用 Google REST-only API(例如翻译)时,您需要使用GoogleAuthUtil,它会为特定用户和包/指纹生成令牌。但是,这需要GET_ACCOUNTS 权限,聪明的用户对此很警惕。

您也可以使用AccountManagergetAuthToken() 方法,但这不仅需要GET_ACCOUNTS 权限,还需要USE_CREDENTIALS

最好使用 API 密钥并稍微隐藏一下。

【讨论】:

Google Books API 也是 REST-only API 吗?我在 Google Books API 上看到过类似的问题。如果我不得不请求用户获得 GET_ACCOUNTS 权限(更不用说 USE_CREDENTIALS)只是为了显示有关一本书的一些基本信息,那将是一种耻辱。 来自Android documentation: "注意:从Android 6.0 (API level 23)开始,如果应用共享管理账户的验证者签名,则不需要GET_ACCOUNTS权限读取有关该帐户的信息。在 Android 5.1 及更低版本上,所有应用都需要GET_ACCOUNTS 权限才能读取有关任何帐户的信息。" 这看起来应该可以工作,因为我收到了 OK 令牌,但是当我使用 API 密钥以及 Authorization 标头中的 OAuth 令牌发出翻译请求时,我仍然看到 ipRefererBlocked 错误。还在调查中…… 最近,API 返回的消息已更改为The Android package name and signing-certificate fingerprint, null and null, do not match the app restrictions configured on your API key。我正在使用 Google Play 服务 9.2.0。这似乎是朝着正确方向迈出的一步,但它仍然不起作用。【参考方案3】:

包限制和网址签名

当我在努力限制对反向地理编码和静态地图 API 的访问时遇到这篇文章时,我也想分享我的发现。

请注意,并非所有谷歌服务都允许相同的限制。

我们使用 url 签名和 android/ios 包限制。 Link to the Google documentation

获取apk指纹

有多种方法可以从安卓 apk 中获取指纹。

使用密钥库

keytool -list -v keystore mystore.keystore

使用 apk

extract *.apk
navigate to folder META-INF
keytool.exe" -printcert -file *.RSA

C# 示例代码 (Xamarin) 开始使用

在我的生产代码中,我有一个 Headerinfo 的基类,并为 Geoprovider 类提供了一个实例。 使用这种方法,google 服务的代码在 windows、android 和 ios => nuget 包之间 100% 共享。

Android 标头

httpWebRequest.Headers["x-android-package"] = "packageName";
httpWebRequest.Headers["x-android-package"] = "signature";

IOS 标头

httpWebRequest.Headers["x-ios-bundle-identifier"] = "bundleIdentifier";

获取静态地图的示例代码

public byte[] GenerateMap(double latitude, double longitude, int zoom, string size, string mapType)

    string lat = latitude.ToString(CultureInfo.InvariantCulture);
    string lng = longitude.ToString(CultureInfo.InvariantCulture);
    string url = $"https://maps.googleapis.com/maps/api/staticmap?center=lat,lng&zoom=zoom&size=size&maptype=mapType&markers=lat,lng&key=_apiKey";

    // get the secret from your firebase console don't create always an new instance in productive code
    string signedUrl = new GoogleUrlSigner("mysecret").Sign(url);

    HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(signedUrl);

    //Add your headers httpWebRequest.Headers...

    // get the response for the request
    HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse();

    // do whatever you want to do with the response

google 提供的 url 签名示例代码

https://developers.google.com/maps/documentation/geocoding/get-api-key

internal class GoogleUrlSigner

    private readonly string _secret;

    public GoogleUrlSigner(string secret)
    
        _secret = secret;
    

    internal string Sign(string url)
    
        ASCIIEncoding encoding = new ASCIIEncoding();

        // converting key to bytes will throw an exception, need to replace '-' and '_' characters first.
        string usablePrivateKey = _secret.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;
    

【讨论】:

感谢您也提到要在 iOS 应用程序中处理的案例。【参考方案4】:

直接从您的代码而不是通过 Google 提供的中间 SDK 访问 API 意味着没有可用的机制来安全地获取您应用的证书指纹并将该指纹传递给 API。另一方面,当您使用一个提供的 Android SDK 而不是直接访问 API 时(例如,当您使用 Android Google Maps SDK 发送请求时)SDK 可以处理获取您应用的证书指纹,以便应用限制将按预期工作。

Google 开发者控制台在这方面具有误导性,因为对于它的某些 API,它允许开发者根据 Android 应用证书指纹设置密钥限制,但没有提供适用于 Android 的 SDK,它能够在运行时检查该指纹。那么,开发人员剩下的就是更糟糕、更不安全的选择,即在他们的请求旁边发送 X-Android-Cert 和 X-Android-Package 标头,如此处其他答案中所述。

因此,对于尚未发布用于处理应用证书指纹检查的 Android SDK 的 API,事实证明,没有隐藏的简单方法可以获取诸如 Google Play 服务之类的东西来处理获取应用证书指纹以便正确使用应用程序密钥限制 - 只是没有办法做到这一点。

【讨论】:

强调更糟糕、更不安全的选择【参考方案5】:

如果您使用 appBundle 而不是普通的 apk 文件,您还需要从 play.google.com/console/ 获取 SHA-1:

然后将它与你的包一起添加到console.developers.google.com/apis/credentials

希望它能为某人节省神经......

【讨论】:

以上是关于限制对 Google API 的 Android 密钥的使用的主要内容,如果未能解决你的问题,请参考以下文章

限制Google API的Android密钥使用

Android Google Maps Direction Api - Api 密钥限制不起作用

在Android中使用Google API密钥,但有限制

如何使用 google maps api for android 将地图限制在一个国家/地区

用于 android 的 Google MAPs API 限制 2500 个请求/天是每个客户端设备还是每个应用程序密钥?

Google MAP API,对 MarkerClusterer 的限制