如何使用 Firebase 身份驗證構建基於角色的 API
已發表: 2022-03-11在本教程中,我們將構建一個 REST API 來使用 Firebase 和 Node.js 管理用戶和角色。 此外,我們將了解如何使用 API 來授權(或不授權)哪些用戶可以訪問特定資源。
介紹
幾乎每個應用程序都需要某種級別的授權系統。 在某些情況下,使用我們的用戶表驗證用戶名/密碼集就足夠了,但通常,我們需要更細粒度的權限模型來允許某些用戶訪問某些資源並限制其他用戶訪問它們。 構建支持後者的系統並非易事,而且可能非常耗時。 在本教程中,我們將學習如何使用 Firebase 構建基於角色的身份驗證 API,這將幫助我們快速啟動和運行。
基於角色的身份驗證
在此授權模型中,訪問權限被授予角色,而不是特定用戶,並且根據您設計權限模型的方式,用戶可以擁有一個或多個。 另一方面,資源需要某些角色才能允許用戶執行它。
火力基地
Firebase 身份驗證
簡而言之,Firebase 身份驗證是一個可擴展的基於令牌的身份驗證系統,並提供與最常見的提供商(如 Google、Facebook 和 Twitter 等)的開箱即用集成。
它使我們能夠使用自定義聲明,我們將利用這些聲明構建靈活的基於角色的 API。
我們可以在聲明中設置任何 JSON 值(例如, { role: 'admin' }
或{ role: 'manager' }
)。
設置後,自定義聲明將包含在 Firebase 生成的令牌中,我們可以讀取該值來控制訪問。
它還附帶一個非常慷慨的免費配額,在大多數情況下綽綽有餘。
Firebase 函數
Functions 是一種完全託管的無服務器平台服務。 我們只需要在 Node.js 中編寫代碼並部署它。 Firebase 負責按需擴展基礎架構、服務器配置等。 在我們的例子中,我們將使用它來構建我們的 API 並通過 HTTP 將其公開給 Web。
Firebase 允許我們將express.js
應用程序設置為不同路徑的處理程序——例如,您可以創建一個 Express 應用程序並將其掛接到/mypath
,所有到達此路由的請求都將由配置的app
處理。
在函數的上下文中,您可以使用 Admin SDK 訪問整個 Firebase 身份驗證 API。
這就是我們創建用戶 API 的方式。
我們將建造什麼
所以在我們開始之前,讓我們看看我們將構建什麼。 我們將使用以下端點創建一個 REST API:
Http動詞 | 小路 | 描述 | 授權 |
---|---|---|---|
得到 | /用戶 | 列出所有用戶 | 只有管理員和管理員有權訪問 |
郵政 | /用戶 | 創建新用戶 | 只有管理員和管理員有權訪問 |
得到 | /users/:id | 獲取 :id 用戶 | 管理員、經理和與 :id 相同的用戶具有訪問權限 |
修補 | /users/:id | 更新 :id 用戶 | 管理員、經理和與 :id 相同的用戶具有訪問權限 |
刪除 | /users/:id | 刪除 :id 用戶 | 管理員、經理和與 :id 相同的用戶具有訪問權限 |
這些端點中的每一個都將處理身份驗證、驗證授權、執行相應的操作,並最終返回有意義的 HTTP 代碼。
我們將創建驗證令牌所需的身份驗證和授權函數,並檢查聲明是否包含執行操作所需的角色。
構建 API
為了構建 API,我們需要:
- Firebase 項目
- 安裝了
firebase-tools
首先,登錄 Firebase:
firebase login
接下來,初始化一個 Functions 項目:
firebase init ? Which Firebase CLI features do you want to set up for this folder? ... (O) Functions: Configure and deploy Cloud Functions ? Select a default Firebase project for this directory: {your-project} ? What language would you like to use to write Cloud Functions? TypeScript ? Do you want to use TSLint to catch probable bugs and enforce style? Yes ? Do you want to install dependencies with npm now? Yes
此時,您將擁有一個 Functions 文件夾,其中包含創建 Firebase Functions 的最低設置。
在src/index.ts
有一個helloWorld
示例,您可以取消註釋以驗證您的 Functions 是否有效。 然後你可以cd functions
並運行npm run serve
。 此命令將轉譯代碼並啟動本地服務器。
您可以在 http://localhost:5000/{your-project}/us-central1/helloWorld 查看結果
請注意,該函數在'index.ts: 'helloWorld'
定義為其名稱的路徑上公開。
創建 Firebase HTTP 函數
現在讓我們編寫我們的 API。 我們將創建一個 http Firebase 函數並將其掛接到/api
路徑。
首先,安裝npm install express
。
在src/index.ts
我們將:
- 使用
admin.initializeApp();
- 將 Express 應用程序設置為我們的
api
https 端點的處理程序
import * as functions from 'firebase-functions'; import * as admin from 'firebase-admin'; import * as express from 'express'; admin.initializeApp(); const app = express(); export const api = functions.https.onRequest(app);
現在,所有發往/api
的請求都將由app
實例處理。
接下來我們要做的是將app
實例配置為支持 CORS 並添加 JSON 正文解析器中間件。 這樣我們就可以從任何 URL 發出請求並解析 JSON 格式的請求。
我們將首先安裝所需的依賴項。
npm install --save cors body-parser
npm install --save-dev @types/cors
然後:
//... import * as cors from 'cors'; import * as bodyParser from 'body-parser'; //... const app = express(); app.use(bodyParser.json()); app.use(cors({ origin: true })); export const api = functions.https.onRequest(app);
最後,我們將配置app
將處理的路由。
//... import { routesConfig } from './users/routes-config'; //… app.use(cors({ origin: true })); routesConfig(app) export const api = functions.https.onRequest(app);
Firebase Functions 允許我們將 Express 應用程序設置為處理程序,以及您在functions.https.onRequest(app);
——在這種情況下, api
也將由app
處理。 這允許我們編寫特定的端點,例如api/users
並為每個 HTTP 動詞設置一個處理程序,我們接下來會這樣做。
讓我們創建文件src/users/routes-config.ts
在這裡,我們將在POST '/users'
處設置一個create
處理程序
import { Application } from "express"; import { create} from "./controller"; export function routesConfig(app: Application) { app.post('/users', create ); }
現在,我們將創建src/users/controller.ts
文件。
在這個函數中,我們首先驗證所有字段都在請求正文中,然後,我們創建用戶並設置自定義聲明。
我們只是在setCustomUserClaims
中傳遞{ role }
— Firebase 已經設置了其他字段。
如果沒有發生錯誤,我們會返回一個 201 代碼,其中包含創建的用戶的uid
。
import { Request, Response } from "express"; import * as admin from 'firebase-admin' export async function create(req: Request, res: Response) { try { const { displayName, password, email, role } = req.body if (!displayName || !password || !email || !role) { return res.status(400).send({ message: 'Missing fields' }) } const { uid } = await admin.auth().createUser({ displayName, password, email }) await admin.auth().setCustomUserClaims(uid, { role }) return res.status(201).send({ uid }) } catch (err) { return handleError(res, err) } } function handleError(res: Response, err: any) { return res.status(500).send({ message: `${err.code} - ${err.message}` }); }
現在,讓我們通過添加授權來保護處理程序。 為此,我們將向我們的create
端點添加幾個處理程序。 使用express.js
,您可以設置將按順序執行的處理程序鏈。 在處理程序中,您可以執行代碼並將其傳遞給next()
處理程序或返迴響應。 我們要做的是首先驗證用戶,然後驗證它是否被授權執行。
在文件src/users/routes-config.ts
:
//... import { isAuthenticated } from "../auth/authenticated"; import { isAuthorized } from "../auth/authorized"; export function routesConfig(app: Application) { app.post('/users', isAuthenticated, isAuthorized({ hasRole: ['admin', 'manager'] }), create ); }
讓我們創建文件src/auth/authenticated.ts
。
在此函數中,我們將驗證請求標頭中是否存在authorization
承載令牌。 然後我們將使用admin.auth().verifyidToken()
對其進行解碼,並將用戶的uid
、 role
和email
保存在res.locals
變量中,我們稍後將使用它來驗證授權。
在令牌無效的情況下,我們向客戶端返回 401 響應:
import { Request, Response } from "express"; import * as admin from 'firebase-admin' export async function isAuthenticated(req: Request, res: Response, next: Function) { const { authorization } = req.headers if (!authorization) return res.status(401).send({ message: 'Unauthorized' }); if (!authorization.startsWith('Bearer')) return res.status(401).send({ message: 'Unauthorized' }); const split = authorization.split('Bearer ') if (split.length !== 2) return res.status(401).send({ message: 'Unauthorized' }); const token = split[1] try { const decodedToken: admin.auth.DecodedIdToken = await admin.auth().verifyIdToken(token); console.log("decodedToken", JSON.stringify(decodedToken)) res.locals = { ...res.locals, uid: decodedToken.uid, role: decodedToken.role, email: decodedToken.email } return next(); } catch (err) { console.error(`${err.code} - ${err.message}`) return res.status(401).send({ message: 'Unauthorized' }); } }
現在,讓我們創建一個src/auth/authorized.ts
文件。
在這個處理程序中,我們從我們之前設置的res.locals
中提取用戶的信息,並驗證它是否具有執行操作所需的角色,或者如果操作允許同一用戶執行,我們驗證請求參數上的 ID與身份驗證令牌中的相同。 如果用戶沒有所需的角色,我們將返回 403。
import { Request, Response } from "express"; export function isAuthorized(opts: { hasRole: Array<'admin' | 'manager' | 'user'>, allowSameUser?: boolean }) { return (req: Request, res: Response, next: Function) => { const { role, email, uid } = res.locals const { id } = req.params if (opts.allowSameUser && id && uid === id) return next(); if (!role) return res.status(403).send(); if (opts.hasRole.includes(role)) return next(); return res.status(403).send(); } }
使用這兩種方法,我們將能夠驗證請求並根據傳入令牌中的role
授權它們。 這很好,但由於 Firebase 不允許我們從項目控制台設置自定義聲明,我們將無法執行任何這些端點。 為了繞過這個,我們可以從 Firebase Authentication Console 創建一個 root 用戶
並在代碼中設置電子郵件比較。 現在,當從該用戶發出請求時,我們將能夠執行所有操作。
//... const { role, email, uid } = res.locals const { id } = req.params if (email === '[email protected]') return next(); //...
現在,讓我們將其餘的 CRUD 操作添加到src/users/routes-config.ts
。
對於獲取或更新發送:id
參數的單個用戶的操作,我們還允許同一用戶執行該操作。
export function routesConfig(app: Application) { //.. // lists all users app.get('/users', [ isAuthenticated, isAuthorized({ hasRole: ['admin', 'manager'] }), all ]); // get :id user app.get('/users/:id', [ isAuthenticated, isAuthorized({ hasRole: ['admin', 'manager'], allowSameUser: true }), get ]); // updates :id user app.patch('/users/:id', [ isAuthenticated, isAuthorized({ hasRole: ['admin', 'manager'], allowSameUser: true }), patch ]); // deletes :id user app.delete('/users/:id', [ isAuthenticated, isAuthorized({ hasRole: ['admin', 'manager'] }), remove ]); }
在src/users/controller.ts
上。 在這些操作中,我們利用管理 SDK 與 Firebase 身份驗證進行交互並執行相應的操作。 正如我們之前在create
操作中所做的那樣,我們在每個操作中返回一個有意義的 HTTP 代碼。
對於更新操作,我們驗證所有存在的字段並使用請求中發送的字段覆蓋customClaims
:
//.. export async function all(req: Request, res: Response) { try { const listUsers = await admin.auth().listUsers() const users = listUsers.users.map(mapUser) return res.status(200).send({ users }) } catch (err) { return handleError(res, err) } } function mapUser(user: admin.auth.UserRecord) { const customClaims = (user.customClaims || { role: '' }) as { role?: string } const role = customClaims.role ? customClaims.role : '' return { uid: user.uid, email: user.email || '', displayName: user.displayName || '', role, lastSignInTime: user.metadata.lastSignInTime, creationTime: user.metadata.creationTime } } export async function get(req: Request, res: Response) { try { const { id } = req.params const user = await admin.auth().getUser(id) return res.status(200).send({ user: mapUser(user) }) } catch (err) { return handleError(res, err) } } export async function patch(req: Request, res: Response) { try { const { id } = req.params const { displayName, password, email, role } = req.body if (!id || !displayName || !password || !email || !role) { return res.status(400).send({ message: 'Missing fields' }) } await admin.auth().updateUser(id, { displayName, password, email }) await admin.auth().setCustomUserClaims(id, { role }) const user = await admin.auth().getUser(id) return res.status(204).send({ user: mapUser(user) }) } catch (err) { return handleError(res, err) } } export async function remove(req: Request, res: Response) { try { const { id } = req.params await admin.auth().deleteUser(id) return res.status(204).send({}) } catch (err) { return handleError(res, err) } } //...
現在我們可以在本地運行該函數。 為此,首先您需要設置帳戶密鑰,以便能夠在本地與 auth API 連接。 然後運行:
npm run serve
部署 API
偉大的! 現在我們已經編寫了基於角色的 API,我們可以將其部署到 Web 並開始使用它。 使用 Firebase 進行部署非常簡單,我們只需要運行firebase deploy
即可。 部署完成後,我們可以通過發布的 URL 訪問我們的 API。
您可以在 https://console.firebase.google.com/u/0/project/{your-project}/functions/list 查看 API URL。

就我而言,它是 [https://us-central1-joaq-lab.cloudfunctions.net/api]。
使用 API
部署 API 後,我們有多種使用方法——在本教程中,我將介紹如何通過 Postman 或 Angular 應用程序使用它。
如果我們在任何瀏覽器上輸入 List All Users URL ( /api/users
),我們將得到以下信息:
原因是當從瀏覽器發送請求時,我們正在執行一個沒有 auth 標頭的 GET 請求。 這意味著我們的 API 實際上按預期工作!
我們的 API 通過令牌進行保護——為了生成這樣的令牌,我們需要調用 Firebase 的客戶端 SDK 並使用有效的用戶/密碼憑據登錄。 成功後,Firebase 將在響應中發回一個令牌,然後我們可以將其添加到我們想要執行的任何後續請求的標頭中。
從 Angular 應用程序
在本教程中,我將介紹從 Angular 應用程序使用 API 的重要部分。 可以在此處訪問完整的存儲庫,如果您需要有關如何創建 Angular 應用程序和配置 @angular/fire 以使用的分步教程,您可以查看這篇文章。
所以,回到登錄,我們將有一個帶有<form>
的SignInComponent
,讓用戶輸入用戶名和密碼。
//... <form [formGroup]="form"> <div class="form-group"> <label>Email address</label> <input type="email" formControlName="email" class="form-control" placeholder="Enter email"> </div> <div class="form-group"> <label>Password</label> <input type="password" formControlName="password" class="form-control" placeholder="Password"> </div> </form> //...
在課堂上,我們使用signInWithEmailAndPassword
服務AngularFireAuth
。
//... form: FormGroup = new FormGroup({ email: new FormControl(''), password: new FormControl('') }) constructor( private afAuth: AngularFireAuth ) { } async signIn() { try { const { email, password } = this.form.value await this.afAuth.auth.signInWithEmailAndPassword(email, password) } catch (err) { console.log(err) } } //..
此時,我們可以登錄到我們的 Firebase 項目。
當我們在 DevTools 中檢查網絡請求時,我們可以看到 Firebase 在驗證了我們的用戶和密碼後返回了一個令牌。
這個令牌是我們將用於將我們的標頭請求發送到我們構建的 API 的令牌。 將令牌添加到所有請求的一種方法是使用HttpInterceptor
。
此文件顯示如何從AngularFireAuth
獲取令牌並將其添加到標頭的請求中。 然後我們在 AppModule 中提供攔截器文件。
http-interceptors/auth-token.interceptor.ts
@Injectable({ providedIn: 'root' }) export class AuthTokenHttpInterceptor implements HttpInterceptor { constructor( private auth: AngularFireAuth ) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return this.auth.idToken.pipe( take(1), switchMap(idToken => { let clone = req.clone() if (idToken) { clone = clone.clone({ headers: req.headers.set('Authorization', 'Bearer ' + idToken) }); } return next.handle(clone) }) ) } } export const AuthTokenHttpInterceptorProvider = { provide: HTTP_INTERCEPTORS, useClass: AuthTokenHttpInterceptor, multi: true }
app.module.ts
@NgModule({ //.. providers: [ AuthTokenHttpInterceptorProvider ] //... }) export class AppModule { }
一旦設置了攔截器,我們就可以從httpClient
向我們的 API 發出請求。 例如,這是一個UsersService
,我們將列表稱為所有用戶,通過其 ID 獲取用戶,創建用戶並更新用戶。
//… export type CreateUserRequest = { displayName: string, password: string, email: string, role: string } export type UpdateUserRequest = { uid: string } & CreateUserRequest @Injectable({ providedIn: 'root' }) export class UserService { private baseUrl = '{your-functions-url}/api/users' constructor( private http: HttpClient ) { } get users$(): Observable<User[]> { return this.http.get<{ users: User[] }>(`${this.baseUrl}`).pipe( map(result => { return result.users }) ) } user$(id: string): Observable<User> { return this.http.get<{ user: User }>(`${this.baseUrl}/${id}`).pipe( map(result => { return result.user }) ) } create(user: CreateUserRequest) { return this.http.post(`${this.baseUrl}`, user) } edit(user: UpdateUserRequest) { return this.http.patch(`${this.baseUrl}/${user.uid}`, user) } }
現在,我們可以調用 API 以通過其 ID 獲取登錄用戶並列出來自組件的所有用戶,如下所示:
//... <div *ngIf="user$ | async; let user" class="col-12"> <div class="d-flex justify-content-between my-3"> <h4> Me </h4> </div> <ul class="list-group"> <li class="list-group-item d-flex justify-content-between align-items-center"> <div> <h5 class="mb-1">{{user.displayName}}</h5> <small>{{user.email}}</small> </div> <span class="badge badge-primary badge-pill">{{user.role?.toUpperCase()}}</span> </li> </ul> </div> <div class="col-12"> <div class="d-flex justify-content-between my-3"> <h4> All Users </h4> </div> <ul *ngIf="users$ | async; let users" class="list-group"> <li *ngFor="let user of users" class="list-group-item d-flex justify-content-between align-items-center"> <div> <h5 class="mb-1">{{user.displayName}}</h5> <small class="d-block">{{user.email}}</small> <small class="d-block">{{user.uid}}</small> </div> <span class="badge badge-primary badge-pill">{{user.role?.toUpperCase()}}</span> </li> </ul> //...
//... users$: Observable<User[]> user$: Observable<User> constructor( private userService: UserService, private userForm: UserFormService, private modal: NgbModal, private afAuth: AngularFireAuth ) { } ngOnInit() { this.users$ = this.userService.users$ this.user$ = this.afAuth.user.pipe( filter(user => !!user), switchMap(user => this.userService.user$(user.uid)) ) } //...
這就是結果。
請注意,如果我們使用具有role=user
的用戶登錄,則只會呈現 Me 部分。
我們會在網絡檢查器上得到一個 403。 這是由於我們之前在 API 上設置的限制,只允許“管理員”列出所有用戶。
現在,讓我們添加“創建用戶”和“編輯用戶”功能。 為此,讓我們首先創建一個UserFormComponent
和一個UserFormService
。
<ng-container *ngIf="user$ | async"></ng-container> <div class="modal-header"> <h4 class="modal-title">{{ title$ | async}}</h4> <button type="button" class="close" (click)="dismiss()"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <form [formGroup]="form" (ngSubmit)="save()"> <div class="form-group"> <label>Email address</label> <input type="email" formControlName="email" class="form-control" placeholder="Enter email"> </div> <div class="form-group"> <label>Password</label> <input type="password" formControlName="password" class="form-control" placeholder="Password"> </div> <div class="form-group"> <label>Display Name</label> <input type="string" formControlName="displayName" class="form-control" placeholder="Enter display name"> </div> <div class="form-group"> <label>Role</label> <select class="custom-select" formControlName="role"> <option value="admin">Admin</option> <option value="manager">Manager</option> <option value="user">User</option> </select> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-outline-danger" (click)="dismiss()">Cancel</button> <button type="button" class="btn btn-primary" (click)="save()">Save</button> </div>
@Component({ selector: 'app-user-form', templateUrl: './user-form.component.html', styleUrls: ['./user-form.component.scss'] }) export class UserFormComponent implements OnInit { form = new FormGroup({ uid: new FormControl(''), email: new FormControl(''), displayName: new FormControl(''), password: new FormControl(''), role: new FormControl(''), }); title$: Observable<string>; user$: Observable<{}>; constructor( public modal: NgbActiveModal, private userService: UserService, private userForm: UserFormService ) { } ngOnInit() { this.title$ = this.userForm.title$; this.user$ = this.userForm.user$.pipe( tap(user => { if (user) { this.form.patchValue(user); } else { this.form.reset({}); } }) ); } dismiss() { this.modal.dismiss('modal dismissed'); } save() { const { displayName, email, role, password, uid } = this.form.value; this.modal.close({ displayName, email, role, password, uid }); } }
import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class UserFormService { _BS = new BehaviorSubject({ title: '', user: {} }); constructor() { } edit(user) { this._BS.next({ title: 'Edit User', user }); } create() { this._BS.next({ title: 'Create User', user: null }); } get title$() { return this._BS.asObservable().pipe( map(uf => uf.title) ); } get user$() { return this._BS.asObservable().pipe( map(uf => uf.user) ); } }
回到主組件,讓我們添加按鈕來調用這些操作。 在這種情況下,“編輯用戶”將僅對登錄用戶可用。 如果需要,您可以繼續添加功能以編輯其他用戶!
//... <div class="d-flex justify-content-between my-3"> <h4> Me </h4> <button class="btn btn-primary" (click)="edit(user)"> Edit Profile </button> </div> //... <div class="d-flex justify-content-between my-3"> <h4> All Users </h4> <button class="btn btn-primary" (click)="create()"> New User </button> </div> //...
//... create() { this.userForm.create(); const modalRef = this.modal.open(UserFormComponent); modalRef.result.then(user => { this.userService.create(user).subscribe(_ => { console.log('user created'); }); }).catch(err => { }); } edit(userToEdit) { this.userForm.edit(userToEdit); const modalRef = this.modal.open(UserFormComponent); modalRef.result.then(user => { this.userService.edit(user).subscribe(_ => { console.log('user edited'); }); }).catch(err => { }); }
從郵遞員
Postman 是一種構建 API 並向 API 發出請求的工具。 這樣,我們可以模擬我們正在從任何客戶端應用程序或不同的服務調用我們的 API。
我們將演示的是如何發送請求以列出所有用戶。
打開工具後,我們設置 URL https://us-central1-{your-project}.cloudfunctions.net/api/users:
接下來,在選項卡授權上,我們選擇 Bearer Token 並設置我們之前從開發工具中提取的值。
結論
恭喜! 您已經完成了整個教程,現在您已經學會了在 Firebase 上創建基於用戶角色的 API。
我們還介紹瞭如何從 Angular 應用程序和 Postman 中使用它。
讓我們回顧一下最重要的事情:
- Firebase 允許您使用企業級身份驗證 API 快速啟動和運行,您可以在以後對其進行擴展。
- 幾乎每個項目都需要授權——如果您需要使用基於角色的模型來控制訪問,Firebase 身份驗證可讓您快速入門。
- 基於角色的模型依賴於驗證具有特定角色的用戶與特定用戶請求的資源。
- 在 Firebase Function 上使用 Express.js 應用程序,我們可以創建一個 REST API 並設置處理程序來驗證和授權請求。
- 利用內置的自定義聲明,您可以創建基於角色的身份驗證 API 並保護您的應用程序。
您可以在此處進一步了解 Firebase 身份驗證。 如果你想利用我們定義的角色,你可以使用 @angular/fire 助手。