Firebase 로 사용자 인증이 포함된 실시간 데이터베이스 구축하기

오현재·2024년 11월 8일

이 글에선 Firebase 에서 Authentication (인증) 기능 과 Realtime Database 를 활용하여, 사용자를 식별하고 권한에 따라 데이터에 접근할 수 있는 기능을 구현하는 과정을 다루고 있습니다.

Authentication (인증)

Firebase 인증 기능 은 다양한 앱(IOS, Android, Web, Flutter, C++, Unity) 에 대해 사용자 인증 시 필요한 백엔드 서비스 와 SDK, 기성 UI 라이브러리를 제공한다. 또한, 다양한 인증 방식(비밀번호, 전화번호, ID 공급업체(e.g. Google, Facebook, Twitter 등)) 를 제공한다.

그래서 앱의 환경 과 어떤 인증 방식을 사용할 것인지에 따라 인증 시스템을 구축하는 방법은 매우 다양해 진다. 추가적으로 나는 이 글 에서, Web 앱 에서 ID 공급업체 중 Google 계정을 활용하여 인증 기능을 사용해 볼 것이다.

인증 기능을 사용하는 2가지 방법

Firebase 로 인증 기능을 사용할 수 있는 방법은 다양한 인증 방식에 따라 구체적인 방법에는 더 다양해질 수 있지만, 큰 틀에선 2가지로 나눌 수 있다.

삽입형 솔루션으로써 가장 쉽고 빠르게 앱에 Firebase 인증 시스템을 구축할 수 있는 방법인 FirebaseUI 와 자체 인증 과정을 통해 앱의 로그인 환경을 더 세밀하게 제어하게 할 수 있게 하는 방법인 Firebase SDK 가 있다.

하지만 FirebaseUI 에는 큰 단점이 하나 있는데, Firebase v9 의 모듈식 API 와 호환되지 않는 다는 것이다. FirebaseUI 문서 를 살펴보면 Firebase API 를 네임스페이스 식으로 가져오는 것을 확인할 수 있다.

소스 파일 내에서 다음 모듈을 require 처리합니다.

var firebase = require('firebase');
var firebaseui = require('firebaseui');

하지만 v9 이상 부터는 다음과 같이 모듈식 API 를 사용한다.

// v9 모듈식 API 사용 예

import { getAuth, onAuthStateChanged } from "firebase/auth";

const auth = getAuth(firebaseApp);
onAuthStateChanged(auth, user => {
  // Check for user status
});

따라서, 앞으로 업데이트 될 Firebase 의 최신 기능을 지속적으로 사용하지 못하게 되므로 앱의 인증 기능으로 FirebaseUI 를 사용하는 것은 좋지 않은 선택일 수 있다.

FirebaseUI는 현재 v9 모듈형 SDK 와 호환되지 않습니다v9 호환성 레이어(특히 app-compat 및 auth-compat 패키지)에서는 v9와 함께 FirebaseUI 사용을 허용하지만 앱 크기 감소 등 v9 SDK의 이점을 누릴 수 없습니다.

그래서 결과적으로 이 글에선, Web 앱에서 Google 계정을 사용하고, Firebase SDK 를 활용하여 인증 시스템을 구축해보겠다.

Firebase SDK 로 인증 과정 처리

Google 계정을 사용한 인증 방식의 구체적인 방법에는 2가지 방법이 있다. (앞서 얘기한 Firebase SDK 와 FirebaseUI 2가지 방식은 큰 틀에서의 전반적인 방식에 관한 것이다.)

  1. Firebase SDK 사용하여 Google 로그인 프로세스 진행
  2. Google 의 라이브러리를 사용하여 로그인을 진행하고, 그로부터 얻은 ID 토큰 을 통해 로그인

2.의 경우 수동으로 로그인 과정을 처리하는 것이고, 실제로 문서를 보면 1.의 방식이 훨씬 간편하다는 것을 확인 할 수 있다. 나또한 1.이 간편하다고 느껴 1.의 방법으로 진행했다.

웹 앱을 빌드하는 경우, Google 계정을 통해 Firebase에 사용자를 인증하는 가장 쉬운 방법은 Firebase 자바스크립트 SDK로 로그인 과정을 처리하는 것입니다.

Firebase 의 인증 관련 메서드를 제공하는 firebase/auth 모듈에서 필요한 메서드들을 import 한다. 인증 관련되어 필요한 메서드는 2개이다.

getAuth Firebase 인증 시스템을 사용하기 위한 Auth 인스턴스를 제공하거나 생성하는 역할을 한다. 만약 이미 생성된 인스턴스가 있다면 그것을 반환하고, 없다면 새로운 인스턴스를 생성하여 반환한다.

signInWithPopuppop-up 형태의 구글 로그인을 진행하며, 성공 콜백인자로 인증된 사용자의 정보를 포함하는 result 를 전달한다. (이 사용자의 정보는 이후 Firebase 에서 실시간 데이터베이스 보안 에 필요한 정보도 담고 있다.)

만일 redirection 형태의 ****구글 로그인을 구현하고 싶다면 signInWithRedirect 를 호출하면 된다.

import { getAuth, signInWithPopup } from "firebase/auth";

const auth = getAuth();
signInWithPopup(auth, provider)
  .then((result) => {
	  // 인증된 사용자
	  const user = result.user;
	  // 이후 fireabse 에서 사용하게될 실시간 데이터베이스 보안 에 필요한 유저의 고유한 id
	  const uid = user.uid;
  }).catch((error) => {
    // Handle Errors here.
  });

위의 코드로 간단하게 인증 과정 처리는 끝났다. 그렇다면 인증된 사용자의 정보를 가져오는 것은 어떻게 할 수 있을까?

인증된 사용자 정보 가져오기

현재 사용자를 가져올 때 권장하는 방법은 다음과 같이 Auth 객체에 관찰자를 설정하는 것입니다.

onAuthStateChanged 의 콜백 함수의 인자인 user 에서 현재 인증된 사용자의 정보를 가져올 수 있다.

import { getAuth, onAuthStateChanged } from "firebase/auth";

const auth = getAuth();
onAuthStateChanged(auth, (user) => {
  if (user) {
		// user 는 인증된 사용자
    const uid = user.uid;
    // ...
  } else {
    // User is signed out
    // ...
  }
});

혹은 앞서 얘기한 getAuth 의 반환값중 currentUser 라는 속성을 통해 확인할 수 있다. getAuth 는 이미 생성된 인스턴스가 있다면 그것을 반환한다. 만일 현재 유효한 인증된 사용자가 있다면 getAuth().currentUser 는 사용자의 정보를, 없다면 null 을 반환한다.

import { getAuth } from "firebase/auth";

const auth = getAuth();
// user 는 인증된 사용자
const user = auth.currentUser;

이제 앱에서는 인증된 사용자 정보를 언제든 확인할 수 있게 되었다. 이 인증 상태를 언제까지 지속할수 있느냐인 지속성 또한 설정할 수 있다.

인증 상태의 지속성

Firebase SDK를 사용하면 인증 상태를 유지하는 방식을 지정할 수 있습니다. 로그인한 사용자가 명시적으로 로그아웃할 때까지 무기한 유지할지, 창을 닫으면 상태를 삭제할지, 아니면 페이지 새로고침 시 삭제할지 지정할 수 있습니다.
웹 애플리케이션의 경우 기본 동작은 사용자가 브라우저를 닫은 후에도 사용자의 세션을 유지하는 것입니다.

하지만 만약 민감한 데이터를 사용하거나, 여러 사용자가 같은 기기에서 사용하는 환경의 앱의 경우, 브라우저를 닫은 후에도 사용자의 세션을 유지하는 기본 동작이 적합하지 않을 수 있다. 이러한 경우에는 인증 상태 지속성을 지정할 수 있다.

인증 상태 지속성의 유형은 다음과 같다.

유형

유형설명
firebase.auth.Auth.Persistence.LOCAL'local'브라우저 창이 닫힌 후에도, 상태가 유지. 이 상태를 삭제하려면 명시적으로 로그아웃 해야함.
firebase.auth.Auth.Persistence.SESSION'session'현재의 세션이나 탭에서만 상태가 유지되며 사용자가 인증된 탭이나 창이 닫히면 삭제됨. 웹 앱에만 적용된다.
firebase.auth.Auth.Persistence.NONE'none'상태가 메모리에만 저장되며 창이나 활동이 새로고침되면 삭제됨.

수정

firebase.auth().setPersistence 를 호출하여 기존의 지속성 유형을 지정하거나 수정할 수 있다.

setPersistence 를 사용할 때 유의할 점은, setPersistence 가 반환하는 Promise 성공 콜백함수에서 인증 과정 처리 메서드 를 실행 시켜야 한다는 것이다. 그렇게 되면 해당 인증 과정을 처리한 후 인증된 사용자의 상태 지속성을 지정하거나 수정할 수 있게 된다.

앞선 항목인 Firebase SDK 로 인증 과정 처리 에서 사용했던 예제를 결합시켜 다음과 같이 사용됨을 확인 할 수 있다.

import { getAuth, setPersistence, signInWithPopup, browserLocalPersistence } 
from "firebase/auth";

const auth = getAuth();
setPersistence(auth, browserLocalPersistence)
  .then(() => {
	  // 인증 과정 처리
    return signInWithPopup(auth, email, password);
  })
  .catch((error) => {
    // Handle Errors here.
  });

또 하나는 setPersistence 의 두번째 인자로 앞서 얘기했던 인증 상태 지속성의 유형 에 따라 적합한 값을 보내야한다는 것이다.

이는 상수 형태이며, Persistence 라는 인터페이스 가 할당되어있다.

Persistence 에는 type 이라는 속성이 있으며, 이에서 앞서 확인한 인증 상태 지속성의 유형 을 확인 할 수 있다.

export declare interface Persistence {
    /**
     * Type of Persistence.
     * - 'SESSION' is used for temporary persistence such as `sessionStorage`.
     * - 'LOCAL' is used for long term persistence such as `localStorage` or `IndexedDB`.
     * - 'NONE' is used for in-memory, or no persistence.
     */
    readonly type: 'SESSION' | 'LOCAL' | 'NONE';
}

각 타입에 따라 다음의 상수를 사용한다.

LOCAL : browserLocalPersistence

SESSION : browserSessionPersistence

NONE : inMemoryPersistence

다음 예제를 통해 최종적으로 모든 인증 상태 지속성의 유형 에따라 인증 과정 처리를 구현하는 방식을 확인할 수 있다.

import { getAuth, setPersistence, signInWithPopup, browserLocalPersistence, browserSessionPersistence, inMemoryPersistence } from "firebase/auth";

const auth = getAuth();

// LOCAL
const localPersistenceLogin = setPersistence(auth, browserLocalPersistence)
  .then(() => {
    return signInWithPopup(auth, email, password);
  })
  .catch((error) => {
    // Handle Errors here.
  });
  
// SESSION
const sessionPersistenceLogin = setPersistence(auth, browserSessionPersistence)
  .then(() => {
    return signInWithPopup(auth, email, password);
  })
  .catch((error) => {
    // Handle Errors here.
  });
 
// NONE
const inMemoryPersistenceLogin = setPersistence(auth, inMemoryPersistence)
  .then(() => {
    return signInWithPopup(auth, email, password);
  })
  .catch((error) => {
    // Handle Errors here.
  });

Realtime Database

Firebase 의 Realtime Database 는 NoSQL 클라우드 호스팅 데이터베이스로, Firebase 에서 제공하는 클라우드 데이터베이스 서비스 중 하나이다. Realtime Database 는 모든 클라이언트에서 실시간으로 JSON 형태의 데이터가 동기화되고 앱이 오프라인 일 때도 데이터를 사용할 수 있게 한다.

다양한 방식으로 Realtime Database 를 사용할 수 있지만, Firebase SDK 를 사용해야 하는 다른 기능과 달리, Realtime Database 에서는 REST 요청으로도 사용할 수 있다.

클라이언트 에서 Firebase SDK 를 사용할 수 없거나, 데이터베이스 연결에 따르는 오버헤드를 피하는 경우에서 사용할 수 있는 이점이있다. 따라서, 이 글에선 REST 요청 방식을 다루어보겠다.

구조

Realtime Database 는 JSON 트리형태의 구조를 가진다. 즉, 모든 Realtime Database 데이터는 JSON 객체로 저장된다. SQL 데이터베이스와 달리 테이블이나 레코드가 없으며, JSON 트리에 추가된 데이터는 연결된 키를 갖는 기존 JSON 구조의 노드가 된다. 사용자 ID 또는 의미 있는 이름과 같은 고유 키로 직접 지정할 수도 있고,POST요청을 사용하여 자동으로 지정할 수도 있다.

예를 들어, 사용자의 이름과 생일을 저장하는 데이터베이스가 있다고 치자. 데이터베이스 의 구조는 다음과 같을 것 이다.

{
  "users": {
    "kinght95": {
      "name": "hyunjae",
      "birthday": "Sep 26, 1995"
    },
    "ghost1101": {
      "name": "junhye",
      "birthday": "Feb 01, 1995"
    },
  }
}

REST 요청하기

REST 요청의 엔드포인트는 앞서 살펴본 Realtime Database 의 경로(혹은 URL), 그 자체를 표현한다.

my-project 라는 이름의 Firebase 프로젝트 를 생성했다고 치자. 그렇다면 애플리케이션의 모든 데이터는 Firebase 데이터베이스 URL https://my-examples.firebaseio.com/ 의 경로에 저장된다.

우리는 users 라는 경로에 새로운 유저 데이터를 추가하고, logs 라는 경로에 유저가 행한 주요한 이벤트 데이터를 추가해볼 것이다. 예를 들어, users 경로에 보낼 요청의 엔드포인트는 https://my-examples.firebaseio.com/users.json 이 된다.

데이터 쓰기

PUT : 데이터 쓰기 작업에서 기본작업 에 해당된다. 데이터를 쓰거나 대체 한다.

PATCH : 정의된 경로에서 모든 데이터를 대체할 필요 없이 일부 키를 업데이트 한다.

POST : 새로운 데이터를 데이터 목록에 추가 한다. 고유한 타임스탬프 기반 키를 자동으로 생성한다.


PUTPOST 의 주요한 차이점은 POST 요청은 데이터 목록에 추가될 때, 고유한 키(타임스탬프) 를 자동으로 생성한다는 점이다.

새로운 유저 를 추가할 때, 유저 아이디 를 기준으로 한다고치자. 이때, 유저 아이디데이터의 고유한 키가 된다. 예를 들어, 새로운 유저 에 knight95 라는 유저 아이디, 즉 고유한 키를 포함한다고 치자. 이런 경우, 이미 데이터에 고유한 키가 포함되어있기 때문에, 키를 자동으로 생성하는 POST 가 아닌 PUT 을 사용하는 것이 더 적합하다.

그렇다면 다음과 같이 PUT 을 통해 knight95 라는 유저 아이디를 가진 데이터를 추가하는 REST 요청을 보내보자.

curl -X PUT -d '{
	"knight95": {
    "name": "hyunjae",
    "birthday": "Sep 26, 1995"
	 }
}' 'https://my-app.firebaseio.com/users.json'

위와 같이 요청을 보내면 자동으로 users 경로의 하위로 맵핑 되어 데이터가 생성 된다.
혹은, 다음과 같이 직접 경로를 지정하여 요청을 보낼수 있다. 이 두 요청은 결과적으로 같은 구조의 데이터베이스를 생성한다.

curl -X PUT -d '{
    "name": "hyunjae",
    "birthday": "Sep 26, 1995"
	 }' 'https://my-app.firebaseio.com/users/knight95.json'

이렇게 데이터를 추가했다면, 이제 데이터베이스 구조는 다음과 같을 것이다.

{
  "users": {
    "kinght95": {
      "name": "hyunjae",
      "birthday": "Sep 26, 1995"
    },
  },
  "logs": {
  }
}

하지만, 유저 아이디를 포함한 새로운 유저에 대한 데이터와 달리, 사용자의 불특정한 이벤트를 저장하는 요청이라면, 키를 자동으로 생성하는 POST 가 더 적합할 수 있다.

curl -X POST -d '{
  "user": "knight95",
  "event": "killed boss"
}' 'https://my-app.firebaseio.com/logs.json'

이렇게 데이터를 추가했다면, 이제 데이터베이스 구조는 다음과 같을 것이다. 고유한 타임스탬프 기반 키가 자동으로 생성 됨을 확인할 수 있다.

{
  "users": {
    "kinght95": {
      "name": "hyunjae",
      "birthday": "Sep 26, 1995"
    },
  },
  "logs": {
	   "-JSOpn9ZC54A4P4RoqVa": {
      "user": "kinght95",
      "event": "killed boss"
    }
  }
}

데이터 읽기

user 의 데이터에 접근하기 위한 HTTP 요청은 다음과 같다. 앞서 말했듯, JSON 형태로 구성된 데이터베이스의 구조를 그대로 표현한다.

curl https://my-project.firebaseio.com/users.json

데이터에 접근하려는 사용자에 대한 인증

앞서 REST 요청으로 Realtime Database 에 접근하는 방법을 알아보았다.

여기서 주의할 점은, 아무런 권한이 없거나, 식별되지 않은 사용자가 데이터에 접근할 수 있어서는 안된다는 것이다. 그렇기에, 데이터에 대해 접근하려는, 즉 읽고 쓰려는 사용자에 대한 인증은 필수적이다.

그 첫번째 단계로, 사용자를 식별하기위해 사용한 것이 Firebase 인증 기능이었다.

즉, 우리는 데이터베이스(Realtime Database) 에 접근하는 사용자에 대한 인증의 과정이 다음과 같음을 알 수 있다.

  1. 사용자를 식별(인증) 하고
  2. 이 사용자가 데이터에 대한 액세스 권한을 가지고 있는 지를 규칙 으로 판별(인증) 하는 것

그렇다면 규칙 이라는 것은 무엇일까? 어떤 사용자 가 데이터에 접근할 수 있고, 데이터를 수정할 수 있느냐 를 지정하는 규칙 정도로 설명할 수 있을 것이다.

이것을 Firebase 에서는 보안 규칙 이라는 것으로 정의하여 사용한다.

보안 규칙

보안 규칙 은 Firebase 에서 제공하는 데이터를 관리하는 기능(Cloud Firestore, Realtime Database, Cloud Storage) 에 대해, 데이터를 악의적인 사용자로부터 보호하기 위해 제공되는 기능이다.

모든 읽기/쓰기 요청은 이 규칙에 따라 허용될 때만 완료된다. 우리는 이러한 보안 규칙 의 설정을 통해 데이터에 대한 액세스 제어와, 데이터의 구조 및 색인 생성 여부를 결정할 수 있다.

Firebase 의 각각의 클라우드 데이터베이스 기능 에서 사용되는 보안 규칙은 조금씩 차이가 있으며, 다음 항목에서 Realtime Database 의 보안 규칙에 대해 자세히 알아보겠다.

Realtime Database 의 보안 규칙

💡 Realtime Database 보안 규칙 은 Firebase Console 을 통해 자신이 생성한 프로젝트 에서, Realtime Database규칙 탭에서 설정 할 수 있다.

규칙 유형

Realtime Database 보안 규칙 유형에는 4가지가 있으며 다음과 같다.

규칙 유형
.read사용자가 데이터를 읽을 수 있는 조건.
.write사용자가 데이터를 쓸 수 있는 조건.
.validate값의 올바른 형식, 하위 속성을 갖는지 여부 및 데이터 유형.
.indexOn정렬 및 쿼리를 위해 색인을 생성할 하위 항목을 지정.

구조

보안 규칙은 Realtime Database 에서는 JSON 구조를 가진다. Cloud Firestore, Cloud Storage 에서는 고유한 언어인 CEL 이라는 언어를 사용한다.

Realtime Database 보안 규칙 의 기본 구조는 다음과 같다.

{
  "rules": {
    "<경로>": {
    //  Allow the request if the condition for each method is true.
      ".read(<요청>)": <조건>,
      ".write(<요청>)": <조건>,
      ".validate(<요청>)": <조건>
    }
  }
}

<> 로 표시된 각 요소에 대해 살펴보겠다.

경로 : 보안규칙이 적용될 데이터베이스 의 경로 를 의미한다. 앞서 살펴본 데이터베이스의 JSON 구조를 반영한다.

요청 : 규칙에서 액세스 를 허용하기 위해 사용하는 메서드 를 뜻한다. 앞서 설명한 규칙 유형에 해당 된다고 볼 수 있다. read 및 write 규칙은 광범위한 읽기 및 쓰기 액세스를 허용하지만 validate 규칙은 수신되는 데이터 또는 기존 데이터를 기반으로 액세스 를 허용하는 2차 검증으로 작용한다.

조건: true 로 평가되면 요청 을 허용하는 조건 을 의미한다.

보안 규칙이 경로에 적용되는 방법

상위 노드에 있는 규칙은 하위 노드에 있는 규칙보다 우선시 된다. 따라서, 하위 노드의 규칙은 상위 경로(데이터) 에 액세스를 허용할 수 없다. 즉, 상위 경로 중 하나에 이미 액세스가 허용된 경우 데이터베이스 구조의 하위 경로에 대한 액세스를 세분화하거나 취소할 수 없다.

예를 들어 다음 보안 규칙 을 살펴보자.

{
  "rules": {
     "foo": {
        // /foo/* 경로 를 읽는 것에 대해 허용
        ".read": true
        "bar": {
          // foo 노드에서 이미 읽는 것에 허용 했기 에 무시됨
          ".read": false
        }
     }
  }
}

위의 보안 규칙에서 상위 노드는 foo 에, 하위 노드는 bar 에 해당한다. foo 에서 /foo/* 경로에 대한 읽기 액세스를 허용했고, 하위 노드인 /bar 에서 경로 /foo/bar/* 에 대한 읽기 액세스를 거부하려한다. 하지만 이는 무시되며, /foo/bar/* 경로 의 읽기 액세스는 허용되게 된다.

위치 변수

앞서 확인한 경로 를 고정된 문자열이 아닌 변수로써 사용할 수 있다. 이는 위치 변수 라고 하기도 한다. $ 프리픽스를 사용한다.

{
  "rules": {
    "users" : {
	    "$userId": {
	      ".read": "<조건>",
	    }
    }
  }
}

구조, 위치변수 등에 알아 보았으니 앞서 살펴본 Realtime Database 의 구조 에서 들은 예시에 보안규칙을 적용해보겠다. 다음과 같이 생성해볼 수 있을 것이다.

예시

{
	"rules": {
		{
		  "users": {
		    "$userId": {
						".read": true,
						".write": false
		    },
		  },
		  "logs": {
			   "$logId": {
			      ".read": false,
			      ".write": false
		    }
		  }
		}
	}
}

보안규칙으로 사용자에 따른 데이터 접근 제어하기

가장 일반적인 보안 규칙 패턴 중 하나는 바로 우리에게 필요한 사용자의 인증 상태에 따라 데이터에 대한 접근을 제어하는 것이다.

Firebase 인증 기능과의 통합

Firebase 인증 은 Firebase Realtime Database 의 보안규칙과 결합되어, 조건을 사용하여 사용자별 데이터 접근을 제어하는 기능을 제공한다. 인증 기능을 통해 사용자가 인증 되면 Realtime Database 보안 규칙 의 auth 변수라는 것에 인증된 사용자의 정보가 채워진다.

이렇게 각각의 기능의 두 개념을 결합하여 결과적으로,

1. 인증 기능에서 사용자를 식별하고 고유한 ID(uid) 를 할당
2. 고유한 ID 는 auth 변수 의 속성인 uid 에 할당되어, 보안규칙에서 접근이 가능하게 되고, <조건> 으로 사용되어 데이터 접근을 제어하는데 사용

할 수 있는 프로세스가 생성되는 것이다.

auth 변수

auth 변수 라는 것은 앞서 얘기했 듯, 인증 기능을 사용하면 채워지는 변수이다.

uid고유 사용자 ID (제공업체에 관계없이 항상 고유함)
tokenFirebase 인증 ID 토큰의 콘텐츠

auth 변수는 인증이 완료되기 전에는 null 이다. 즉, Firebase 인증을 통해 사용자가 인증이 되어야, auth 변수에 위의 속성들이 포함되는 것이다. 위의 속성들은 auth.uid, auth.token 과 같은 형태로 앞서 얘기했 듯, 보안규칙의 <조건> 내에서 사용될 수 있다.

그렇다면, 보안규칙 예시 항목에서 작성했던 보안규칙을 다음과 같이 변경해보며 확인해보자.

{
  "rules": {
    "users": {
      "$userId": {
        ".write": "$userId === auth.uid"
      }
    }
  }
}

auth 변수의 uid 속성으로 위치 변수인 $userId 와 일치 여부에 따라, 데이터에 대한 접근을 제어하고 있는 것을 확인할 수 있다.

인증 기능을 사용할 때의 보안규칙 패턴

보통은 규칙을 작성하기 쉬운 방향으로 데이터베이스를 구조화하는 것이 도움이 된다. 이유는 앞서 살펴 보았듯, 데이터베이스 구조의 경로자체가 보안규칙에 포함되기 때문이다.

따라서, Realtime Database에 사용자 데이터를 저장하는 일반적인 패턴 중 하나는 모든 노드를 그들의 uid 값으로 저장하는 것이다. 예를 들면 다음과 같을 것이다.

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth !== null && auth.uid === $uid"
      }
    }
  }
}

인증된 REST 요청하기

앞서 Realtime Database 에서 REST 요청하는 방법과, 보안규칙을 생성하고, auth 변수에 따라 접근을 제어하는 방법을 알아보았다.

이제 최종적으로 인증된 REST 를 요청하는 방법에 대해 알아보겠다. 방법 자체는 아주 간단하다. 다음 두가지 방법을 통해 인증된 REST 요청을 할 수 있다.

Google OAuth2 액세스 토큰

Realtime Database 의 REST API 는 표준 Google OAuth2 Access Token 을 허용한다. OAuth2 Access Token 은 앞서 진행한 인증 의 Google 로그인 과정에서 얻을 수 있다.

import { getAuth, signInWithPopup, GoogleAuthProvider } from "firebase/auth";

const auth = getAuth();
signInWithPopup(auth, provider)
  .then((result) => {
		// Google OAuth2 Access Token
    const token = GoogleAuthProvider.credentialFromResult(result).accessToken;

  }).catch((error) => {
    // Handle Errors here.
  });

이 OAuth2 Access Token 을 호출하는 쿼리 에 access_token 이라는 파라미터를 추가해 삽입하거나, headersAuthorization 속성에 담아 보낼수 있다. 쿼리 파라미터 로 보낼때의 주의할 점은 꼭 파라미터명을 access_token 로 지정해야한다는 것이다.

/* ... */
await fetch({
  method: "PUT",
  param: `users/${auth.user.uid}.json?access_token=${token}`,
  body : {
    someData: "some data"
  }
});
/* ... */

// or

/* ... */
await fetch({
  method: "PUT",
  param: `users/${auth.user.uid}.json`,
  body : {
    someData: "some data"
  },
  headers:{
   Authorization: `Bearer ${token}`
  }
});
/* ... */

Firebase ID 토큰

ID 토큰 은 Firebase 인증을 사용하여 로그인하면 발급되는 고유하게 식별가능한 토큰이다. 이전 과정에서 지속적으로 등장했던 uid 가 이에 해당된다. 즉, 이 토큰을 사용하면 앞서 살펴보았던 auth 변수 를 통해 보안규칙에서 걸러질 것이다.

ID 토큰signInWithPopup 의 성공 콜백 인자 에서 얻을 수 있다.

import { getAuth, signInWithPopup, GoogleAuthProvider } from "firebase/auth";

const auth = getAuth();
signInWithPopup(auth, provider)
  .then(async (result) => {
	  // 인증된 사용자
	  const user = result.user;
	  // Firebase ID Token
    const idToken = await user.getIdToken();
  }).catch((error) => {
    // Handle Errors here.
  });

혹은, Firebase.Auth.currentUser 에서 가져올 수 있다.

firebase.auth().currentUser.getIdToken(/* forceRefresh */ true)
.then(function(idToken) {
  // Send token to your backend via HTTPS
  // ...
}).catch(function(error) {
  // Handle error
});

이제 이 ID 토큰 을 쿼리 파라미터로 전달하면 된다. 이 역시 주의할 점은 꼭 쿼리 파라미터명을 auth 로 지정해야 한다는 점이다.

/* ... */
await fetch({
  method: "GET",
  param: `users/${auth.user.uid}.json?auth=${idToken}`,
});
/* ... */

이렇게 Firebase 의 인증 기능과 Realtime Database 를 사용하면, 사용자를 식별하고, 사용자에 따라 접근이 제어되는 데이터베이스를 구축하는 것을 비교적 쉽게 할 수 있다.

profile
안녕하세요. 환영합니다. 프론트엔드 개발자 오현재입니다.

0개의 댓글