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

1. Zanim zaczniesz

Aby zapewnić wiarygodność użytkowników wchodzących w interakcję z Twoją aplikacją internetową, zaimplementujesz Sprawdzanie aplikacji Firebase, wykorzystując tokeny JWT reCAPTCHA do weryfikacji sesji użytkowników. Ta konfiguracja umożliwi bezpieczne obsługiwanie żądań z aplikacji klienckiej do interfejsu Places API (nowego).

b40cfddb731786fa.png

Live Link

Co utworzysz

Aby to zademonstrować, utworzysz aplikację internetową, która po wczytaniu wyświetla mapę. Dyskretnie wygeneruje też token reCAPTCHA za pomocą pakietu SDK Firebase. Token jest następnie wysyłany na serwer Node.js, gdzie Firebase weryfikuje go przed zrealizowaniem żądań do interfejsu Places API.

Jeśli token jest ważny, 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 zobaczy prośbę o ponowne przeprowadzenie weryfikacji reCAPTCHA w celu uzyskania nowego tokena.

2. Wymagania wstępne

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

Wymagane usługi Google Cloud

  • Google Cloud Sprawdzanie aplikacji Firebase: baza danych do zarządzania tokenami
  • Google reCAPTCHA: tworzenie i weryfikacja tokenów. To narzędzie służące do odróżniania ludzi od botów w witrynach. Działa ona na podstawie analizy zachowań użytkowników, atrybutów przeglądarki i informacji o sieci, aby wygenerować wynik wskazujący prawdopodobieństwo, że użytkownik jest botem. Jeśli wynik jest wystarczająco wysoki, użytkownik jest uznawany za osobę i nie musisz nic robić. Jeśli wynik jest niski, może zostać wyświetlone zadanie CAPTCHA, aby potwierdzić tożsamość użytkownika. To rozwiązanie jest mniej uciążliwe niż tradycyjne metody CAPTCHA, co przekłada się na lepsze wrażenia użytkowników.
  • (Opcjonalnie) Google Cloud App Engine: środowisko wdrażania.

Wymagane usługi Google Maps Platform

W tym ćwiczeniu wykorzystasz te usługi Google Maps Platform:

Inne wymagania dotyczące tego ćwiczenia

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

  • konto Google Cloud Platform z włączonymi płatnościami;
  • Klucz interfejsu API Google Maps Platform z włączonymi interfejsami Maps JavaScript API i Places
  • podstawowa znajomość języków JavaScript, HTML i CSS;
  • podstawową wiedzę o Node.js;
  • wybrany edytor tekstu lub środowisko IDE;

3. Konfiguracja

Konfigurowanie Google Maps Platform

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

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

e7ffad81d93745cd.png

  1. Włącz interfejsy API i pakiety SDK Google Maps Platform wymagane w tym ćwiczeniu w Google Cloud Marketplace. Aby to zrobić, wykonaj czynności opisane w tym filmie lub 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 wysyłane do Google Maps Platform wymagają klucza interfejsu API.

Domyślne uwierzytelnianie aplikacji

Do interakcji z projektem w Firebase i wysyłania żądań do interfejsu Places API będziesz używać pakietu Firebase Admin SDK. Aby to działało, musisz podać prawidłowe dane logowania.

Do uwierzytelniania serwera podczas wysyłania żądań będziemy używać uwierzytelniania ADC (automatycznego uwierzytelniania domyślnego). Możesz też (niezalecane) utworzyć konto usługi i przechowywać dane logowania w kodzie.

Definicja: domyślne uwierzytelnianie aplikacji (ADC) to mechanizm udostępniany przez Google Cloud, który umożliwia automatyczne uwierzytelnianie aplikacji bez konieczności jawnego zarządzania danymi logowania. Wyszukuje ona dane logowania w różnych lokalizacjach (np. w zmiennych środowiskowych, plikach konta usługi lub na serwerze metadanych Google Cloud) i używa pierwszych znalezionych danych.

  • W terminalu użyj tego polecenia, aby umożliwić aplikacjom bezpieczny dostęp do zasobów Google Cloud w imieniu aktualnie zalogowanego użytkownika:
gcloud auth application-default login
  • W katalogu głównym utworzysz 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ł) Nadaj mu prawidłową nazwę w pliku serwera nodejs server.js (​​firebase-credentials.json).

4. Integracja ze Sprawdzaniem aplikacji Firebase

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

Wklej je do aplikacji demonstracyjnej i uruchom serwer.

Tworzenie aplikacji w Firebase

WYBIERZ utworzony wcześniej projekt Google Cloud (może być konieczne określenie: „Wybieranie zasobu nadrzędnego”).

a6d171c6d7e98087.png a16010ba102cc90b.png

  • Dodawanie aplikacji z menu w lewym górnym rogu (koło zębate)

18e5a7993ad9ea53.png 4632158304652118.png

Kod inicjowania Firebase

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

f10dcf6f5027e9f0.png

  • Rejestrowanie aplikacji, aby umożliwić Firebase korzystanie z tokenów reCAPTCHA v3

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

da7efe203ce4142c.png

  • Wybierz reCAPTCHA → utwórz klucz w witrynie reCAPTCHA (z prawidłowo skonfigurowanymi domenami: localhost na potrzeby tworzenia aplikacji).

b47eab131617467.png e6bddef9d5cf5460.png

  • Wklej klucz tajny reCAPTCHA w usłudze Sprawdzanie aplikacji Firebase

a63bbd533a1b5437.png

  • Stan aplikacji powinien zmienić się na zielony.

4f7962b527b78ee5.png

5. Aplikacja w wersji demonstracyjnej

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

Konfiguracja Node.js:

  • Przejdź: otwórz terminal i przejdź do katalogu głównego sklonowanego projektu.
  • Zainstaluj Node.js (w razie potrzeby): wersja 18 lub nowsza.
node -v  # Check installed version
  • Zainicjuj projekt: Uruchom następujące 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 konfiguracyjne i nie powinien być zapisywany w systemie kontroli wersji.
  • Wypełnij zmienne środowiskowe: otwórz plik .env i dodaj te zmienne, zastępując symbole zastępcze 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

  • Wczytuje 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 Google Maps i funkcji Sprawdzanie aplikacji Firebase z serwera backendu.
  • Inicjuje Firebase: konfiguruje Firebase na potrzeby uwierzytelniania i bezpieczeństwa. (Zastąp konfigurację → patrz rozdział 4).

Czas ważności tokena Sprawdzania aplikacji Firebase, który wynosi od 30 minut do 7 dni, jest konfigurowany w konsoli Firebase i nie można go zmienić, próbując wymusić odświeżenie tokena.

  • Aktywuje Sprawdzanie aplikacji: włącza Sprawdzanie aplikacji Firebase, aby weryfikować autentyczność żądań przychodzących.
  • Wczytuje interfejs API Map Google: dynamicznie wczytuje bibliotekę JavaScript Map Google, aby wyświetlić mapę.
  • Inicjuje mapę: tworzy Mapę Google wyśrodkowaną na domyślnej lokalizacji.
  • Obsługuje kliknięcia mapy: nasłuchuje kliknięć na mapie i odpowiednio aktualizuje punkt środkowy.
  • Wysyła zapytania do interfejsu Places API: wysyła żądania do backendu interfejsu API (/api/data), aby pobrać informacje o miejscach (restauracjach, parkach, barach) w pobliżu klikniętej lokalizacji. Do autoryzacji używa funkcji Sprawdzanie aplikacji Firebase.
  • Wyświetla znaczniki: nanosi na mapę pobrane dane w postaci znaczników, wyświetlając ich nazwy i ikony.
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

  • Wczytuje zmienne środowiskowe (klucze interfejsu API, identyfikator projektu Google) z pliku .env.
  • Uruchamia serwer,który nasłuchuje żądań na porcie http://localhost:3000.
  • Inicjuje pakiet Firebase Admin SDK przy użyciu domyślnych uwierzytelnień aplikacji (ADC).
  • Otrzymuje token reCAPTCHA z usługi script.js.
  • Sprawdza ważność otrzymanego tokena.
  • Jeśli token jest prawidłowy, wysyła żądanie POST do interfejsu Google Places API z uwzględnionymi 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 z terminala i przejdź do adresu http://localhost:3000.

npm start 

Token jest tworzony jako zmienna globalna, ukryta w oknie przeglądarki użytkownika, i przesyłany 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órego można używać 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 ADC

  • Sprawdź, czy korzystasz z właściwego konta gcloud.
gcloud auth login 
  • Sprawdź, czy zainstalowane są niezbędne biblioteki.
npm install @googlemaps/places firebase-admin
  • Sprawdź, czy w pliku server.js załadowana jest biblioteka Firebase.
const {GoogleAuth} = require('google-auth-library');
gcloud auth application-default login
  • Podszywanie się: zapisano dane logowania ADC
gcloud auth application-default login --impersonate-service-account your_project@appspot.gserviceaccount.com
  • Ostatecznie przetestuj lokalnie ADC, zapisując poniższy 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. Świetnie!

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.

Ulepszanie Uwierzytelniania Firebase:

  • Domyślne i niestandardowe tokeny: wdróż niestandardowe tokeny Firebase, aby w pełni korzystać z usług Firebase.
  • Okres ważności tokena: ustaw odpowiedni okres ważności tokena – krótszy w przypadku operacji wrażliwych (niestandardowy token Firebase: do godziny), a dłuższy w przypadku sesji ogólnych (token reCAPTCHA: od 30 minut do 7 godzin).
  • Poznaj alternatywy dla reCAPTCHA: sprawdź, czy DeviceCheck (iOS), SafetyNet (Android) lub App Attest spełniają Twoje wymagania dotyczące bezpieczeństwa.

Integracja usług Firebase:

  • Baza danych czasu rzeczywistego lub Firestore: jeśli aplikacja wymaga synchronizacji danych w czasie rzeczywistym lub funkcji offline, zintegruj ją z bazą danych czasu rzeczywistego lub Firestore.
  • Cloud Storage: używaj Cloud Storage do przechowywania i obsługi treści użytkowników, takich jak obrazy czy filmy.
  • Uwierzytelnianie: korzystaj z usługi Uwierzytelnianie Firebase, aby tworzyć konta użytkowników, zarządzać sesjami logowania i obsługiwać resetowanie haseł.

Rozszerzanie na urządzenia mobilne:

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