<블로그 원문은
여기에서 확인하실 수 있으며, 블로그 번역 리뷰는
김태호(Android GDE)님이 참여해 주셨습니다.>
Firebase에서는 Firebase 콘솔에서 개발자가 생성하는 프로젝트를 통해 구현하는 앱에서 함께 사용할 수 있는 여러 가지 기능을 제공합니다. 보통은 단일 프로젝트에서 제공하는 개발자 앱의 리소스만 전부 확보하면 충분하지만, 단 하나의 앱으로 여러 프로젝트의 데이터에 액세스할 수 있도록 하는 경우도 많습니다. 예를 들어, 두 가지 다른 데이터베이스의 데이터에 액세스하여 각 데이터베이스에 대한 사용자의 액세스를 인증할 수 있어야 할 경우도 있습니다. 이 글에서는 이러한 과정이 어떤 식으로 이루어지는지 보여드리겠습니다.
먼저 몇 가지 용어부터 살펴보겠습니다.
이전 Firebase.com 프로젝트
이전 콘솔에서 생성하여 새로운 콘솔로 업그레이드하지 않은 Firebase 데이터베이스와 연결된 프로젝트입니다.
Google API 프로젝트
대개
https://console.developers.google.com 또는 https://console.cloud.google.com에서 Google API에 액세스하기 위해 사용하는 프로젝트입니다.
Firebase 프로젝트
새로운 Firebase 콘솔에서 생성한 프로젝트입니다. Firebase 프로젝트는 모두 아래에서 설명하는 Google API 프로젝트이기도 합니다.
앱
특정 플랫폼을 위한 클라이언트입니다. 각 프로젝트마다 여러 앱이 연결되어 있을 수 있습니다.
기존 Google API 프로젝트와 함께 업그레이드한 이전 Firebase.com 프로젝트
이 시나리오는 다른 Google API 프로젝트에서 서비스를 사용할 수도 있지만 기존의 이전 Firebase.com 데이터베이스를 새로운 Firebase 프로젝트로 업그레이드하려는 개발자에게 적합합니다. 업그레이드한 이전 프로젝트는 새로운 Firebase 프로젝트가 되는데, 기존 사용자를 위한 Google Sign-In 인증을 제공하는 Google API 프로젝트와 함께 사용해야 합니다.
여기서 문제는 Google Sign-In 구성 요소가 Android에서 제대로 작동해야 하려면 SHA-1(APK 서명에 사용하는 키 지문)과 패키지 이름(예: com.foo.bar)을 앱에 등록해야 한다는 점입니다. Google Sign-In은 이 조합을 통해 특정 앱에서 사용 중인 Google API 프로젝트가 무엇인지 인식할 수 있기 때문입니다. SHA1과 패키지 이름으로 구성되는 쌍은 Google과 Firebase 프로젝트에서 전역적으로 고유하므로, 동일한 SHA-1과 패키지 이름 쌍을 업그레이드한 Firebase 프로젝트에 추가하려고 하면 (Google API 프로젝트에) OAuth2 클라이언트가 이미 존재한다는 오류 메시지가 나타납니다.
경고: 프로덕션 환경에서 이러한 오류가 발생할 경우 앱에 등록된 기존 클라이언트 ID를 삭제하지 마세요! 삭제하면 기존 사용자가 앱을 올바로 사용할 수 없게 되기 때문입니다. 이 경우 올바른 해결책은 Firebase 콘솔에서 업그레이드한 프로젝트에 대해 동일한 패키지 이름으로 새로운 앱을 생성하는 것입니다. 단, SHA1은 포함하지
않아야 합니다.
이제, 평상시와 같이
Firebase 인증을 사용하여 Google Sign In을 구현합니다. 어느 한 시점에서 Google Sign Options 객체를 구성해야 합니다.
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build();
여기서
default_web_client_id 문자열은 ID 토큰의
audience 필드 설정에 사용됩니다. 값은 Google 프로젝트가 아닌 Firebase 프로젝트의 google-services.json 파일에서 가져옵니다. 따라서 이 값을 Google 프로젝트의 클라이언트 ID로 바꿔야 합니다. 어떤 웹 클라이언트 ID라도 사용할 수도 있으며, 새로 생성할 수도 있습니다.
다음으로, Firebase 프로젝트로 돌아가서
GoogleSignInOptions (Firebase 콘솔의
Auth > Sign In Providers > Google 섹션)에 설정한 클라이언트 ID를 허용 목록에 추가합니다.
google-services.json을 다시 다운로드하여 Android 앱에 추가해야 합니다. 그러면 Firebase 프로젝트가 Google 프로젝트에서 생성되는 Google ID 토큰을 허용합니다. 따라서 Android 앱이 Google 프로젝트를 사용하여 Google에 로그인한 후
정상적인 접근 방식에 따라 Google ID 토큰을 사용해 Firebase 프로젝트로 인증하게 됩니다. Google API 프로젝트에 연결된 Google API에 대해 인증된 호출을 생성하고 Firebase 프로젝트를 사용하여 Firebase API에 대한 인증된 호출을 생성할 수 있습니다.
두 가지 다른 Firebase 프로젝트에서 데이터베이스에 액세스
앞서 설명한 상황에서는 Google 프로젝트에도 액세스해야 하는 단일 Firebase 프로젝트가 있었던 경우였고, API가 서로 달랐기 때문에 아무런 문제가 없었습니다. 하지만 같은 API를 사용하여 여러 프로젝트에 액세스해야 할 때도 있습니다. 여러 데이터베이스 인스턴스에 액세스하는 경우를 예로 들 수 있습니다.
Firebase를 사용하는 Android 앱의 경우 모든 Firebase API의 구성을 관리하는 중앙
FirebaseApp 객체가 있습니다. 이 객체는 앱 실행 시 콘텐츠 제공자가 자동으로 초기화하므로, 보통은 이 객체와 상호 작용해야 할 필요가 전혀 없습니다. 하지만 단일 앱에서 여러 프로젝트에 액세스하려는 경우 각 프로젝트를 개별적으로 참조하기 위해 별개의 FirebaseApp이 필요합니다. Firebase에서 자동으로 생성하는 기본 인스턴스 이외의 인스턴스 초기화는 개발자 자신이 결정할 문제입니다.
예를 들어, 기본 Firebase 데이터베이스 인스턴스에 연결할 때는 암시적으로 기본 Firebase 앱을 사용합니다.
FirebaseDatabase database = FirebaseDatabase.getInstance();
다른 프로젝트에서 다른 Firebase 실시간 데이터베이스에 연결하려면 우선 연결하려는 다른 Firebase 프로젝트에 대해 FirebaseApp 인스턴스를 초기화하고 인스턴스의 식별자를 지정합니다. 이 경우에는 다음과 같이
"secondary"입니다.
FirebaseOptions options = new FirebaseOptions.Builder()
.setApplicationId("1:530266078999:android:481c4ecf3253701e") // Required for Analytics.
.setApiKey("AIzaSyBRxOyIj5dJkKgAVPXRLYFkdZwh2Xxq51k") // Required for Auth.
.setDatabaseUrl("https://project-1765055333176374514.firebaseio.com/") // Required for RTDB.
.build();
FirebaseApp.initializeApp(this /* Context */, options, "secondary");
그러면 같은 클라이언트 API를 사용하여 데이터베이스에 액세스할 수 있지만, 이번에는 관련
FirebaseApp을
FirebaseDatabase.getInstance()에 전달함으로써 어떤 프로젝트에 액세스할지 지정합니다.
// Retrieve my other app.
FirebaseApp app = FirebaseApp.getInstance("secondary");
// Get the database for the other app.
FirebaseDatabase secondaryDatabase = FirebaseDatabase.getInstance(app);
두 개의 다른 Firebase 데이터베이스 인증
위에서 설명한 두 가지 방법을 결합하면 연결할 외부 ID가 있을 때마다 Firebase 프로젝트 간에 인증 데이터를 공유할 수 있습니다.
예를 들어, 앱에서 Google Sign-In을 통한 로그인을 허용하고 기본 프로젝트 및 보조 프로젝트에서 인증을 요구하는 데이터베이스 규칙을 구성한 경우 동일한 Google 자격 증명을 사용하여 두 시스템 모두에 로그인할 수 있습니다.
먼저
평소와 같이 기본 프로젝트에서 Google Sign-In을 사용하도록 앱을 설정합니다. 그러면 기본 프로젝트에서 기본 클라이언트 ID를 얻게 됩니다. 클라이언트 ID는 지정된 앱 클라이언트(웹, Android, iOS)의 식별자일 뿐이며, 보통 클라이언트 자체에 포함되어 있습니다. 한 프로젝트의 클라이언트 ID가 여러 개일 수 있지만,
GoogleSignInOptions builder에 대한 requestIdToken 호출에 지정된 항목을 허용 목록에 추가해야 합니다.
.requestIdToken(getString(R.string.default_web_client_id))
보통 google-services.json에서 유형이 "3"인 첫 번째 client_id로 이 항목을 찾을 수 있습니다. 제 경우엔 다음과 같았습니다.
{
"client_id": "56865680640-e8mr503bun5eaevqctn4u807q4hpi44s.apps.googleusercontent.com",
"client_type": 3
},
이제
Auth > Sign In Providers 섹션의
Google 패널(
보조 프로젝트에 있음)로 이동합니다. 여기서 클라이언트 ID를 허용 목록에 추가할 수 있습니다.
이제 Google Sign-In 결과에서 동일한
GoogleSignInAccount 객체를 가져와서 기본 앱과 보조 앱에 대해 모두 인증할 수 있습니다.
AuthCredential credential = GoogleAuthProvider.getCredential(account.getIdToken(), null);
FirebaseAuth.getInstance().signInWithCredential(credential);
FirebaseApp app = FirebaseApp.getInstance("secondary");
FirebaseAuth.getInstance(app).signInWithCredential(credential);
사용자가 한 번만 로그인해도 두 프로젝트에 대해 모두 인증됩니다.
프로젝트 간 UID 공유
여기서 한 가지 문제점은 각 프로젝트에서 Firebase 사용자 ID가 다르다는 점입니다. 예를 들어, 동일한 Google 자격 증명을 사용하는 경우 다음과 같은 두 가지 UID를 얻습니다.
Default Auth UID: 0960868722032022577213DA4EA8B7A1683D92B405DD
Secondary Auth UID: 7h6XOeSxmkNsSseFJ1jU31WZHDP2
앱이
계정 링크 기능을 제공하지 않으면 데이터베이스 구조 및 보안 규칙 등에 Google(또는 Facebook, Twitter 등) 사용자 ID를 사용할 수 있습니다. 하지만 각 프로젝트에 똑같은 사용자 ID를 사용해야 하거나 이메일/비밀번호 또는 익명 인증을 사용하는 경우 상황은 약간 더 까다롭습니다.
다행히도, 사용자설정 인증 토큰이 자체 UID를 지정하므로 서버 측 코드와 함께 사용자설정 인증 기능으로 이 문제를 해결할 수 있습니다.
이번에는 보조 프로젝트에서 어떠한 항목도 허용 목록에 추가하지 않지만, 보조 프로젝트와 기본 프로젝트 모두의
서비스 계정을 다운로드합니다. 우선, Android 클라이언트에서 로그인하여 FirebaseAuth 클라이언트에서 Firebase ID 토큰을 가져옵니다.
참고: 여기서는 원하는 로그인 제공자를 사용할 수 있습니다. 그냥 사용자설정 토큰을 사용해 프로젝트 전체에 걸쳐 사용자 ID를 연결하겠습니다.
firebaseAuth.getCurrentUser().getToken(false /* forceRefresh */)
.addOnCompleteListener(new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
String token = task.getResult().getToken(); // Send this to the server.
}
});
토큰을 서버로 보내고 이 토큰으로 Firebase 사용자설정 토큰을 생성합니다. 현재 서버 측에 있기 때문에 서비스 계정을 사용하고는 있지만 Android에서와 마찬가지로 각각의 앱을 초기화해야 합니다(여기서는 Java 서버 SDK를 사용하지만 NodeJS도 이와 마찬가지로 사용할 수 있음).
FirebaseOptions options = new FirebaseOptions.Builder()
.setServiceAccount(new FileInputStream("default-service-account.json"))
.build();
FirebaseApp.initializeApp(options);
FirebaseOptions secondaryOptions = new FirebaseOptions.Builder()
.setServiceAccount(new FileInputStream("secondary-service-account.json"))
.build();
FirebaseApp.initializeApp(secondaryOptions, "secondary");
기본 앱은 클라이언트에서 오는 토큰을 인증하는 데 사용하고 보조 앱은 적당한 UID 세트로 사용자설정 인증 토큰을 생성하는 데 사용합니다.
// Verify the ID token using the default app.
FirebaseAuth.getInstance().verifyIdToken(idToken)
.addOnSuccessListener(new OnSuccessListener() {
@Override
public void onSuccess(FirebaseToken decodedToken) {
String uid = decodedToken.getUid();
System.out.println("User " + uid + " verified");
FirebaseApp app = FirebaseApp.getInstance("secondary");
String customToken = FirebaseAuth.getInstance(app).createCustomToken(uid);
// TODO: Send the token back to the client!
}
});
Android 앱으로 돌아가서, 서버에서 사용자설정 토큰을 가져와서 이를 사용해 보조 프로젝트를 인증합니다.
FirebaseApp app = FirebaseApp.getInstance("secondary");
FirebaseAuth.getInstance(app).signInWithCustomToken(token);
이제, 두 프로젝트의 Firebase UID가 일치합니다.
Default Auth UID: 0960868722032022577213DA4EA8B7A1683D92B405DD
Secondary Auth UID: 0960868722032022577213DA4EA8B7A1683D92B405DD
iOS와 웹도 마찬가지
오늘 설명해드린 내용이 단일 앱에서 여러 Firebase 프로젝트를 다룰 때 유용한 옵션으로 활용할 수 있길 바라겠습니다. iOS와 웹에도 이런 사항이 적용되는지 궁금하시다면 확실히 그렇다고 말씀드릴 수 있습니다. Android의 FirebaseApp에 상응하는 앱을 사용하여 보조 프로젝트에 대한 참조를 생성하기만 하면 됩니다.
JavaScript에서는
firebase.app을 사용합니다.
var config = {
apiKey: "",
authDomain: ".firebaseapp.com",
databaseURL: "https://.firebaseio.com",
storageBucket: ".appspot.com",
messagingSenderId: "",
};
var secondary = firebase.initializeApp(otherAppConfig, "secondary");
var secondaryDatabase = secondary.database();
iOS에서는
FIRApp을 사용합니다.
// Alt: load from plist using |FIROptions(contentsOfFile:)|
let options = FIROptions(googleAppID: googleAppID, bundleID: bundleID, GCMSenderID: GCMSenderID, APIKey: nil, clientID: nil, trackingID: nil, androidClientID: nil, databaseURL: databaseURL, storageBucket: nil, deepLinkURLScheme: nil)
FIRApp.configure(withName: "secondary", options: fileopts)
guard let secondary = FIRApp.init(named: "secondary")
else { assert(false, "Could not retrieve secondary app") }
let secondaryDatabase = FIRDatabase.database(app: secondary);
자세한 내용과 관련 링크는 Firebase 문서에 새로 추가된
Configuring Your Firebase Project(Firebase 프로젝트 구성) 페이지를 살펴보시기 바랍니다.