Firebase 앱 체크 및 reCAPTCHA로 Places API 요청 확인

1. 시작하기 전에

웹 애플리케이션과 상호작용하는 사용자의 적법성을 보장하기 위해 reCAPTCHA JWT 토큰을 활용하여 사용자 세션을 확인하는 Firebase 앱 체크를 구현합니다. 이 설정을 사용하면 클라이언트 애플리케이션에서 Places API (신규)에 대한 요청을 안전하게 처리할 수 있습니다.

b40cfddb731786fa.png

게시 링크

빌드할 항목

이를 보여주기 위해 로드 시 지도를 표시하는 웹 앱을 만들어 보겠습니다. 또한 Firebase SDK를 사용하여 reCAPTCHA 토큰을 눈에 띄지 않게 생성합니다. 그러면 이 토큰이 Node.js 서버로 전송되며, Firebase는 Places API에 대한 요청을 처리하기 전에 이 토큰을 검증합니다.

토큰이 유효하면 Firebase 앱 체크에서 만료될 때까지 토큰을 저장하므로 모든 클라이언트 요청에 대해 새 토큰을 만들 필요가 없습니다. 토큰이 유효하지 않으면 사용자에게 reCAPTCHA 인증을 다시 완료하여 새 토큰을 받으라는 메시지가 표시됩니다.

2. 기본 요건

이 Codelab을 완료하려면 아래 항목을 숙지해야 합니다. daea823b6bc38b67.png

필수 Google Cloud 제품

  • Google Cloud Firebase 앱 체크: 토큰 관리를 위한 데이터베이스
  • Google reCAPTCHA: 토큰 생성 및 확인 웹사이트에서 사람과 봇을 구분하는 데 사용되는 도구입니다. 사용자 행동, 브라우저 속성, 네트워크 정보를 분석하여 사용자가 봇일 가능성을 나타내는 점수를 생성합니다. 점수가 충분히 높으면 사용자는 인간으로 간주되며 추가 조치가 필요하지 않습니다. 점수가 낮은 경우 사용자의 신원을 확인하기 위해 CAPTCHA 퍼즐이 표시될 수 있습니다. 이 접근 방식은 기존 CAPTCHA 방식보다 사용자에게 방해가 덜 되므로 사용자 환경이 더 원활해집니다.
  • (선택사항) Google Cloud App Engine: 배포 환경

필수 Google Maps Platform 제품

이 Codelab에서는 다음 Google Maps Platform 제품을 사용합니다.

이 Codelab의 기타 요구사항

이 Codelab을 완료하려면 다음 계정, 서비스, 도구가 필요합니다.

  • 결제가 사용 설정된 Google Cloud Platform 계정
  • Maps JavaScript API 및 Places가 사용 설정된 Google Maps Platform API 키
  • 자바스크립트, HTML 및 CSS에 대한 기본 지식
  • Node.js에 대한 기본 지식
  • 원하는 텍스트 편집기 또는 IDE

3. 설정하기

Google Maps Platform 설정하기

Google Cloud Platform 계정과 결제가 사용 설정된 프로젝트가 없는 경우 Google Maps Platform 시작하기 가이드를 참고하여 결제 계정 및 프로젝트를 만드세요.

  1. Cloud Console에서 프로젝트 드롭다운 메뉴를 클릭하고 이 Codelab에 사용할 프로젝트를 선택합니다.

e7ffad81d93745cd.png

  1. Google Cloud Marketplace에서 이 Codelab에 필요한 Google Maps Platform API 및 SDK를 사용 설정합니다. 이렇게 하려면 이 동영상 또는 이 문서에 나와 있는 단계를 따르세요.
  2. 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
  • 루트에 Google Cloud 프로젝트 변수를 지정하는 .env 파일을 만듭니다.
GOOGLE_CLOUD_PROJECT="your-project-id"

서비스 계정 만들기

사용자 인증 정보

  • 생성된 서비스 계정을 클릭합니다.
  • KEYS 탭으로 이동하여 키 만들기 > JSON > 다운로드한 JSON 사용자 인증 정보를 저장합니다. 자동으로 다운로드된 xxx.json 파일을 루트 폴더로 이동합니다.
  • (다음 장) nodejs 파일 server.js (​​firebase-credentials.json)에 올바르게 이름을 지정합니다.

4. Firebase AppCheck 통합

Firebase 구성 세부정보와 reCAPTCHA 보안 비밀 키를 가져옵니다.

데모 애플리케이션에 붙여넣고 서버를 시작합니다.

Firebase에서 애플리케이션 만들기

이미 생성된 Google Cloud 프로젝트를 선택합니다('상위 리소스 선택'을 지정해야 할 수 있음).

a6d171c6d7e98087.png a16010ba102cc90b.png

  • 왼쪽 상단 메뉴 (톱니바퀴)에서 애플리케이션 추가

18e5a7993ad9ea53.png 4632158304652118.png

Firebase 초기화 코드

  • Firebase 초기화 코드 저장을 클라이언트 측의 script.js (다음 장)에 붙여넣습니다.

f10dcf6f5027e9f0.png

  • Firebase에서 reCAPTCHA v3 토큰을 사용할 수 있도록 앱 등록

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

da7efe203ce4142c.png

  • reCAPTCHA 선택 → reCAPTCHA 웹사이트에서 키 만들기 (올바른 도메인 구성: 앱 개발의 경우 localhost)

b47eab131617467.png e6bddef9d5cf5460.png

  • Firebase AppCheck에 reCAPTCHA 보안 비밀 붙여넣기

a63bbd533a1b5437.png

  • 앱 상태가 녹색으로 바뀌어야 합니다.

4f7962b527b78ee5.png

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 앱 체크의 API 키를 가져옵니다.
  • Firebase 초기화: 인증 및 보안을 위해 Firebase를 설정합니다. 구성 교체(4장 참고)

Firebase 앱 체크 토큰의 유효 기간(30분~7일)은 Firebase Console에서 구성되며 토큰 새로고침을 강제로 시도하여 변경할 수 없습니다.

  • 앱 체크 활성화: Firebase 앱 체크가 수신 요청의 진위를 확인하도록 사용 설정합니다.
  • 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 설정에서

e6bddef9d5cf5460.png

  • Firebase 설정에서

7e17bfbcb8007763.png

기타

  • 테스트 및 문제 해결을 위해 script.js 내에서 reCAPTCHA 사이트 키 대신 사용할 수 있는 디버그 토큰을 만듭니다.

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);
}
  • 잘못된 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');
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 기능을 모바일 앱에 원활하게 통합합니다.