web-dev-qa-db-fra.com

Limite d'utilisation d'une clé Android pour une API Google

Ma question concerne la définition correcte du nom du package et de l'empreinte digitale du certificat SHA-1 dans la console des développeurs Google afin de limiter l'utilisation de ma clé API Android à mon application.

Lorsque rien n'est défini dans la section "Limiter l'utilisation à vos applications Android", mes demandes à l'API Google Translate fonctionnent correctement. L'API répond normalement avec le code d'état 200 et le résultat attendu.

Mais lorsque je spécifie un nom de package et une empreinte digitale de certificat SHA-1 pour mon application à l'aide de la console des développeurs, je reçois systématiquement 403 réponses interdites, comme suit:

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

La demande ressemble à la suivante. Notez qu'il n'y a pas d'en-tête de référent dans la demande:

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

Je suppose que le message d'erreur indique un nom de package ou un problème d'empreinte digitale SHA-1, malgré le message "restriction par adresse IP ou par référent". Alors que les clés de navigateur permettent de définir une restriction par référent, j'utilise une clé Android sans nulle part où définir une restriction par IP ou par référent.

Je suis certain d'avoir correctement saisi le nom du package dans la console Google Developers. Je lis le nom du paquet à partir de l'attribut package de la balise manifest de mon fichier manifeste Android.

Je suis également sûr que les empreintes digitales SHA-1 sont correctement définies dans la console des développeurs Google. Je lis cette valeur de mon magasin de clés en utilisant la commande keytool -list -v -keystore /path/to/my/keystore. Je reçois la même valeur lorsque je le lis à partir du fichier APK à l'aide de keytool -list -printcert -jarfile myAppName.apk. J'installe ce même fichier APK en utilisant adb.

Voici ce que je vois dans la console des développeurs:

 console screenshot

J'ai testé cela sur plusieurs appareils fonctionnant sous Android. Je reçois la réponse d'erreur sur le réseau wifi et sur le réseau cellulaire, que je proxy le trafic ou non.

Lorsque je supprime la restriction de la console des développeurs, l'application fonctionne à nouveau correctement.

Qu'est-ce que je fais mal ici?

Note: Plusieurssimilairesquestionsontétéont demandéauparavant , maisavecnonadéquatréponses . Je ne veux pas utiliser une clé de navigateur ou supprimer la restriction complètement. Je souhaite que la restriction d'utilisation fonctionne correctement.

17
rmtheis

Tout ce que vous avez fait sur la console de développement Google pour limiter l'utilisation de votre clé d'API pour l'application Android est correct. Après restriction, cette clé d'API acceptera uniquement les demandes de votre application avec le nom du package et l'empreinte digitale du certificat SHA-1 spécifiées.

Alors, comment google sait-il que cette requête a été envoyée depuis votre application Android? Vous DEVEZ ajouter le nom du package de votre application et SHA-1 dans l'en-tête de chaque demande (évidemment). Et vous n'avez pas besoin des autorisations GoogleAuthUtil et GET_ACCOUNTS.

D'ABORD, obtenez votre signature SHA de l'application (vous aurez besoin de Guava bibliothèque):

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

Ajoutez ensuite le nom du package et la signature de certificat SHA à l'en-tête de requête:

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

Sinon, si vous utilisez l'API Google Vision, vous pouvez créer votre demande avec 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());
}

Ajoutez les dépendances suivantes à votre diplôme:

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'

J'espère que cette aide :)

17
Duy Pham

Si vous utilisez une API Google REST uniquement, telle que la traduction, vous devrez utiliser GoogleAuthUtil , qui générera un jeton pour un utilisateur et un package/empreinte digitale spécifiques. Cependant, cela nécessite l'autorisation GET_ACCOUNTS, ce dont se méfient les utilisateurs intelligents. 

Vous pouvez également utiliser la méthode getAuthToken() de AccountManager 's, mais cela nécessiterait non seulement l'autorisation GET_ACCOUNTS, mais également USE_CREDENTIALS

Vous feriez mieux d'utiliser une clé API et de la masquer un peu.

7
323go

Restriction de package et signature d'URL

Lorsque je suis tombé sur ce message lorsque j'ai eu du mal à limiter l'accès aux codages inversés en géo et aux cartes statiques, je souhaite également partager mes découvertes.

Notez que tous les services Google ne permettent pas les mêmes restrictions.

Nous utilisons la signature d'URL et la restriction de paquet Android/ios. Lien vers la documentation Google

Obtenir l'empreinte digitale d'apk

Il existe de nombreuses manières d’obtenir l’empreinte de l’apk Android.

avec magasin de clés

keytool -list -v keystore mystore.keystore

avec apk

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

C # Exemple de code (Xamarin) pour commencer

Dans mon code productif, j'ai une classe de base pour Headerinfo et une instance pour la classe Geoprovider. Avec cette approche, le code des services Google est partagé à 100% entre Windows, Android et le paquet nuget.

en-têtes Android

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

IOS en-têtes

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

Exemple de code permettant d'extraire une carte statique

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
}

exemple de code de signature d'URL fourni par 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;
    }
}
1
eugstman