วิธีสร้าง 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
สังเกตว่าฟังก์ชั่นถูกเปิดเผยบนเส้นทางที่กำหนดเป็นชื่อของมันที่ '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
และตั้งค่าการเปรียบเทียบอีเมลในรหัส ตอนนี้ เมื่อส่งคำขอจากผู้ใช้รายนี้ เราจะสามารถดำเนินการทั้งหมดได้
//... 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 ที่เผยแพร่
คุณสามารถตรวจสอบ URL ของ API ได้ที่ https://console.firebase.google.com/u/0/project/{your-project}/functions/list
ในกรณีของฉัน มันคือ [https://us-central1-joaq-lab.cloudfunctions.net/api]
การใช้ API
เมื่อปรับใช้ API ของเราแล้ว เรามีวิธีใช้งานหลายวิธี ในบทช่วยสอนนี้ ฉันจะอธิบายวิธีใช้งานผ่านบุรุษไปรษณีย์หรือจากแอป Angular
หากเราป้อน URL ของรายการผู้ใช้ทั้งหมด ( /api/users
) บนเบราว์เซอร์ใดๆ เราจะได้รับสิ่งต่อไปนี้:
เหตุผลก็คือเมื่อส่งคำขอจากเบราว์เซอร์ เรากำลังดำเนินการคำขอ 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 ได้
และเมื่อเราตรวจสอบคำขอเครือข่ายใน 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)) ) } //...
และนี่คือผลลัพธ์
สังเกตว่าถ้าเราลงชื่อเข้าใช้ด้วยผู้ใช้ที่ 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 => { }); }
จากบุรุษไปรษณีย์
บุรุษไปรษณีย์เป็นเครื่องมือในการสร้างและส่งคำขอไปยัง API ด้วยวิธีนี้ เราสามารถจำลองว่าเรากำลังเรียก API ของเราจากแอปไคลเอ็นต์หรือบริการอื่น
สิ่งที่เราจะสาธิตคือวิธีส่งคำขอเพื่อแสดงรายการผู้ใช้ทั้งหมด
เมื่อเราเปิดเครื่องมือแล้ว เราจะตั้งค่า URL https://us-central1-{your-project}.cloudfunctions.net/api/users:
ต่อไป ในการอนุญาตแท็บ เราเลือก Bearer Token และตั้งค่าที่เราแยกจาก Dev Tools ก่อนหน้านี้
บทสรุป
ยินดีด้วย! คุณผ่านบทช่วยสอนทั้งหมดแล้ว และตอนนี้คุณได้เรียนรู้การสร้าง API ตามบทบาทของผู้ใช้บน Firebase แล้ว
นอกจากนี้เรายังครอบคลุมถึงวิธีการใช้งานจากแอป Angular และบุรุษไปรษณีย์
มาสรุปสิ่งที่สำคัญที่สุด:
- Firebase ช่วยให้คุณเริ่มต้นใช้งานได้อย่างรวดเร็วด้วย API การตรวจสอบสิทธิ์ระดับองค์กร ซึ่งคุณสามารถขยายได้ในภายหลัง
- เกือบทุกโครงการต้องมีการให้สิทธิ์ หากคุณต้องการควบคุมการเข้าถึงโดยใช้แบบจำลองตามบทบาท การตรวจสอบสิทธิ์ของ Firebase จะช่วยให้คุณเริ่มต้นได้อย่างรวดเร็ว
- โมเดลตามบทบาทอาศัยการตรวจสอบความถูกต้องของทรัพยากรที่ร้องขอจากผู้ใช้ที่มีบทบาทเฉพาะกับผู้ใช้เฉพาะ
- การใช้แอป Express.js บนฟังก์ชัน Firebase เราสามารถสร้าง REST API และตั้งค่าตัวจัดการเพื่อรับรองความถูกต้องและอนุญาตคำขอได้
- คุณสามารถสร้าง API การตรวจสอบสิทธิ์ตามบทบาทและรักษาความปลอดภัยแอปของคุณด้วยการใช้ประโยชน์จากการอ้างสิทธิ์ที่กำหนดเองในตัว
คุณสามารถอ่านเพิ่มเติมเกี่ยวกับ Firebase auth ได้ที่นี่ และถ้าคุณต้องการใช้ประโยชน์จากบทบาทที่เรากำหนดไว้ คุณสามารถใช้ @angular/fire helpers