Weryfikowanie żądań interfejsu Places API za pomocą Sprawdzania aplikacji Firebase i reCAPTCHA

1. Zanim zaczniesz

Aby mieć pewność, że użytkownicy, którzy korzystają z Twojej aplikacji internetowej, są autentyczni, wprowadź Sprawdzanie aplikacji Firebase, korzystając z tokenów JWT reCAPTCHA do weryfikowania sesji użytkowników. Dzięki temu będziesz mógł bezpiecznie obsługiwać żądania wysyłane przez aplikację klienta do interfejsu Places API (New).

b40cfddb731786fa.png

Link do transmisji na żywo

Co utworzysz.

Aby to zademonstrować, utwórz aplikację internetową, która po załadowaniu wyświetla mapę. Skryjesz też token reCAPTCHA za pomocą pakietu Firebase SDK. Następnie token jest wysyłany na serwer Node.js, gdzie Firebase weryfikuje go przed spełnieniem żądań do interfejsu Places API.

Jeśli token jest prawidłowy, Sprawdzanie aplikacji Firebase będzie go przechowywać do momentu wygaśnięcia, co eliminuje konieczność tworzenia nowego tokena dla każdego żądania klienta. Jeśli token jest nieprawidłowy, użytkownik zostanie poproszony o ponowne przejście weryfikacji reCAPTCHA w celu uzyskania nowego tokena.

2. Wymagania wstępne

Aby ukończyć to ćwiczenie, musisz zapoznać się z informacjami poniżej. daea823b6bc38b67.png

Wymagane usługi Google Cloud

  • Sprawdzanie aplikacji Firebase w Google Cloud: baza danych do zarządzania tokenami
  • Google reCAPTCHA: tworzenie i weryfikacja tokena. Jest to narzędzie służące do odróżniania ludzi od botów w witrynach. Polega ono na analizie zachowania użytkownika, atrybutów przeglądarki i informacji o sieci w celu wygenerowania wyniku wskazującego na prawdopodobieństwo, że użytkownik jest botem. Jeśli wynik jest wystarczająco wysoki, użytkownik jest uznawany za człowieka i nie musi podejmować żadnych dalszych działań. Jeśli wynik jest niski, może zostać wyświetlona łamigłówka CAPTCHA, która pozwoli potwierdzić tożsamość użytkownika. To rozwiązanie jest mniej uciążliwe niż tradycyjne metody CAPTCHA, co zwiększa wygodę użytkowników.
  • (Opcjonalnie) Google Cloud App Engine: środowisko wdrażania.

Wymagane usługi Google Maps Platform

W tym Codelab użyjesz tych usług Google Maps Platform:

Inne wymagania dotyczące tego ćwiczenia z programowania

Aby ukończyć to ćwiczenie, potrzebujesz tych kont, usług i narzędzi:

  • Konto Google Cloud Platform z włączonym rozliczaniem
  • Klucz interfejsu API Google Maps Platform z włączonymi interfejsami Maps JavaScript API i Places
  • podstawy JavaScriptu, HTML-a i CSS-a;
  • podstawową znajomość Node.js;
  • Edytor tekstu lub IDE

3. Konfiguracja

Konfigurowanie Google Maps Platform

Jeśli nie masz jeszcze konta Google Cloud Platform ani projektu z włączonymi płatnościami, zapoznaj się z poradnikiem Pierwsze kroki z Google Maps Platform, aby utworzyć konto rozliczeniowe i projekt.

  1. W konsoli Google Cloud kliknij menu projektu i wybierz projekt, którego chcesz użyć w tym CodeLab.

e7ffad81d93745cd.png

  1. Włącz interfejsy API i pakiety SDK Google Maps Platform wymagane w tym laboratorium kodu na platformie Google Cloud Marketplace. Aby to zrobić, wykonaj czynności opisane w tym filmie lub w tej dokumentacji.
  2. Wygeneruj klucz interfejsu API na stronie Dane logowania w konsoli Cloud. Możesz wykonać czynności opisane w tym filmie lub w tej dokumentacji. Wszystkie żądania do Google Maps Platform wymagają klucza interfejsu API.

Domyślne dane logowania aplikacji

Pakiet SDK Firebase Admin będzie Ci służyć do interakcji z projektem Firebase oraz do wysyłania żądań do interfejsu Places API. Aby działał, musisz podać prawidłowe dane logowania.

Do uwierzytelniania serwera w celu wysyłania żądań użyjemy uwierzytelniania ADC (automatycznego uwierzytelniania za pomocą domyślnych danych logowania). Możesz też (nie jest to zalecane) utworzyć konto usługi i przechowywać w nim dane uwierzytelniające.

Definicja: domyślne uwierzytelnianie aplikacji (ADC) to mechanizm Google Cloud umożliwiający automatyczne uwierzytelnianie aplikacji bez konieczności zarządzania danymi logowania. Szuka danych logowania w różnych lokalizacjach (np. w zmiennych środowiskowych, plikach konta usługi lub na serwerze metadanych Google Cloud) i używa pierwszego znalezionego.

  • W terminalu użyj tego polecenia, które umożliwia Twoim aplikacjom bezpieczny dostęp do zasobów Google Cloud w imieniu aktualnie zalogowanego użytkownika:
gcloud auth application-default login
  • Utwórz w katalogu głównym plik .env, który określa zmienną projektu Google Cloud:
GOOGLE_CLOUD_PROJECT="your-project-id"

Tworzenie konta usługi

Dane logowania

  • Kliknij utworzone konto usługi.
  • Na karcie KLUCZE kliknij Utwórz klucz > JSON > zapisz pobrane dane logowania w formacie JSON. Przenieś automatycznie pobrany plik xxx.json do folderu głównego.
  • (Następny rozdział) Wpisz poprawną nazwę w pliku nodejs server.js (firebase-credentials.json)

4. Integracja z Firebase AppCheck

Uzyskasz szczegóły konfiguracji Firebase i klucze tajne reCAPTCHA.

Wklej je w aplikacji demonstracyjnej i uruchom serwer.

Tworzenie aplikacji w Firebase

WYBIERZ projekt Google Cloud, który został już utworzony (może być konieczne określenie: „Wybieranie zasobu nadrzędnego”).

a6d171c6d7e98087.png a16010ba102cc90b.png

  • Dodaj aplikację w lewym górnym rogu menu (ikona koła zębatego)

18e5a7993ad9ea53.png 4632158304652118.png

Kod inicjalizacji Firebase

  • Zapisz kod inicjowania Firebase, aby wkleić go w pliku script.js (następny rozdział) po stronie klienta.

f10dcf6f5027e9f0.png

  • Zarejestruj aplikację, aby umożliwić Firebase używanie tokenów reCAPTCHA v3

https://console.firebase.google.com/u/0/project/YOUR_PROJECT/appcheck/apps

da7efe203ce4142c.png

  • Wybierz reCAPTCHA → utwórz klucz na stronie reCAPTCHA (z prawidłowo skonfigurowanymi domenami: localhost dla programistów aplikacji).

b47eab131617467.png e6bddef9d5cf5460.png

  • Wklej klucz tajny reCAPTCHA w Firebase AppCheck

a63bbd533a1b5437.png

  • Stan aplikacji powinien się zmienić na zielony.

4f7962b527b78ee5.png

5. Aplikacja demonstracyjna

  • Aplikacja internetowa klienta: pliki HTML, JavaScript i CSS.
  • Serwer: plik Node.js.
  • Środowisko (plik .env): klucze interfejsu API
  • Konfiguracja (plik app.yaml): ustawienia wdrożenia Google App Engine.

Konfiguracja Node.js:

  • Przechodzenie: otwórz terminal i przejdź do głównego katalogu sklonowanego projektu.
  • Zainstaluj Node.js (w razie potrzeby): wersja 18 lub nowsza.
node -v  # Check installed version
  • Inicjowanie projektu: uruchom to polecenie, aby zainicjować nowy projekt Node.js, pozostawiając wszystkie ustawienia domyślne:
npm init 
  • Zainstaluj zależności: aby zainstalować wymagane zależności projektu, użyj tego polecenia:
npm install @googlemaps/places firebase-admin express axios dotenv

Konfiguracja: zmienne środowiskowe projektu Google Cloud

  • Tworzenie pliku środowiska: w katalogu głównym projektu utwórz plik o nazwie .env. Ten plik będzie przechowywać poufne dane konfiguracji i nie powinien być uwzględniany w kontroli wersji.
  • Wypełnianie zmiennych środowiskowych: otwórz plik .env i dodaj te zmienne, zastępując puste miejsca rzeczywistymi wartościami z projektu Google Cloud:
# Google Cloud Project ID
GOOGLE_CLOUD_PROJECT="your-cloud-project-id"

# reCAPTCHA Keys (obtained in previous steps) 
RECAPTCHA_SITE_KEY="your-recaptcha-site-key"
RECAPTCHA_SECRET_KEY="your-recaptcha-secret-key"

# Maps Platform API Keys (obtained in previous steps)
PLACES_API_KEY="your-places-api-key"
MAPS_API_KEY="your-maps-api-key"

6. Omówienie kodu

index.html

  • Ładuje biblioteki Firebase, aby utworzyć token w aplikacji
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Places API with AppCheck</title>
  <style></style>  </head>
<body>
  <div id="map"></div>

    <!-- Firebase services -->
  <script src="https://www.gstatic.com/firebasejs/9.15.0/firebase-app-compat.js"></script>
  <script src="https://www.gstatic.com/firebasejs/9.15.0/firebase-app-check-compat.js"></script>
  
  <script type="module" src="./script.js"></script> 
  <link rel="stylesheet" href="./style.css">
</body>
</html>

script.js

  • Pobiera klucze interfejsu API: pobiera klucze interfejsu API Map Google i sprawdzającej aplikacji Firebase z serwera backendu.
  • Inicjowanie Firebase: konfiguruje Firebase na potrzeby uwierzytelniania i zabezpieczeń. (Konfiguracja zastępcza – patrz rozdział 4).

Czas ważności tokena Sprawdzania aplikacji Firebase, który może wynosić od 30 minut do 7 dni, jest konfigurowany w konsoli Firebase i nie można go zmienić przez wymuszenie odświeżenia tokena.

  • Aktywuje Sprawdzanie aplikacji: umożliwia Sprawdzaniu aplikacji Firebase weryfikowanie autentyczności przychodzących żądań.
  • Wczytuje interfejs Google Maps API: dynamicznie wczytuje bibliotekę JavaScript Google Maps, aby wyświetlić mapę.
  • Inicjowanie mapy: tworzy mapę Google wyśrodkowaną na domyślnej lokalizacji.
  • Obsługuje kliknięcia na mapie: nasłuchuje kliknięć na mapie i odpowiednio aktualizuje punkt centralny.
  • Wysyła zapytania do interfejsu Places API: wysyła żądania do interfejsu API (/api/data) w celu pobierania informacji o miejscach (restauracje, parki, bary) w pobliżu klikniętego miejsca, korzystając z autoryzacji Firebase App Check.
  • Wyświetlanie znaczników: pobrane dane są nanoszone na mapie jako znaczniki z nazwami i ikonami.
let mapsApiKey, recaptchaKey; // API keys
let currentAppCheckToken = null; // AppCheck token

async function init() {
  try {
    await fetchConfig(); // Load API keys from .env variable

    /////////// REPLACE with your Firebase configuration details
    const firebaseConfig = {
      apiKey: "AIza.......",
      authDomain: "places.......",
      projectId: "places.......",
      storageBucket: "places.......",
      messagingSenderId: "17.......",
      appId: "1:175.......",
      measurementId: "G-CPQ.......",
    };
    /////////// REPLACE 

    // Initialize Firebase and App Check
    await firebase.initializeApp(firebaseConfig);
    await firebase.appCheck().activate(recaptchaKey);

    // Get the initial App Check token
    currentAppCheckToken = await firebase.appCheck().getToken();

    // Load the Maps JavaScript API dynamically
    const scriptMaps = document.createElement("script");
    scriptMaps.src = `https://maps.googleapis.com/maps/api/js?key=${mapsApiKey}&libraries=marker,places&v=beta`;
    scriptMaps.async = true;
    scriptMaps.defer = true;
    scriptMaps.onload = initMap; // Create the map after the script loads
    document.head.appendChild(scriptMaps);
  } catch (error) {
    console.error("Firebase initialization error:", error);
    // Handle the error appropriately (e.g., display an error message)
  }
}
window.onload = init()

// Fetch configuration data from the backend API
async function fetchConfig() {
  const url = "/api/config";

  try {
    const response = await fetch(url);
    const config = await response.json();
    mapsApiKey = config.mapsApiKey;
    recaptchaKey = config.recaptchaKey;
  } catch (error) {
    console.error("Error fetching configuration:", error);
    // Handle the error (e.g., show a user-friendly message)
  }
}

// Initialize the map when the Maps API script loads
let map; // Dynamic Map
let center = { lat: 48.85557501, lng: 2.34565006 };
function initMap() {
  map = new google.maps.Map(document.getElementById("map"), {
    center: center,
    zoom: 13,
    mapId: "b93f5cef6674c1ff",
    zoomControlOptions: {
      position: google.maps.ControlPosition.RIGHT_TOP,
    },
    streetViewControl: false,
    mapTypeControl: false,
    clickableIcons: false,
    fullscreenControlOptions: {
      position: google.maps.ControlPosition.LEFT_TOP,
    },
  });

  // Initialize the info window for markers
  infoWindow = new google.maps.InfoWindow({});

  // Add a click listener to the map
  map.addListener("click", async (event) => {
    try {
      // Get a fresh App Check token on each click
      const appCheckToken = await firebase.appCheck().getToken();
      currentAppCheckToken = appCheckToken;

      // Update the center for the Places API query
      center.lat = event.latLng.lat();
      center.lng = event.latLng.lng();

      // Query for places with the new token and center
      queryPlaces();
    } catch (error) {
      console.error("Error getting App Check token:", error);
    }
  });
}

function queryPlaces() {
  const url = '/api/data'; // "http://localhost:3000/api/data"

  const body = {
    request: {
      includedTypes: ['restaurant', 'park', 'bar'],
      excludedTypes: [],
      maxResultCount: 20,
      locationRestriction: {
        circle: {
          center: {
            latitude: center.lat,
            longitude: center.lng,
          },
          radius: 4000,
        },
      },
    },
  };

  // Provides token to the backend using header: X-Firebase-AppCheck

  fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Firebase-AppCheck': currentAppCheckToken.token,
    },
    body: JSON.stringify(body),
  })
    .then((response) => response.json())
    .then((data) => {
      // display if response successful
      displayMarkers(data.places);
    })
    .catch((error) => {
      alert('No places');
      // eslint-disable-next-line no-console
      console.error('Error:', error);
    });
}


//// display places markers on map
...

server.js

  • Ładuje zmienną środowiskową (klucze interfejsu API, identyfikator projektu Google) z pliku .env.
  • Uruchamia serwer i nasłuchuje żądań na porcie http://localhost:3000.
  • Inicjowanie pakietu Firebase Admin SDK za pomocą domyślnych danych logowania aplikacji.
  • Otrzymuje token reCAPTCHA od script.js.
  • Weryfikuje ważność otrzymanego tokena.
  • Jeśli token jest prawidłowy, wysyła żądanie POST do interfejsu Places API z dołączonymi parametrami wyszukiwania.
  • Przetwarza i zwraca odpowiedź z Places API do klienta.
const express = require('express');
const axios = require('axios');

const admin = require('firebase-admin');

// .env variables
require('dotenv').config();

// Store sensitive API keys in environment variables
const recaptchaSite = process.env.RECAPTCHA_SITE_KEY;
const recaptchaSecret = process.env.RECAPTCHA_SECRET_KEY;
const placesApiKey = process.env.PLACES_API_KEY;
const mapsApiKey = process.env.MAPS_API_KEY;

// Verify environment variables loaded (only during development)
console.log('recaptchaSite:', recaptchaSite, '\n');
console.log('recaptchaSecret:', recaptchaSecret, '\n');

const app = express();
app.use(express.json());

// Firebase Admin SDK setup with Application Default Credentials (ADC)
const { GoogleAuth } = require('google-auth-library');
admin.initializeApp({
  // credential: admin.credential.applicationDefault(), // optional: explicit ADC
});

// Main API Endpoint 
app.post('/api/data', async (req, res) => {
  const appCheckToken = req.headers['x-firebase-appcheck'];

  console.log("\n", "Token", "\n", "\n", appCheckToken, "\n")

  try {
    // Verify Firebase App Check token for security
    const appCheckResult = await admin.appCheck().verifyToken(appCheckToken);

    if (appCheckResult.appId) {
      console.log('App Check verification successful!');
      placesQuery(req, res);
    } else {
      console.error('App Check verification failed.');
      res.status(403).json({ error: 'App Check verification failed.' });
    }
  } catch (error) {
    console.error('Error verifying App Check token:', error);
    res.status(500).json({ error: 'Error verifying App Check token.' });
  }
});

// Function to query Google Places API
async function placesQuery(req, res) {
  console.log('#################################');
  console.log('\n', 'placesApiKey:', placesApiKey, '\n');

  const queryObject = req.body.request;
  console.log('\n','Request','\n','\n', queryObject, '\n')

  const headers = {
    'Content-Type': 'application/json',
    'X-Goog-FieldMask': '*',
    'X-Goog-Api-Key': placesApiKey,
    'Referer': 'http://localhost:3000',  // Update for production(ie.: req.hostname)
  };

  const myUrl = 'https://places.googleapis.com/v1/places:searchNearby';

  try {
    // Authenticate with ADC
    const auth = new GoogleAuth();
    const { credential } = await auth.getApplicationDefault();

    const response = await axios.post(myUrl, queryObject, { headers, auth: credential });
    
    console.log('############### SUCCESS','\n','\n','Response','\n','\n', );
    const myBody = response.data;
    myBody.places.forEach(place => {
      console.log(place.displayName); 
    });
    res.json(myBody); // Use res.json for JSON data
  } catch (error) {
    console.log('############### ERROR');
    // console.error(error); // Log the detailed error for debugging
    res.status(error.response.status).json(error.response.data); // Use res.json for errors too
  }
}

// Configuration endpoint (send safe config data to the client)
app.get('/api/config', (req, res) => {
  res.json({
    mapsApiKey: process.env.MAPS_API_KEY, 
    recaptchaKey: process.env.RECAPTCHA_SITE_KEY, 
  });
});

// Serve static files
app.use(express.static('static'));

// Start the server
const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server listening on port ${port}`, '\n');
});

7. Uruchamianie aplikacji

W wybranym środowisku uruchom serwer w terminalu i przejdź do adresu http://localhost:3000.

npm start 

Token jest tworzony jako zmienna globalna, ukryty w oknie przeglądarki użytkownika i przekazywany na serwer do przetworzenia. Szczegóły tokena znajdziesz w dziennikach serwera.

Szczegółowe informacje o funkcjach serwera i odpowiedzi na żądanie wyszukiwania w pobliżu w interfejsie Places API znajdziesz w logach serwera.

Rozwiązywanie problemów:

Upewnij się, że identyfikator projektu Google jest spójny w konfiguracji:

  • w pliku .env (zmienna GOOGLE_CLOUD_PROJECT).
  • w konfiguracji gcloud w terminalu:
gcloud config set project your-project-id
  • w konfiguracji reCaptcha.

e6bddef9d5cf5460.png

  • w konfiguracji Firebase

7e17bfbcb8007763.png

Inne

  • Utwórz token debugowania, który może być używany zamiast klucza witryny reCAPTCHA w script.js na potrzeby testowania i rozwiązywania problemów.

9c0beb760d13faef.png

try {
 // Initialize Firebase first
 await firebase.initializeApp(firebaseConfig);
  // Set the debug token
  if (window.location.hostname === 'localhost') { // Only in development
    await firebase.appCheck().activate(
      'YOUR_DEBUG_FIREBASE_TOKEN', // Replace with the token from the console
      true // Set to true to indicate it's a debug token
      );
  } else {
      // Activate App Check
      await firebase.appCheck().activate(recaptchaKey);
}
  • Zbyt wiele nieudanych prób uwierzytelnienia, np. użycie nieprawidłowego klucza witryny reCAPTCHA, może spowodować tymczasowe ograniczenie.
FirebaseError: AppCheck: Requests throttled due to 403 error. Attempts allowed again after 01d:00m:00s (appCheck/throttled).

Dane logowania do ADC

  • Sprawdź, czy używasz właściwego konta gcloud
gcloud auth login 
  • Upewnij się, że wymagane biblioteki są zainstalowane
npm install @googlemaps/places firebase-admin
  • Upewnij się, że biblioteka Firebase server.js jest załadowana.
const {GoogleAuth} = require('google-auth-library');
gcloud auth application-default login
  • Podszycie się: dane logowania ADC zostały zapisane
gcloud auth application-default login --impersonate-service-account your_project@appspot.gserviceaccount.com
  • Ostatecznie przetestuj lokalnie ADC, zapisując ten skrypt jako test.js i uruchamiając go w terminalu: node test.js
const {GoogleAuth} = require('google-auth-library');

async function requestTestADC() {
 try {
   // Authenticate using Application Default Credentials (ADC)
   const auth = new GoogleAuth();
   const {credential} = await auth.getApplicationDefault();

   // Check if the credential is successfully obtained
   if (credential) {
     console.log('Application Default Credentials (ADC) loaded successfully!');
     console.log('Credential:', credential); // Log the credential object
   } else {
     console.error('Error: Could not load Application Default Credentials (ADC).');
   }

   // ... rest of your code ...

 } catch (error) {
   console.error('Error:', error);
 }
}

requestTestADC();

8. To wszystko. Dobra robota.

Dalsze kroki

Wdrażanie w App Engine:

  • Przygotuj projekt do wdrożenia w Google App Engine, wprowadzając niezbędne zmiany w konfiguracji.
  • Aby wdrożyć aplikację, użyj narzędzia wiersza poleceń gcloud lub konsoli App Engine.

Ulepsz Uwierzytelnianie Firebase:

  • Domyślne i niestandardowe tokeny: wdróż niestandardowe tokeny Firebase, aby móc lepiej korzystać z usług Firebase.
  • Czas ważności tokena: ustaw odpowiedni czas ważności tokena, krótszy w przypadku operacji wrażliwych (niestandardowy token Firebase do 1 godziny), dłuższy w przypadku sesji ogólnych (token reCAPTCHA: 30 minut do 7 godzin).
  • Poznaj alternatywy dla reCAPTCHA: sprawdź, czy DeviceCheck (iOS), SafetyNet (Android) lub App Attest są odpowiednie do Twoich potrzeb związanych z bezpieczeństwem.

Integracja usług Firebase:

  • Baza danych czasu rzeczywistego lub Firestore: jeśli Twoja aplikacja potrzebuje synchronizacji danych w czasie rzeczywistym lub funkcji offline, zintegruj ją z Bazą danych czasu rzeczywistego lub Firestore.
  • Cloud Storage: służy do przechowywania i wyświetlania treści wygenerowanych przez użytkowników, takich jak obrazy czy filmy.
  • Uwierzytelnianie: korzystaj z usługi Uwierzytelnianie Firebase do tworzenia kont użytkowników, zarządzania sesjami logowania i resetowania haseł.

Rozwijanie na urządzeniach mobilnych:

  • Android i iOS: jeśli planujesz utworzyć aplikację mobilną, utwórz wersje na platformy Android i iOS.
  • Pakiety SDK Firebase: użyj pakietów SDK Firebase na Androida i iOS, aby bezproblemowo zintegrować funkcje Firebase z aplikacjami mobilnymi.