1. 准备工作
为了确保与您的 Web 应用互动的用户的合法性,您将实现 Firebase App Check,并利用 reCAPTCHA JWT 令牌验证用户会话。通过此设置,您可以安全地处理客户端应用向 Places API(新版)发出的请求。
构建内容。
为演示这一点,您将创建一个 Web 应用,该应用会在加载时显示地图。它还会使用 Firebase SDK 谨慎地生成 reCAPTCHA 令牌。然后,此令牌会发送到您的 Node.js 服务器,Firebase 会在验证该令牌后再执行对 Places API 的任何请求。
如果令牌有效,Firebase App Check 会将其存储起来,直到其过期为止,这样就无需为每个客户端请求创建新的令牌。如果令牌无效,系统会提示用户重新完成 reCAPTCHA 验证以获取新的令牌。
2. 前提条件
您必须熟悉以下各项内容,才能完成此 Codelab。
必需的 Google Cloud 产品
- Google Cloud Firebase App Check:用于管理令牌的数据库
- Google reCAPTCHA:令牌创建和验证。人机识别系统是一种工具,用于在网站上区分真人和机器人。该模型的运作方式是分析用户行为、浏览器属性和网络信息,以生成一个得分,表示用户是机器人的可能性。如果得分足够高,系统会将用户视为真人,而无需采取进一步行动。如果得分较低,系统可能会显示 CAPTCHA 图形验证题,以确认用户的身份。与传统的 CAPTCHA 方法相比,这种方法的干扰性更小,可提供更流畅的用户体验。
- (可选)Google Cloud App Engine:部署环境。
所需的 Google Maps Platform 产品
在此 Codelab 中,您将使用以下 Google Maps Platform 产品:
- Maps JavaScript API 已加载并显示在 Web 应用中
- 后端服务器发出的 Places API(新) 请求
完成此 Codelab 的其他要求
若要完成此 Codelab,您需要以下账号、服务和工具:
- 已启用结算功能的 Google Cloud Platform 账号
- 已启用 Maps JavaScript API 和 Places 的 Google Maps Platform API 密钥
- 具备 JavaScript、HTML 和 CSS 方面的基础知识
- Node.js 基础知识
- 您自己喜欢用的文本编辑器或 IDE
3. 进行设置
设置 Google Maps Platform
如果您还没有 Google Cloud Platform 账号,也没有启用了结算功能的项目,请先参阅 Google Maps Platform 使用入门指南创建一个结算账号和一个项目。
- 在 Cloud 控制台中,点击项目下拉菜单,然后选择要用于此 Codelab 的项目。
- 在 Google Cloud Marketplace 中,启用此 Codelab 所需的 Google Maps Platform API 和 SDK。为此,请按照此视频或此文档中的步骤操作。
- 在 Cloud Console 的凭据页面中,生成 API 密钥。您可以按照此视频或此文档中的步骤操作。向 Google Maps Platform 发出的所有请求都需要 API 密钥。
应用默认凭据
您将使用 Firebase Admin SDK 与 Firebase 项目交互,以及向 Places API 发出请求,并且需要提供有效的凭据才能正常运行。
我们将使用 ADC 身份验证(自动默认凭据)对您的服务器进行身份验证,以便发出请求。或者(不推荐),您也可以创建服务账号并在代码中存储凭据。
定义:应用默认凭据 (ADC) 是 Google Cloud 提供的一种机制,可自动对您的应用进行身份验证,而无需显式管理凭据。它会在各种位置(例如环境变量、服务账号文件或 Google Cloud 元数据服务器)中查找凭据,并使用找到的第一个凭据。
- 在终端中,使用以下命令可让您的应用代表当前登录的用户安全地访问 Google Cloud 资源:
gcloud auth application-default login
- 您将在根目录中创建一个 .env 文件,用于指定 Google Cloud 项目变量:
GOOGLE_CLOUD_PROJECT="your-project-id"
备用凭据(不推荐)
创建服务账号
- “Google Maps Platform”标签页 >“+ 创建凭据”> 服务账号
- 添加 Firebase AppCheck Admin 角色,然后输入您刚才输入的服务账号名称,例如:firebase-appcheck-codelab@yourproject.iam.gserviceaccount.com
凭据
- 点击创建的服务账号
- 在“密钥”标签页中依次选择“创建密钥”>“JSON”> 保存下载的 JSON 凭据。将自动下载的 xxx.json 文件移至根文件夹
- (下一章)将其正确命名为 nodejs 文件 server.js (firebase-credentials.json)
4. Firebase AppCheck 集成
您将获得 Firebase 配置详细信息和 reCAPTCHA 密钥。
您将它们粘贴到演示版应用中,然后启动服务器。
在 Firebase 中创建应用
- 前往项目管理中心 https://console.firebase.google.com(找到链接):
SELECT the already created Google Cloud project (you may have to specify: "Selecting the parent resource")"
- 从左上角的“菜单”图标(齿轮)中添加应用
Firebase 初始化代码
- 保存 Firebase 初始化代码,以便在客户端的 script.js 中粘贴(下一章)
- 注册您的应用以允许 Firebase 使用 reCAPTCHA v3 令牌
https://console.firebase.google.com/u/0/project/YOUR_PROJECT/appcheck/apps
- 选择 reCAPTCHA → 在 reCAPTCHA 网站中创建密钥(配置正确的网域:对于应用开发,请使用 localhost)
- 将 reCAPTCHA 密钥粘贴到 Firebase AppCheck 中
- 应用状态应变为绿色
5. 演示应用
- 客户端 Web 应用: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 Maps API:动态加载 Google Maps JavaScript 库以显示地图。
- 初始化地图:创建以默认位置为中心的 Google 地图。
- 处理地图点击:监听地图上的点击并相应地更新中心点。
- 查询 Places API:使用 Firebase App Check 进行授权,向后端 API (
/api/data
) 发送请求,以提取点击的地点附近地点(餐厅、公园、酒吧)的相关信息。 - 显示标记:将提取的数据在地图上以标记的形式绘制出来,并显示其名称和图标。
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 附近搜索请求的响应,请参阅服务器日志。 |
问题排查:
确保设置中的 Google 项目 ID 一致:
- 在 .env 文件中(GOOGLE_CLOUD_PROJECT 变量)
- 在终端 gcloud 配置中:
gcloud config set project your-project-id
- 在 reCAPTCHA 设置中
- 中进行设置
其他
- 创建一个调试令牌,以便在
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 Authentication:
- 默认令牌与自定义令牌:实现 Firebase 自定义令牌,以便更深入地使用 Firebase 服务。
- 令牌生命周期:设置适当的令牌生命周期,对于敏感操作,请设置较短的生命周期(自定义 Firebase 令牌最长为 1 小时),对于常规会话,请设置较长的生命周期(reCAPTCHA 令牌:30 分钟到 7 小时)。
- 探索 reCAPTCHA 的替代方案:调查 DeviceCheck (iOS)、SafetyNet (Android) 或 App Attest 是否适合您的安全需求。
集成 Firebase 产品:
- Realtime Database 或 Firestore:如果您的应用需要实时数据同步或离线功能,请与 Realtime Database 或 Firestore 集成。
- Cloud Storage:使用 Cloud Storage 存储和提供用户生成的内容,例如图片或视频。
- 身份验证:利用 Firebase Authentication 创建用户账号、管理登录会话和处理密码重置。
扩展到移动设备:
- Android 和 iOS:如果您打算开发移动应用,请为 Android 和 iOS 平台分别创建版本。
- Firebase SDK:使用适用于 Android 和 iOS 的 Firebase SDK 将 Firebase 功能无缝集成到您的移动应用中。