1. 事前準備
為確保使用者與網頁應用程式互動的合法性,您將導入 Firebase App Check,並利用 reCAPTCHA JWT 權杖驗證使用者工作階段。這項設定可讓您安全地處理從用戶端應用程式傳送至 Places API (新版) 的要求。
建構目標。
為了說明這項功能,您將建立一個網頁應用程式,在載入時顯示地圖。並會使用 Firebase SDK 悄悄產生 reCAPTCHA 權杖。接著,這個權杖會傳送至 Node.js 伺服器,Firebase 會在驗證權杖後,再滿足對 Places API 的任何要求。
如果權杖有效,Firebase App Check 會儲存權杖,直到權杖到期為止,因此您不必為每個用戶端要求建立新的權杖。如果權杖無效,系統會提示使用者再次完成 reCAPTCHA 驗證,以取得新的權杖。
2. 先決條件
您必須熟悉下列項目,才能完成本程式碼研究室。
必要的 Google Cloud 產品
- Google Cloud Firebase App Check:用於管理權杖的資料庫
- Google reCAPTCHA:建立和驗證權杖。這項工具可用於區分網站上的真人和機器人。這項功能會分析使用者行為、瀏覽器屬性和網路資訊,產生分數,指出使用者是機器人的可能性。如果分數夠高,系統就會判定使用者是真人,因此不需要採取進一步行動。如果分數偏低,系統可能會顯示驗證圖片,要求使用者確認身分。這種做法比傳統的 CAPTCHA 方法干擾程度較低,可提供更流暢的使用者體驗。
- (選用) Google Cloud App Engine:部署環境。
必要的 Google 地圖平台產品
在本程式碼研究室中,您將使用下列 Google 地圖平台產品:
- Maps JavaScript API 已載入並顯示在網頁應用程式中
- 後端伺服器發出的 Places API (新版) 要求
本程式碼研究室的其他規定
如要完成本程式碼研究室,您需要具備下列帳戶、服務和工具:
- 已啟用帳單功能的 Google Cloud Platform 帳戶
- 已啟用 Maps JavaScript API 和 Places 的 Google 地圖平台 API 金鑰
- 具備 JavaScript、HTML 和 CSS 的基本知識
- 具備 Node.js 的基本知識
- 您選擇的文字編輯器或 IDE
3. 進行設定
設定 Google 地圖平台
如果您尚未建立 Google Cloud Platform 帳戶,以及已啟用帳單功能的專案,請參閱「開始使用 Google 地圖平台」指南,建立帳單帳戶和專案。
- 在 Cloud 控制台中,點選專案下拉式選單,然後選取要用於本程式碼研究室的專案。
- 在 Google Cloud Marketplace 中啟用本程式碼研究室所需的 Google 地圖平台 API 和 SDK。如要這樣做,請按照這部影片或這份說明文件中的步驟操作。
- 在 Cloud 控制台的「憑證」頁面中產生 API 金鑰。您可以按照這部影片或這份說明文件中的步驟操作。所有 Google 地圖平台要求都需要 API 金鑰。
應用程式預設憑證
您將使用 Firebase Admin SDK 與 Firebase 專案互動,並向 Places API 提出要求,因此必須提供有效的憑證,才能讓 SDK 正常運作。
我們會使用 ADC 驗證 (自動預設憑證) 驗證您的伺服器,以便提出要求。或者,您也可以建立服務帳戶,並在程式碼中儲存憑證 (不建議)。
定義:應用程式預設憑證 (ADC) 是 Google Cloud 提供的一種機制,可自動驗證應用程式,而無需明確管理憑證。會在各種位置 (例如環境變數、服務帳戶檔案或 Google Cloud 中繼資料伺服器) 中尋找憑證,並使用找到的第一個憑證。
- 在終端機中使用下列指令,讓應用程式可代表目前登入的使用者,安全地存取 Google Cloud 資源:
gcloud auth application-default login
- 您會在根目錄中建立 .env 檔案,指定 Google Cloud 專案變數:
GOOGLE_CLOUD_PROJECT="your-project-id"
其他憑證 (不建議使用)
建立服務帳戶
- 「Google 地圖平台」分頁標籤 >「+ 建立憑證」> 服務帳戶
- 新增 Firebase AppCheck 管理員角色,然後輸入剛才輸入的服務帳戶名稱,例如:firebase-appcheck-codelab@yourproject.iam.gserviceaccount.com
憑證
- 按一下已建立的服務帳戶
- 前往「金鑰」分頁,然後依序點選「建立金鑰」>「JSON」> 儲存下載的 JSON 憑證。將自動下載的 xxx.json 檔案移至根目錄
- (下一章) 在 nodejs 檔案 server.js (firebase-credentials.json) 中正確命名
4. Firebase AppCheck 整合
您將取得 Firebase 設定詳細資料和 reCAPTCHA 密鑰。
您會將這些值貼入示範應用程式,並啟動伺服器。
在 Firebase 中建立應用程式
- 前往 Project Admin https://console.firebase.google.com (找到連結):
SELECT 已建立的 Google Cloud 專案 (您可能需要指定「選取父項資源」)
- 從左上方的選單 (齒輪圖示) 新增應用程式
Firebase 初始化程式碼
- 儲存 Firebase 初始化程式碼,以便將其貼到用戶端的 script.js (下一章)
- 註冊應用程式,讓 Firebase 使用 reCAPTCHA v3 權杖
https://console.firebase.google.com/u/0/project/YOUR_PROJECT/appcheck/apps
- 選擇 reCAPTCHA → 在 reCAPTCHA 網站中建立金鑰 (設定正確的網域:應用程式開發人員的 localhost)
- 在 Firebase AppCheck 中貼上 reCAPTCHA 密鑰
- 應用程式狀態應變成綠色
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
檔案,然後加入下列變數,並將預留位置替換為 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 金鑰:從後端伺服器擷取 Google 地圖和 Firebase App Check 的 API 金鑰。
- 初始化 Firebase:設定 Firebase 以進行驗證和安全防護。(取代設定 → 請參閱第 4 章)。
Firebase App Check 權杖的有效期限長度介於 30 分鐘到 7 天之間,可在 Firebase 控制台中設定,無法透過強制權杖重新整理來變更。
- 啟用 App Check:啟用 Firebase App Check,以便驗證傳入要求的真實性。
- 載入 Google 地圖 API:動態載入 Google 地圖 JavaScript 程式庫,以便顯示地圖。
- 初始化地圖:建立以預設位置為中心的 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
- 從
.env
檔案載入環境變數 (API 金鑰、Google 專案 ID)。 - 啟動伺服器,監聽
http://localhost:3000
上的要求。 - 使用應用程式預設憑證 (ADC) 初始化 Firebase Admin SDK。
- 從
script.js
接收 reCAPTCHA 權杖。 - 驗證收到的權杖是否有效。
- 如果符記有效,就會向 Google Places API 發出 POST 要求,並附上搜尋參數。
- 將 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 Nearby Search 要求的回應,請參閱伺服器記錄。 |
疑難排解:
請確認 Google 專案 ID 在設定中保持一致:
- 在 .env 檔案中 (GOOGLE_CLOUD_PROJECT 變數)
- 在終端機 gcloud 設定中:
gcloud config set project your-project-id
- 在 reCAPTCHA 設定中
- 在 Firebase 設定中
其他
- 建立偵錯符記,以便在
script.js
中取代 reCAPTCHA 網站金鑰,用於測試和疑難排解。
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
- 確認 server.js 已載入 Firebase 程式庫
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,然後在終端機中執行:
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 權杖最多為 1 小時),一般工作階段的生命週期較長 (reCAPTCHA 權杖:30 分鐘至 7 小時)。
- 探索 reCAPTCHA 的替代方案:調查 DeviceCheck (iOS)、SafetyNet (Android) 或 App Attest 是否符合您的安全需求。
整合 Firebase 產品:
- 即時資料庫或 Firestore:如果應用程式需要即時資料同步處理或離線功能,請整合即時資料庫或 Firestore。
- Cloud Storage:使用 Cloud Storage 儲存及提供使用者原創內容,例如圖片或影片。
- 驗證:利用 Firebase 驗證功能建立使用者帳戶、管理登入工作階段,以及處理密碼重設作業。
擴展至行動裝置:
- Android 和 iOS:如果您打算推出行動應用程式,請為 Android 和 iOS 平台建立版本。
- Firebase SDK:使用 Android 和 iOS 版 Firebase SDK,將 Firebase 功能無縫整合至行動應用程式。