這篇文章要來介紹一下我如何結合 Angular 和 Firebase 做到以角色為基礎的權限管理,有些英文的文章可以參考,不過還沒有看到太多中文的,分享一下。這篇文章會跳過 Firebase 上的基礎設定,以及基本的登入,這些網路上都有相關文章。
使用情境及流程
這次的故事是這樣,一個內部網站,只能接受有限度的人登入,登入之後會給予該使用者應該有的權限。假設有三種人:管理者,老師以及學生。另外有一個 canUse
的標籤來表示是否可以使用系統。
流程如下:
- 如果該使用者是第一次使用,檢查是否有其權限在資料庫中,若有,將權限寫入 Firebase Auth;若無,則不給登入。
- 如果該使用者並非第一次使用,則從 Firebase Auth 讀出資料,並且寫到 UserDetail 中。在 Angular 中用 UserDetail 來決定是否可執行某些操作。
- 改動使用者權限,檢查該使用者是否在 Firebase Auth 中,若有,則寫入權限至 Firebase Auth 以便下次登入使用;若無,僅將權限寫回資料庫。
我們必須使用 Firebase Function 來幫我們做一些額外的事情。
新使用者登入
針對新使用者,我們利用 Firebase Functions 來塞權限。程式碼如下:
// Firebase Function code
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp();
// 當有使用者被新增時,執行這段程式碼
export const newUser = functions.auth.user().onCreate(async(user) => {
const setClaimResult = await setNewUserDefaultPriv(user);
});
async function setNewUserDefaultPriv(user) {
const uid = user.uid;
const email = user.email;
// 檢查是否在資料庫
const memberRes =
await admin.firestore().collection(`PATH-TO-MEMBER`).where('email', '==', email).limit(1).get();
if (memberRes.empty) {
return false;
}
let memberData;
memberRes.forEach((data) => {
memberData = data.data();
});
// 讀書使用者在資料庫的權限,寫到 claims
const claims = {
isAdmin: memberData.isAdmin ? memberData.isAdmin : false,
isTeacher: memberData.isTeacher ? memberData.isTeacher : false,
isStudent: memberData.isStudent ? memberData.isStudent : false,
canUse: memberData.canUse ? memberData.canUse : false,
};
// 將 claims 寫回該使用者的帳號中
await admin.auth().setCustomUserClaims(uid, claims);
return true;
}
舊使用者登入
當使用者按下了登入按鈕,被我們寫到 Firebase Auth 中之後,就變成舊使用者了。這邊我們直接看 AuthService 的程式碼:
// auth.service.ts should be declare in constructor within auth-guard.ts in Angular
import { AngularFireAuth } from '@angular/fire/auth';
import * as firebase from 'firebase/app';
@Injectable({
providedIn: 'root'
})
export class AuthService {
firebaseUserDetail: firebase.User = null;
userDetail = {
email: '',
canUse: false,
isAdmin: false,
isTeacher: false,
isStudent: false,
};
private isNewUser: boolean; // 為了給不同的錯誤訊息
constructor(private afAuth: AngularFireAuth,) {
this.processUserInfo().subscribe((verified) => {
if (verified) {
} else {
if (this.isNewUser) {
// 可能是合法使用者,請稍後登入
} else {
// 不合法使用者
}
}
}
}
private processUserInfo() {
return this.afAuth.authState.pipe(
mergeMap(async(firebaseUser) => {
if (firebaseUser) {
this.firebaseUserDetail = firebaseUser;
return firebaseUser.getIdTokenResult();
} else {
return of(null);
}
}),
map((idTokenResult) => {
// 記得在新使用者加入的時候我們塞的 claims 嗎?
// claims 會存在 Firebase User 的 idTokenResult
// 拿出來之後存回 userDetail 以便之後使用了
if (idTokenResult && idTokenResult.hasOwnProperty('claims')) {
this.userDetail.email = this.firebaseUserDetail.email;
this.userDetail.canUse = idTokenResult['claims'].canUse;
this.userDetail.isAdmin = idTokenResult['claims'].isAdmin;
this.userDetail.isTeacher = idTokenResult['claims'].isTeacher;
this.userDetail.isStudent = idTokenResult['claims'].isStudent;
return true;
} else {
return false;
}
}),
);
}
}
改動使用者權限
這個做法很可惜的地方就是在 Firebase 上沒有辦法改動之前設定的 claims,也看不到。變得反而不好管理,因此在程式碼中我們還是把權限寫進資料庫,並且讓資料庫跟 Firebase Auth 連動,改動資料庫就順便改動 Firebase User 的資料,下次登入也可以拿到最新的權限。一樣利用 Firebase Function 做到這件事情:
// Firebase Function code
// 只要有改動某個使用者的資料,就會執行這個函式
export const changeUserPrivOnUpdate = functions.firestore.document(`DOCUMENT-TO-THE-USER`).onUpdate(
async (change, context) => {
await changeUserPriv(change.after.data());
}
);
async function changeUserPriv(memberData)) {
try {
// 從使用者資料的 email 找到該使用者的 Firebase User
user = await admin.auth().getUserByEmail(memberData.email);
// 寫進目前資料庫的權限至 Firebase user 中
await admin.auth().setCustomUserClaims(user.uid, {
isAdmin: employeeData.isAdmin,
isTeacher: employeeData.isTeacher,
isStudent: employeeData.isStudent,
canUse: employeeData.canUse,
});
return true;
} catch {
return false;
}
}
結束
上述程式碼結合 Angular 和 Firebase 做到以角色為基礎的權限管理,我只有遇到有一個問題
- 第一次登入的使用者因為要設定權限,大約要等個 5~10 秒等待 Function 執行完畢,重新整理頁面之後才可以登入成功。
不過因為是內部網頁,而且也有提示訊息,所以使用者還算清楚。整理來說我覺得還可以接受,另外也是使用原生地 Firebase 方式,不用自己維護使用者登入資料,簡單很多。
如果有問題或建議請留言,看到都會回。