Google API용 Android 키 사용 제한 (Restricting usage for an Android key for a Google API)


문제 설명

Google API용 Android 키 사용 제한 (Restricting usage for an Android key for a Google API)

내 질문은 내 Android API 키의 사용을 내 앱으로 제한하기 위해 Google 개발자 콘솔에서 패키지 이름과 SHA‑1 인증서 지문을 올바르게 설정하는 방법에 관한 것입니다.

그렇지 않을 때 "Android 앱 사용 제한" 섹션에서 설정한 항목이 없으면 Google 번역 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."
 }
}

요청은 다음과 같습니다. 요청에 참조 헤더가 없습니다.

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

I' m "IP별 또는 참조자별 제한"에 대한 메시지에도 불구하고 오류 메시지가 패키지 이름 또는 SHA‑1 지문 문제를 나타내는 것으로 가정합니다. 브라우저 키가 참조자별 제한 설정을 허용하는 동안 IP별 또는 참조자별 제한을 설정할 곳이 없는 Android 키를 사용하고 있습니다.

패키지를 입력했다고 확신합니다. Google 개발자 콘솔에서 이름을 올바르게 지정하십시오. 내 Android 매니페스트 파일의 manifest 태그에 있는 package 속성에서 패키지 이름을 읽고 있습니다.

또한 Google 개발자 콘솔에서 SHA‑1 지문이 올바르게 설정되었습니다. keytool ‑list ‑v ‑keystore /path/to/my/keystore 명령을 사용하여 키 저장소에서 이 값을 읽고 있습니다. keytool ‑list ‑printcert ‑jarfile myAppName.apk를 사용하여 APK 파일에서 읽을 때 동일한 값을 얻습니다. adb를 사용하여 동일한 APK 파일을 설치하고 있습니다.

다음은 개발자 콘솔에 표시되는 내용입니다.

콘솔 스크린샷

나는 재고 Android를 실행하는 여러 기기에서 이것을 테스트했습니다. 트래픽을 프록시하는지 여부에 관계없이 Wi‑Fi 및 셀 네트워크에서 오류 응답을 받았습니다.

개발자 콘솔에서 제한을 제거하면 앱이 다시 제대로 작동합니다.

여기서 내가 무엇을 잘못하고 있습니까?

참고: , 하지만 포함 아니요 적절함 답변. 나는 하지 않는다 브라우저 키를 사용하거나 제한을 완전히 제거하고 싶습니다. 사용 제한이 제대로 작동하도록 하고 싶습니다.


참조 솔루션

방법 1:

Everything you've done on Google Developer Console to restrict usage of your api key for Android app is OK. After restricted, this API key will only accept request from your app with package name and SHA‑1 certificate fingerprint specified.

So how google know that request's sent FROM YOUR ANDROID APP? You MUST add your app's package name and SHA‑1 in the header of each request (obviously). And you don't need GoogleAuthUtil and GET_ACCOUNTS permission.

FIRST, get your app SHA signature (you will need Guava library):

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

Then, add package name and SHA certificate signature to request header:

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

Other way, if you are using Google Vision API, you can build your request with 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());
}

Add following dependencies to your 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'

Hope this help :)

방법 2:

When using a Google REST‑only API, such as Translate, you'll need to use GoogleAuthUtil, which will generate a token for a specific user and package/fingerprint. However, that requires GET_ACCOUNTS permission, which smart users are wary of.

You could also use the AccountManager's getAuthToken() method, but that would require not only the GET_ACCOUNTS permission, but also USE_CREDENTIALS.

You might be best off using an API key and obscuring it a bit.

방법 3:

Package Restriction and Url signing

As I came across this post when I struggled with restricting access for inverse geo encoding and static map api I also want to share my findings.

Note that not all google services allow the same restrictions.

We use url signing and android / ios package restriction. Link to the Google documentation

Get apk fingerprint

There are multiple ways to get the fingerprint from the android apk.

With keystore

keytool ‑list ‑v keystore mystore.keystore

With apk

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

C# Example code (Xamarin) to get started

In my productive code I have a base class for Headerinfo and provide an instace to the Geoprovider class. With this approach the code for the google services is 100% shared between windows, android and ios => nuget package.

Android Headers

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

IOS Headers

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

Example code to fetch a static map

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
}

sample code for url signing provided by 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;
    }
}

방법 4:

Hitting the API directly from your code rather than going through a Google‑provided intermediate SDK means that there's no mechanism available to securely get your app's certificate fingerprint and pass that fingerprint along to the API. On the other hand, when you're using one of the provided Android SDKs instead of hitting the API directly—for example, when you send requests using the Android Google Maps SDK—the SDK can handle getting your app's certificate fingerprint so that the app restriction will work as intended.

The Google Developers Console is misleading in this respect because, for some of its APIs, it allows developers to set up key restrictions based on the Android app certificate fingerprint, but then doesn't make available an SDK for Android that's able to check that fingerprint at runtime. What developers are left with, then, is the worse, more insecure option of sending the X‑Android‑Cert and X‑Android‑Package headers alongside their requests as described in the other answer here.

So for the APIs for which no accompanying Android SDK to handle the checking of app certificate fingerprint has been published, it turns out that there's no hidden easy way to get something like, say, Google Play Services to handle getting your app's certificate fingerprint in order to properly use the app key restriction—there's just not a way to do it.

방법 5:

In case you are using appBundle instead of ordinary apk file you need also get SHA‑1 from play.google.com/console/:

enter image description here

and then add it with your package to the console.developers.google.com/apis/credentials

Hope it will save nerves for someone...

(by rmtheisDuy Pham323goeugstmanrmtheisAndriy Antonov)

참조 문서

  1. Restricting usage for an Android key for a Google API (CC BY‑SA 2.5/3.0/4.0)

#google-cloud-platform #google-translate #Android #google-api






관련 질문

Google API용 Android 키 사용 제한 (Restricting usage for an Android key for a Google API)

GCS 버킷에서 큰 폴더를 삭제하는 빠른 방법 (Fast way to delete big folder on GCS bucket)

Terraform 코드와 충돌하는 "소유자"에 의한 GCP 콘솔/클라우드 셸 변경을 어떻게 방지할 수 있습니까? (How can you prevent GCP console/cloud shell changes by "Owners" conflicting with the terraform code?)

서비스 계정으로 인증할 때 project_id를 명시적으로 설정하는 것을 피할 수 있습니까? (Is it possible to avoid setting project_id explicitly when authing with service account?)

Wordpress가 새 PHP 버전을 감지하지 못합니다 (Wordpress doesn't detect new php version)

카운터를 사용하여 테이블에 삽입된 레코드 수를 계산하고 python 스크립트를 사용하여 실행되는 각 작업에서 삽입된 레코드 수를 인쇄하는 방법 (How to count number of inserted records in table using counter and print the count of inserted record in each job run using python script)

Google Cloud 자연어 가져오기 오류 (Google Cloud Natural Language Import Error)

gcloud는 전체 컨테이너를 다시 빌드하지만 Dockerfile은 동일하고 스크립트만 변경됨 (gcloud rebuilds complete container but Dockerfile is the same, only the script has changed)

GCP를 통한 Kubernetes 대시보드 (Kubernetes dashboard via GCP)

Firebase 실시간 데이터베이스 키 없이 하위 레코드를 얻는 방법 (Firebase realtime database how to get a child record without the key)

Google Cloud Function / MongoDB VM 인스턴스 통신 (Google Cloud Function / MongoDB VM instance communication)

Google Artifact Registry에서 도커 풀 수를 추적할 수 있습니까? (Is it possible track the number of docker pulls in Google Artifact Registry?)







코멘트