Ограничение использования ключа Android для API Google

У меня вопрос о том, как правильно установить имя пакета и отпечаток сертификата SHA-1 в консоли разработчиков Google, чтобы ограничить использование моего ключа API Android для моего приложения.

Когда в разделе "Ограничить использование приложений Android" ничего не настроено, мои запросы к API Google Translate работают правильно. API обычно отвечает кодом состояния 200 и моим ожидаемым результатом.

Но когда я указываю имя пакета и отпечаток сертификата SHA-1 для своего приложения с помощью консоли разработчиков, я последовательно получаю 403 Запрещенных ответа, таких как:

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."
 }
}

Запрос выглядит следующим образом. Обратите внимание, что в запросе нет заголовка реферера:

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, несмотря на его сообщение об "ограничении на IP или на Referer". В то время как ключи браузера позволяют устанавливать ограничение для каждого реферера, я использую ключ Android, и нигде не устанавливается ограничение для каждого IP-адреса или для каждого реферира.

Я уверен, что правильно ввел имя пакета в консоли разработчиков Google. Я читаю название пакета из package атрибут на manifest тег в моем файле манифеста Android.

Я также уверен, что у меня правильно установлен отпечаток пальца SHA-1 в консоли разработчиков Google. Я читаю это значение из моего хранилища ключей с помощью команды keytool -list -v -keystore /path/to/my/keystore, Я получаю то же значение, когда читаю его из файла APK, используя keytool -list -printcert -jarfile myAppName.apk, Я устанавливаю тот же файл APK с помощью ADB.

Вот что я вижу в консоли разработчиков:

скриншот консоли

Я проверял это на нескольких устройствах под управлением Android. Я получаю сообщение об ошибке по Wi-Fi и сотовой сети, независимо от того, проксирую ли я трафик или нет.

Когда я удаляю ограничение из консоли разработчиков, приложение снова работает правильно.

Что я здесь не так делаю?

Примечание: несколько похожих вопросов задавались ранее, но без адекватных ответов. Я не хочу использовать ключ браузера или вообще снять ограничение. Я хочу, чтобы ограничение использования работало правильно.

5 ответов

Все, что вы сделали в Google Developer Console, чтобы ограничить использование вашего API-ключа для приложения Android, в порядке. После ограничения этот ключ API будет принимать запрос только от вашего приложения с указанным именем пакета и отпечатком сертификата SHA-1.

Итак, как Google узнает, что запрос отправлен с вашего Android-приложения? Вы ДОЛЖНЫ добавить имя пакета вашего приложения и SHA-1 в заголовок каждого запроса (очевидно). И тебе не нужно GoogleAuthUtil а также GET_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();
}

Другой способ, если вы используете API Google Vision, вы можете создать свой запрос с помощью 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 REST только для Google, такого как Translate, вам необходимо использовать GoogleAuthUtil, который сгенерирует токен для конкретного пользователя и пакета / отпечатка пальца. Однако это требует GET_ACCOUNTS разрешение, которое умные пользователи опасаются.

Вы также можете использовать AccountManager "s getAuthToken() метод, но это потребует не только GET_ACCOUNTS разрешение, но также USE_CREDENTIALS,

Возможно, вам лучше использовать API-ключ и немного его скрыть.

Нажатие на API непосредственно из вашего кода, а не через промежуточный SDK, предоставленный Google, означает, что не существует механизма для безопасного получения отпечатка сертификата вашего приложения и его передачи API. С другой стороны, когда вы используете один из предоставленных Android SDK вместо прямого обращения к API, например, когда вы отправляете запросы с помощью Android Google Maps SDK, SDK может обрабатывать получение отпечатка сертификата вашего приложения, чтобы приложение ограничение будет работать по назначению.

Консоль разработчиков Google вводит в заблуждение в этом отношении, потому что для некоторых из ее API-интерфейсов она позволяет разработчикам устанавливать ключевые ограничения на основе отпечатка сертификата приложения Android, но затем не предоставляет SDK для Android, который может проверить этот отпечаток во время выполнения. Таким образом, разработчикам остается худший и более небезопасный вариант отправки заголовков X-Android-Cert и X-Android-Package вместе со своими запросами, как описано в другом ответе здесь.

Итак, для API, для которых не был опубликован сопутствующий Android SDK для проверки отпечатка сертификата приложения, оказывается, что нет скрытого простого способа получить что-то вроде, скажем, Google Play Services для обработки получения отпечатка сертификата вашего приложения в нужном порядке. чтобы правильно использовать ограничение ключа приложения - просто нет способа сделать это.

Ограничение пакетов и подпись URL

Когда я наткнулся на этот пост, когда я боролся с ограничением доступа для обратного геокодирования и статического API-интерфейса карты, я также хочу поделиться своими выводами.

Обратите внимание, что не все службы Google допускают одинаковые ограничения.

Мы используем URL-подписи и ограничения пакета Android / IOS. Ссылка на документацию Google

Получить apk отпечаток пальца

Есть несколько способов получить отпечаток пальца с Android 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 на 100% распределяется между окнами, android и пакетом ios => nuget.

Заголовки 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
}

Пример кода для подписи URL предоставлен Google

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;
    }
}

Если вы используете appBundle вместо обычного файла apk, вам также необходимо получить SHA-1 от play.google.com/console/:

а затем добавьте его со своим пакетом в console.developers.google.com/apis/credentials

Надеюсь кому-то это сбережет нервы...

Другие вопросы по тегам