วิธีสร้าง API ตามบทบาทด้วย Firebase Authentication

เผยแพร่แล้ว: 2022-03-11

ในบทช่วยสอนนี้ เราจะสร้าง REST API เพื่อจัดการผู้ใช้และบทบาทโดยใช้ Firebase และ Node.js นอกจากนี้ เราจะดูวิธีการใช้ API เพื่ออนุญาต (หรือไม่) ว่าผู้ใช้รายใดสามารถเข้าถึงทรัพยากรเฉพาะ

บทนำ

เกือบทุกแอปต้องมีระบบการอนุญาตในระดับหนึ่ง ในบางกรณี การตรวจสอบความถูกต้องของชื่อผู้ใช้/รหัสผ่านที่ตั้งค่าด้วยตารางผู้ใช้ของเราก็เพียงพอแล้ว แต่บ่อยครั้ง เราจำเป็นต้องมีรูปแบบการอนุญาตที่ละเอียดยิ่งขึ้นเพื่ออนุญาตให้ผู้ใช้บางคนเข้าถึงทรัพยากรบางอย่างและจำกัดไม่ให้เข้าถึงทรัพยากรเหล่านั้น การสร้างระบบเพื่อรองรับสิ่งหลังไม่ใช่เรื่องเล็กน้อยและอาจใช้เวลานานมาก ในบทช่วยสอนนี้ เราจะเรียนรู้วิธีสร้าง API การตรวจสอบสิทธิ์ตามบทบาทโดยใช้ Firebase ซึ่งจะช่วยให้เราเริ่มต้นใช้งานได้อย่างรวดเร็ว

การตรวจสอบตามบทบาท

ในรูปแบบการอนุญาตนี้ การเข้าถึงจะได้รับบทบาท แทนที่จะเป็นผู้ใช้เฉพาะ และผู้ใช้สามารถมีได้ตั้งแต่หนึ่งรายการขึ้นไป ขึ้นอยู่กับว่าคุณออกแบบแบบจำลองการอนุญาตของคุณอย่างไร ในทางกลับกัน ทรัพยากรต้องการบทบาทบางอย่างเพื่อให้ผู้ใช้ดำเนินการได้

การตรวจสอบตามบทบาทพร้อมภาพประกอบ

Firebase

การตรวจสอบสิทธิ์ Firebase

โดยสรุปแล้ว Firebase Authentication คือระบบตรวจสอบสิทธิ์ที่ใช้โทเค็นแบบขยายได้ และให้การผสานการทำงานแบบสำเร็จรูปกับผู้ให้บริการทั่วไปส่วนใหญ่ เช่น Google, Facebook และ Twitter และอื่นๆ

ช่วยให้เราใช้การอ้างสิทธิ์ที่กำหนดเองซึ่งเราจะใช้ประโยชน์เพื่อสร้าง API ตามบทบาทที่ยืดหยุ่น

เราสามารถตั้งค่า JSON ใด ๆ ในการอ้างสิทธิ์ (เช่น { role: 'admin' } หรือ { role: 'manager' } )

เมื่อตั้งค่าแล้ว การอ้างสิทธิ์ที่กำหนดเองจะรวมอยู่ในโทเค็นที่ Firebase สร้าง และเราสามารถอ่านค่าเพื่อควบคุมการเข้าถึงได้

นอกจากนี้ยังมาพร้อมกับโควต้าฟรีที่เอื้อเฟื้อ ซึ่งโดยส่วนใหญ่ก็เพียงพอแล้ว

ฟังก์ชัน Firebase

ฟังก์ชันเป็นบริการแพลตฟอร์มแบบไร้เซิร์ฟเวอร์ที่มีการจัดการเต็มรูปแบบ เราเพียงแค่ต้องเขียนโค้ดของเราใน Node.js และปรับใช้ Firebase ดูแลการปรับขนาดโครงสร้างพื้นฐานตามความต้องการ การกำหนดค่าเซิร์ฟเวอร์ และอื่นๆ ในกรณีของเรา เราจะใช้มันเพื่อสร้าง API และเปิดเผยผ่าน HTTP สู่เว็บ

Firebase ช่วยให้เราตั้งค่าแอป express.js เป็นตัวจัดการสำหรับพาธต่างๆ ได้ ตัวอย่างเช่น คุณสามารถสร้างแอป Express และเชื่อมต่อกับ /mypath และคำขอทั้งหมดที่มายังเส้นทางนี้จะได้รับการจัดการโดย app ที่กำหนดค่าไว้

จากภายในบริบทของฟังก์ชัน คุณสามารถเข้าถึง Firebase Authentication API ทั้งหมดได้โดยใช้ Admin SDK

นี่คือวิธีที่เราจะสร้าง API ผู้ใช้

สิ่งที่เราจะสร้าง

ก่อนที่เราจะเริ่มต้น มาดูกันว่าเราจะสร้างอะไร เรากำลังจะสร้าง REST API ด้วยจุดปลายดังต่อไปนี้:

HTTP Verb เส้นทาง คำอธิบาย การอนุญาต
รับ /ผู้ใช้ แสดงรายการผู้ใช้ทั้งหมด เฉพาะผู้ดูแลระบบและผู้จัดการเท่านั้นที่มีสิทธิ์เข้าถึง
โพสต์ /ผู้ใช้ สร้างผู้ใช้ใหม่ เฉพาะผู้ดูแลระบบและผู้จัดการเท่านั้นที่มีสิทธิ์เข้าถึง
รับ /users/:id รับ :id ผู้ใช้ ผู้ดูแลระบบ ผู้จัดการ และผู้ใช้เดียวกันกับ :id มีการเข้าถึง
ปะ /users/:id อัปเดต :id user ผู้ดูแลระบบ ผู้จัดการ และผู้ใช้เดียวกันกับ :id มีการเข้าถึง
ลบ /users/:id ลบ :id user ผู้ดูแลระบบ ผู้จัดการ และผู้ใช้เดียวกันกับ :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

ที่ src/index.ts มีตัวอย่าง helloWorld ซึ่งคุณสามารถยกเลิกการใส่ความคิดเห็นเพื่อตรวจสอบว่าฟังก์ชันของคุณใช้งานได้ จากนั้นคุณสามารถ cd functions และเรียกใช้ npm run serve คำสั่งนี้จะแปลงรหัสและเริ่มเซิร์ฟเวอร์ภายใน

สามารถตรวจสอบผลได้ที่ http://localhost:5000/{your-project}/us-central1/helloWorld

แอพ Firebase ใหม่

สังเกตว่าฟังก์ชั่นถูกเปิดเผยบนเส้นทางที่กำหนดเป็นชื่อของมันที่ 'index.ts: 'helloWorld'

การสร้างฟังก์ชัน Firebase HTTP

ตอนนี้ มาโค้ด API ของเรากัน เราจะสร้างฟังก์ชัน http Firebase และเชื่อมต่อกับเส้นทาง /api

ขั้นแรก ติดตั้ง npm install express

บน src/index.ts เราจะ:

  • เริ่มต้นโมดูล firebase-admin SDK ด้วย 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 ช่วยให้เราตั้งค่าแอป Express เป็นตัวจัดการ และพาธใดๆ หลังจากตั้งค่าที่ functions.https.onRequest(app); —ในกรณีนี้ api —จะถูกจัดการโดย app พด้วย สิ่งนี้ทำให้เราสามารถเขียนจุดปลายเฉพาะ เช่น api/users และตั้งค่าตัวจัดการสำหรับกริยา HTTP แต่ละตัว ซึ่งเราจะทำต่อไป

มาสร้างไฟล์กัน src/users/routes-config.ts

ที่นี่ เราจะตั้งค่าตัวจัดการการ create ที่ POST '/users'

 import { Application } from "express"; import { create} from "./controller"; export function routesConfig(app: Application) { app.post('/users', create ); }

ตอนนี้ เราจะสร้างไฟล์ src/users/controller.ts

ในฟังก์ชันนี้ ก่อนอื่นเราจะตรวจสอบว่าฟิลด์ทั้งหมดอยู่ในคำขอเนื้อหา จากนั้นเราจะสร้างผู้ใช้และตั้งค่าการอ้างสิทธิ์ที่กำหนดเอง

เราเพิ่งส่ง { role } ใน setCustomUserClaims — 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 คุณสามารถตั้งค่า chain of handlers ที่จะดำเนินการตามลำดับ ภายในตัวจัดการ คุณสามารถรันโค้ดและส่งไปยังตัวจัดการ 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

การสร้างผู้ใช้จาก Firebase Authentication Console

และตั้งค่าการเปรียบเทียบอีเมลในรหัส ตอนนี้ เมื่อส่งคำขอจากผู้ใช้รายนี้ เราจะสามารถดำเนินการทั้งหมดได้

 //... const { role, email, uid } = res.locals const { id } = req.params if (email === '[email protected]') return next(); //...

ตอนนี้ มาเพิ่มการดำเนินการ CRUD ที่เหลือใน src/users/routes-config.ts

สำหรับการดำเนินการที่จะได้รับหรืออัปเดตผู้ใช้รายเดียวโดยที่ :id param ถูกส่ง เรายังอนุญาตให้ผู้ใช้รายเดียวกันดำเนินการการดำเนินการดังกล่าว

 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 ตามบทบาทแล้ว เราก็ปรับใช้กับเว็บและเริ่มใช้งานได้ การปรับใช้กับ Firebase นั้นง่ายมาก เราแค่ต้องเรียกใช้ firebase deploy เมื่อปรับใช้เสร็จสมบูรณ์แล้ว เราสามารถเข้าถึง API ของเราได้ที่ URL ที่เผยแพร่

การรันคำสั่ง firebase deploy

คุณสามารถตรวจสอบ URL ของ API ได้ที่ https://console.firebase.google.com/u/0/project/{your-project}/functions/list

URL ของ API ที่คอนโซล Firebase

ในกรณีของฉัน มันคือ [https://us-central1-joaq-lab.cloudfunctions.net/api]

การใช้ API

เมื่อปรับใช้ API ของเราแล้ว เรามีวิธีใช้งานหลายวิธี ในบทช่วยสอนนี้ ฉันจะอธิบายวิธีใช้งานผ่านบุรุษไปรษณีย์หรือจากแอป Angular

หากเราป้อน URL ของรายการผู้ใช้ทั้งหมด ( /api/users ) บนเบราว์เซอร์ใดๆ เราจะได้รับสิ่งต่อไปนี้:

Firebase Authentication API

เหตุผลก็คือเมื่อส่งคำขอจากเบราว์เซอร์ เรากำลังดำเนินการคำขอ GET โดยไม่มีส่วนหัวตรวจสอบสิทธิ์ ซึ่งหมายความว่า API ของเราใช้งานได้จริงตามที่คาดไว้!

API ของเราได้รับการรักษาความปลอดภัยผ่านโทเค็น ในการสร้างโทเค็นดังกล่าว เราจำเป็นต้องเรียก SDK ไคลเอ็นต์ของ Firebase และเข้าสู่ระบบด้วยข้อมูลรับรองผู้ใช้/รหัสผ่านที่ถูกต้อง เมื่อสำเร็จแล้ว Firebase จะส่งโทเค็นกลับมาในการตอบสนอง ซึ่งเราสามารถเพิ่มไปยังส่วนหัวของคำขอใดๆ ต่อไปนี้ที่เราต้องการดำเนินการ

จากแอปเชิงมุม

ในบทช่วยสอนนี้ ฉันจะพูดถึงส่วนสำคัญๆ ในการใช้ API จากแอป Angular คุณสามารถเข้าถึงที่เก็บแบบเต็มได้ที่นี่ และหากคุณต้องการคำแนะนำทีละขั้นตอนเกี่ยวกับวิธีสร้างแอป Angular และกำหนดค่า @angular/fire ให้ใช้ คุณสามารถตรวจสอบโพสต์นี้ได้

กลับไปที่การลงชื่อเข้าใช้ เราจะมี SignInComponent พร้อม <form> เพื่อให้ผู้ใช้ป้อนชื่อผู้ใช้และรหัสผ่าน

 //... <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 ได้

ลงชื่อเข้าใช้ผ่านแอป Angular

การตอบสนอง API เมื่อลงชื่อเข้าใช้จากแอป Angular

และเมื่อเราตรวจสอบคำขอเครือข่ายใน DevTools เราจะเห็นว่า Firebase ส่งคืนโทเค็นหลังจากตรวจสอบผู้ใช้และรหัสผ่านของเราแล้ว

โทเค็นนี้เป็นโทเค็นที่เราจะใช้เพื่อส่งคำขอส่วนหัวของเราไปยัง API ที่เราสร้างขึ้น วิธีหนึ่งในการเพิ่มโทเค็นให้กับคำขอทั้งหมดคือการใช้ HttpInterceptor

ไฟล์นี้แสดงวิธีรับโทเค็นจาก AngularFireAuth และเพิ่มไปยังคำขอของส่วนหัว จากนั้นเราจะจัดเตรียมไฟล์ interceptor ใน 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 { }

เมื่อตั้งค่า interceptor แล้ว เราสามารถส่งคำขอไปยัง API ของเราได้จาก httpClient ตัวอย่างเช่น นี่คือ 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)) ) } //...

และนี่คือผลลัพธ์

ผู้ใช้ทั้งหมดในแอพ Angular ของเรา

สังเกตว่าถ้าเราลงชื่อเข้าใช้ด้วยผู้ใช้ที่ role=user เฉพาะส่วน Me เท่านั้นที่จะแสดง

มุมมองของทรัพยากรผู้ใช้ที่ผู้ใช้ที่มีบทบาทที่ผู้ใช้มีสิทธิ์เข้าถึง

และเราจะได้รับ 403 จากตัวตรวจสอบเครือข่าย นี่เป็นเพราะข้อจำกัดที่เราตั้งไว้ก่อนหน้านี้ใน API ให้อนุญาตเฉพาะ “ผู้ดูแลระบบ” เพื่อแสดงรายการผู้ใช้ทั้งหมด

ข้อผิดพลาด 403 ในตัวตรวจสอบเครือข่าย

ตอนนี้ มาเพิ่มฟังก์ชัน "สร้างผู้ใช้" และ "แก้ไขผู้ใช้" ในการทำเช่นนั้น ให้สร้าง 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">&times;</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 => { }); }

จากบุรุษไปรษณีย์

บุรุษไปรษณีย์เป็นเครื่องมือในการสร้างและส่งคำขอไปยัง API ด้วยวิธีนี้ เราสามารถจำลองว่าเรากำลังเรียก API ของเราจากแอปไคลเอ็นต์หรือบริการอื่น

สิ่งที่เราจะสาธิตคือวิธีส่งคำขอเพื่อแสดงรายการผู้ใช้ทั้งหมด

เมื่อเราเปิดเครื่องมือแล้ว เราจะตั้งค่า URL https://us-central1-{your-project}.cloudfunctions.net/api/users:

API URL ที่โหลดในช่องบุรุษไปรษณีย์พร้อมที่จะเริ่มทำงานเป็นคำขอ GET

ต่อไป ในการอนุญาตแท็บ เราเลือก Bearer Token และตั้งค่าที่เราแยกจาก Dev Tools ก่อนหน้านี้

การตั้งค่าโทเค็นผู้ถือในบุรุษไปรษณีย์

เนื้อความของการตอบสนองที่เราได้รับ

บทสรุป

ยินดีด้วย! คุณผ่านบทช่วยสอนทั้งหมดแล้ว และตอนนี้คุณได้เรียนรู้การสร้าง API ตามบทบาทของผู้ใช้บน Firebase แล้ว

นอกจากนี้เรายังครอบคลุมถึงวิธีการใช้งานจากแอป Angular และบุรุษไปรษณีย์

มาสรุปสิ่งที่สำคัญที่สุด:

  1. Firebase ช่วยให้คุณเริ่มต้นใช้งานได้อย่างรวดเร็วด้วย API การตรวจสอบสิทธิ์ระดับองค์กร ซึ่งคุณสามารถขยายได้ในภายหลัง
  2. เกือบทุกโครงการต้องมีการให้สิทธิ์ หากคุณต้องการควบคุมการเข้าถึงโดยใช้แบบจำลองตามบทบาท การตรวจสอบสิทธิ์ของ Firebase จะช่วยให้คุณเริ่มต้นได้อย่างรวดเร็ว
  3. โมเดลตามบทบาทอาศัยการตรวจสอบความถูกต้องของทรัพยากรที่ร้องขอจากผู้ใช้ที่มีบทบาทเฉพาะกับผู้ใช้เฉพาะ
  4. การใช้แอป Express.js บนฟังก์ชัน Firebase เราสามารถสร้าง REST API และตั้งค่าตัวจัดการเพื่อรับรองความถูกต้องและอนุญาตคำขอได้
  5. คุณสามารถสร้าง API การตรวจสอบสิทธิ์ตามบทบาทและรักษาความปลอดภัยแอปของคุณด้วยการใช้ประโยชน์จากการอ้างสิทธิ์ที่กำหนดเองในตัว

คุณสามารถอ่านเพิ่มเติมเกี่ยวกับ Firebase auth ได้ที่นี่ และถ้าคุณต้องการใช้ประโยชน์จากบทบาทที่เรากำหนดไว้ คุณสามารถใช้ @angular/fire helpers