Подтвердить что ты не робот

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

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

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

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

console screenshot

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

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

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

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

4b9b3361

Ответ 1

Попадание в 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 для обработки получения отпечатка сертификата приложения в порядке правильно использовать ограничение ключа приложения - просто нет способа сделать это.

Ответ 2

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

Итак, как Google знает, что запрос отправлен из вашего сайта ANDROID APP? Вы ДОЛЖНЫ добавить имя своего приложения и SHA-1 в заголовке каждого запроса (очевидно). И вам не нужны разрешения GoogleAuthUtil и GET_ACCOUNTS.

FIRST, получите подпись своего приложения 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 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'

Надеюсь, что эта помощь:)

Ответ 3

При использовании API только для Google REST, такого как Translate, вам нужно будет использовать GoogleAuthUtil, который будет генерировать токен для конкретного пользователя и пакета/отпечатка пальца. Тем не менее, для этого требуется разрешение GET_ACCOUNTS, к которому относятся умные пользователи.

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

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

Ответ 4

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

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

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

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

Получить apk fingerprint

Есть несколько способов получить fingerprint с Android APK.

С хранилищем ключей

keytool -list -v keystore mystore.keystore

с помощью apk

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

Пример кода С# (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;
    }
}