1. לפני שמתחילים
כדי לוודא שהמשתמשים שמקיימים אינטראקציה עם אפליקציית האינטרנט שלכם הם חוקיים, תטמיעו את Firebase App Check ותשתמשו באסימוני JWT של reCAPTCHA כדי לאמת את סשנים של משתמשים. ההגדרה הזו תאפשר לכם לטפל בצורה מאובטחת בבקשות מאפליקציית הלקוח אל Places API (חדש).
מה תפַתחו
כדי להדגים זאת, ניצור אפליקציית אינטרנט שתציג מפה בזמן הטעינה. הוא גם ייצור אסימון reCAPTCHA באופן דיסקרטי באמצעות Firebase SDK. לאחר מכן האסימון הזה נשלח לשרת Node.js, שבו מערכת Firebase מאמתת אותו לפני שהיא ממלאת בקשות ל-Places API.
אם האסימון תקף, מערכת Firebase App Check תשמור אותו עד שתוקפו יפוג, כך שלא תצטרכו ליצור אסימון חדש לכל בקשת לקוח. אם הטוקן לא תקף, המשתמש יתבקש להשלים שוב את האימות באמצעות reCAPTCHA כדי לקבל טוקן חדש.
2. דרישות מוקדמות
כדי להשלים את הקודלאב הזה, עליכם להכיר את הנושאים הבאים.
מוצרי Google Cloud הנדרשים
- Google Cloud Firebase App Check: מסד נתונים לניהול אסימונים
- Google reCAPTCHA: יצירת אסימונים ואימות שלהם. זהו כלי שמשמש להבדיל בין בני אדם לבין בוטים באתרים. המערכת פועלת על ידי ניתוח של התנהגות המשתמשים, מאפייני הדפדפן ומידע על הרשת כדי ליצור ציון שמציין את הסבירות שהמשתמש הוא רובוט. אם הציון גבוה מספיק, המשתמש נחשב לאדם ולא נדרשת פעולה נוספת. אם הציון נמוך, יכול להיות שתוצג חידת CAPTCHA כדי לאשר את זהות המשתמש. הגישה הזו פחות פולשנית משיטות CAPTCHA מסורתיות, וכך חוויית המשתמש חלקה יותר.
- (אופציונלי) Google Cloud App Engine: סביבת הפריסה.
המוצרים הנדרשים של הפלטפורמה של מפות Google
ב-Codelab הזה נשתמש במוצרים הבאים של פלטפורמת מפות Google:
- Maps JavaScript API נטען ומוצג באפליקציית האינטרנט
- בקשה של Places API (חדש) שהונפקה על ידי שרת הקצה העורפי
דרישות נוספות ל-Codelab הזה
כדי להשלים את Codelab הזה, תצטרכו את החשבונות, השירותים והכלים הבאים:
- חשבון ב-Google Cloud Platform שבו החיוב מופעל
- מפתח API של הפלטפורמה של מפות Google עם הפעלה של Maps JavaScript API ו-Places
- ידע בסיסי ב-JavaScript, ב-HTML וב-CSS
- ידע בסיסי ב-Node.js
- עורך טקסט או סביבת פיתוח משולבת (IDE) לבחירתכם
3. טיפים להגדרה
הגדרת הפלטפורמה של מפות Google
אם עדיין אין לכם חשבון ב-Google Cloud Platform ופרויקט שבו החיוב מופעל, תוכלו לעיין במדריך תחילת העבודה עם פלטפורמת מפות Google כדי ליצור חשבון לחיוב ופרויקט.
- ב-Cloud Console, לוחצים על התפריט הנפתח של הפרויקט ובוחרים את הפרויקט שבו רוצים להשתמש ב-codelab הזה.
- מפעילים את ממשקי ה-API וערכות ה-SDK של הפלטפורמה של מפות Google הנדרשים לסדנת הקוד הזו ב-Google Cloud Marketplace. כדי לעשות זאת, פועלים לפי השלבים שמפורטים בסרטון הזה או במסמך הזה.
- יוצרים מפתח API בדף Credentials במסוף Cloud. אפשר לפעול לפי השלבים שמפורטים בסרטון הזה או במסמך הזה. כל הבקשות לפלטפורמה של מפות Google מחייבות מפתח API.
Application Default Credentials
תשתמשו ב-Firebase Admin SDK כדי לקיים אינטראקציה עם פרויקט Firebase שלכם, וגם כדי לשלוח בקשות ל-Places API. כדי שה-SDK יפעל, תצטרכו לספק פרטי כניסה תקפים.
אנחנו נשתמש באימות ADC (Application Default Credentials אוטומטי) כדי לאמת את השרת לצורך שליחת בקשות. לחלופין (לא מומלץ), אפשר ליצור חשבון שירות ולאחסן את פרטי הכניסה בקוד.
הגדרה: Application Default Credentials (ADC) הוא מנגנון של Google Cloud שמאפשר לאמת את האפליקציות באופן אוטומטי בלי לנהל את פרטי הכניסה באופן מפורש. הוא מחפש את פרטי הכניסה במיקומים שונים (כמו משתני סביבה, קבצים של חשבונות שירות או שרת המטא-נתונים של Google Cloud) ומשתמש בפרטי הכניסה הראשונים שהוא מוצא.
- ב-Terminal, משתמשים בפקודה הבאה שמאפשרת לאפליקציות לגשת בצורה מאובטחת למשאבי Google Cloud מטעם המשתמש שמחובר כרגע:
gcloud auth application-default login
- יוצרים קובץ .env ברמה הבסיסית (root) שמציין משתנה של פרויקט ב-Google Cloud:
GOOGLE_CLOUD_PROJECT="your-project-id"
פרטי כניסה חלופיים (לא מומלץ)
יצירה של חשבון שירות
- הכרטיסייה Google Maps Platform > '+Create Credentials' (יצירת פרטי כניסה) > חשבון שירות
- מוסיפים את התפקיד 'אדמין ב-Firebase AppCheck', ואז מזינים את שם חשבון השירות שהקלדתם, למשל: firebase-appcheck-codelab@yourproject.iam.gserviceaccount.com
פרטי כניסה
- לוחצים על חשבון השירות שנוצר.
- עוברים לכרטיסייה KEYS (מפתחות) כדי ליצור מפתח > JSON > שומרים את פרטי הכניסה בפורמט JSON שהורדתם. מעבירים את קובץ xxx.json שהורדתם באופן אוטומטי לתיקיית השורש
- (פרק הבא) נותנים לו שם נכון בקובץ server.js של nodejs (firebase-credentials.json)
4. שילוב של Firebase AppCheck
תקבלו את פרטי ההגדרה של Firebase ומפתחות סודיים של reCAPTCHA.
צריך להדביק אותם באפליקציית הדגמה ולהפעיל את השרת.
יצירת אפליקציה ב-Firebase
- נכנסים לדף 'ניהול הפרויקט' בכתובת https://console.firebase.google.com (קישורים):
SELECT את הפרויקט ב-Google Cloud שכבר נוצר (יכול להיות שתצטרכו לציין: "בחירת משאב ההורה")
- הוספת אפליקציה מהתפריט (גלגל השיניים) בפינה הימנית העליונה
קוד אתחול של Firebase
- שומרים את קוד האיפוס של Firebase כדי להדביק אותו בקובץ script.js (בפרק הבא) בצד הלקוח
- רישום האפליקציה כדי לאפשר ל-Firebase להשתמש באסימונים של reCAPTCHA גרסה 3
https://console.firebase.google.com/u/0/project/YOUR_PROJECT/appcheck/apps
- בוחרים ב-reCAPTCHA → יוצרים מפתח באתר reCAPTCHA (עם הגדרת הדומיינים הנכונים: localhost לפיתוח אפליקציות)
- הדבקת הסוד של reCAPTCHA ב-Firebase AppCheck
- סטטוס האפליקציה אמור להפוך לירוק.
5. אפליקציית הדגמה
- אפליקציית אינטרנט ללקוח: קובצי HTML, JavaScript ו-CSS
- שרת: קובץ Node.js
- סביבה (.env): מפתחות API
- תצורה (app.yaml): הגדרות הפריסה של Google App Engine
הגדרת Node.js:
- ניווט: פותחים את הטרמינל ועוברים לספריית השורש של הפרויקט שהועתק.
- מתקינים את Node.js (אם צריך): גרסה 18 ואילך.
node -v # Check installed version
- הפעלת הפרויקט: מריצים את הפקודה הבאה כדי להפעיל פרויקט Node.js חדש, בלי לשנות את הגדרות ברירת המחדל:
npm init
- התקנת יחסי התלות: משתמשים בפקודה הבאה כדי להתקין את יחסי התלות הנדרשים בפרויקט:
npm install @googlemaps/places firebase-admin express axios dotenv
הגדרה: משתני סביבה לפרויקט ב-Google Cloud
- יצירת קובץ סביבה: יוצרים קובץ בשם
.env
בתיקיית השורש של הפרויקט. הקובץ הזה ישמור נתוני הגדרה רגישים, ואין להתחייב אליו במערכת בקרת הגרסאות. - אכלוס משתני הסביבה: פותחים את הקובץ
.env
ומוסיפים את המשתנים הבאים, מחליפים את הסמנים הזמניים לשמירת מקום (placeholder) בערכים בפועל מהפרויקט ב-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. סקירה כללית על הקוד
index.html
- טעינת ספריות Firebase כדי ליצור את האסימון באפליקציה
<!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
- אחזור של מפתחות API: אחזור של מפתחות API למפות Google ול-Firebase App Check משרת קצה עורפי.
- הפעלת Firebase: הגדרת Firebase לאימות ולאבטחה. (החלפת הגדרות → ראו פרק 4).
משך התוקף של אסימון Firebase App Check נע בין 30 דקות ל-7 ימים, והוא מוגדר במסוף Firebase. אי אפשר לשנות את משך התוקף על ידי ניסיון לאלץ רענון של האסימון.
- הפעלת App Check: הפעלת Firebase App Check לאימות האותנטיות של בקשות נכנסות.
- טעינת Google Maps API: טעינת ספריית JavaScript של מפות Google באופן דינמי כדי להציג את המפה.
- הפעלת המפה: יצירת מפה של Google שממוקדת במיקום ברירת המחדל.
- טיפול בקליקים במפה: האובייקט מקשיב לקליקים במפה ומעדכן את נקודת המרכז בהתאם.
- שליחת שאילתות ל-Places API: שליחת בקשות ל-API לקצה העורפי (
/api/data
) כדי לאחזר מידע על מקומות (מסעדות, פארקים, ברים) בסביבת המיקום שבו לחצתם, באמצעות Firebase App Check לצורך הרשאה. - הצגת סמנים: הצגת הנתונים שאוחזרו במפה כסמנים, עם השמות והסמלים שלהם.
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
- טעינת משתני סביבה (מפתחות API, מזהה פרויקט ב-Google) מקובץ
.env
. - מפעילה את השרת,שמאזין לבקשות ב-
http://localhost:3000
. - הפעלת Firebase Admin SDK באמצעות Application Default Credentials (ADC).
- מקבל טוקן reCAPTCHA מ-
script.js
. - אימות התקינות של האסימון שהתקבל.
- אם האסימון תקין, שולחים בקשת POST ל-Google Places API עם פרמטרים של חיפוש שכלולים בה.
- מעבדת את התשובה ומחזירה אותה מה-Places API ללקוח.
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. הרצת האפליקציה
בסביבה שבחרתם, מריצים את השרת מהמסוף ומנווטים אל http://localhost:3000
npm start
האסימון נוצר כמשתנה גלובלי, מוסתר מחלון הדפדפן של המשתמש ומשודר לשרת לצורך עיבוד. פרטי האסימון מופיעים ביומני השרת. | פרטים על הפונקציות של השרת ועל התגובה לבקשה של Places API לחיפוש בקרבת מקום זמינים ביומני השרת. |
פתרון בעיות:
חשוב לוודא שמזהה הפרויקט ב-Google זהה בכל ההגדרות:
- בקובץ .env (המשתנה GOOGLE_CLOUD_PROJECT)
- בהגדרות של gcloud במסוף:
gcloud config set project your-project-id
- בהגדרה של reCaptcha
- בהגדרה של Firebase
אחר
- יוצרים אסימון ניפוי באגים שאפשר להשתמש בו במקום מפתח האתר של reCAPTCHA ב-
script.js
למטרות בדיקה ופתרון בעיות.
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);
}
- ניסיונות אימות רבים שנכשלו, למשל שימוש במפתח אתר שגוי של recaptcha, עשויים לגרום להאטה זמנית.
FirebaseError: AppCheck: Requests throttled due to 403 error. Attempts allowed again after 01d:00m:00s (appCheck/throttled).
פרטי כניסה ל-ADC
- מוודאים שאתם מחוברים לחשבון gcloud הנכון
gcloud auth login
- מוודאים שהספריות הנחוצות מותקנות
npm install @googlemaps/places firebase-admin
- מוודאים שספריית Firebase נטענת בקובץ server.js
const {GoogleAuth} = require('google-auth-library');
- פיתוח מקומי: הגדרת ADC
gcloud auth application-default login
- התחזות: פרטי הכניסה ל-ADC נשמרו
gcloud auth application-default login --impersonate-service-account your_project@appspot.gserviceaccount.com
- לבסוף, בודקים את ADC באופן מקומי, שומרים את הסקריפט הבא בתור test.js ומריצים אותו ב-Terminal:
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. זהו, כל הכבוד!
השלבים הבאים
פריסה ב-App Engine:
- מכינים את הפרויקט לפריסה ב-Google App Engine, ומבצעים את השינויים הנדרשים בהגדרות.
- משתמשים בכלי שורת הפקודה
gcloud
או במסוף App Engine כדי לפרוס את האפליקציה.
שיפור האימות ב-Firebase:
- אסימונים שמוגדרים כברירת מחדל לעומת אסימונים מותאמים אישית: הטמעת אסימונים מותאמים אישית של Firebase כדי להשתמש בצורה מעמיקה יותר בשירותי Firebase.
- משך החיים של האסימון: מגדירים משכי חיים מתאימים לאסימונים, קצרים יותר לפעולות רגישות (אסימון Firebase בהתאמה אישית עד שעה אחת) וארוכים יותר לסשנים כלליים (אסימון reCAPTCHA: 30 דקות עד 7 שעות).
- בודקים חלופות ל-reCAPTCHA: כדאי לבדוק אם DeviceCheck (iOS), SafetyNet (Android) או App Attest מתאימים לצורכי האבטחה שלכם.
שילוב מוצרים של Firebase:
- Realtime Database או Firestore: אם האפליקציה שלכם זקוקה לסנכרון נתונים בזמן אמת או ליכולות אופליין, כדאי לשלב אותה עם Realtime Database או Firestore.
- Cloud Storage: אפשר להשתמש ב-Cloud Storage כדי לאחסן תוכן שנוצר על ידי משתמשים, כמו תמונות או סרטונים, ולספק אותו.
- אימות: אתם יכולים להשתמש באימות ב-Firebase כדי ליצור חשבונות משתמשים, לנהל סשנים של כניסה ולנהל איפוס סיסמאות.
הרחבה לנייד:
- Android ו-iOS: אם אתם מתכננים ליצור אפליקציה לנייד, כדאי ליצור גרסאות גם לפלטפורמות Android וגם לפלטפורמות iOS.
- ערכות SDK של Firebase: אתם יכולים להשתמש ב-SDK של Firebase ל-Android ול-iOS כדי לשלב בצורה חלקה את התכונות של Firebase באפליקציות לנייד.