การสร้าง Secure REST API ใน Node.js

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

Application Programming Interface (API) มีอยู่ทุกที่ ช่วยให้ซอฟต์แวร์สามารถสื่อสารกับชิ้นส่วนของซอฟต์แวร์อื่นๆ—ภายในหรือภายนอก—อย่างสม่ำเสมอ ซึ่งเป็นส่วนประกอบสำคัญในความสามารถในการปรับขนาด โดยไม่ต้องกล่าวถึงความสามารถในการนำกลับมาใช้ใหม่

ทุกวันนี้ เป็นเรื่องปกติที่บริการออนไลน์จะมี API ที่เปิดเผยต่อสาธารณะ สิ่งเหล่านี้ช่วยให้นักพัฒนารายอื่นสามารถรวมคุณสมบัติต่างๆ เช่น การเข้าสู่ระบบโซเชียลมีเดีย การชำระเงินด้วยบัตรเครดิต และการติดตามพฤติกรรมได้อย่างง่ายดาย มาตรฐาน โดยพฤตินัย ที่ใช้สำหรับสิ่งนี้เรียกว่า REpresentational State Transfer (REST)

ในขณะที่สามารถใช้แพลตฟอร์มและภาษาโปรแกรมมากมายสำหรับงาน เช่น ASP.NET Core, Laravel (PHP) หรือ Bottle (Python) ในบทช่วยสอนนี้ เราจะสร้าง REST API แบ็คเอนด์พื้นฐานแต่ปลอดภัยโดยใช้ กองต่อไปนี้:

  • Node.js ซึ่งผู้อ่านน่าจะคุ้นเคยบ้างแล้ว
  • Express ซึ่งช่วยลดความยุ่งยากอย่างมากในการสร้างงานเว็บเซิร์ฟเวอร์ทั่วไปภายใต้ Node.js และเป็นค่าโดยสารมาตรฐานในการสร้าง REST API แบ็กเอนด์
  • Mongoose ซึ่งจะเชื่อมต่อส่วนหลังของเรากับฐานข้อมูล MongoDB

นักพัฒนาที่ทำตามบทช่วยสอนนี้ควรคุ้นเคยกับเทอร์มินัล (หรือพรอมต์คำสั่ง)

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

กายวิภาคของ REST API

REST API ใช้ในการเข้าถึงและจัดการข้อมูลโดยใช้ชุดปฏิบัติการไร้สัญชาติทั่วไป การดำเนินการเหล่านี้เป็นส่วนสำคัญของโปรโตคอล HTTP และแสดงถึงฟังก์ชันการสร้าง อ่าน อัปเดต และลบ (CRUD) ที่จำเป็น แม้ว่าจะไม่ใช่แบบตัวต่อตัวที่สะอาด:

  • POST (สร้างทรัพยากรหรือให้ข้อมูลโดยทั่วไป)
  • GET (ดึงดัชนีของทรัพยากรหรือทรัพยากรแต่ละรายการ)
  • PUT (สร้างหรือแทนที่ทรัพยากร)
  • PATCH (อัปเดต/แก้ไขทรัพยากร)
  • DELETE (ลบทรัพยากร)

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

ในตอนนี้ มาเริ่มสร้าง REST API ที่ปลอดภัยของเราโดยใช้ Node.js!

ในบทช่วยสอนนี้ เราจะสร้าง REST API ที่ค่อนข้างธรรมดา (และใช้งานได้จริง) สำหรับทรัพยากรที่เรียกว่า users

ทรัพยากรของเราจะมีโครงสร้างพื้นฐานดังต่อไปนี้:

  • id (UUID ที่สร้างขึ้นโดยอัตโนมัติ)
  • firstName
  • lastName
  • email
  • password
  • ระดับการ permissionLevel (ผู้ใช้นี้ได้รับอนุญาตให้ทำอะไร)

และเราจะสร้างการดำเนินการต่อไปนี้สำหรับทรัพยากรนั้น:

  • POST ที่ปลายทาง /users (สร้างผู้ใช้ใหม่)
  • GET ที่ปลายทาง /users (แสดงรายการผู้ใช้ทั้งหมด)
  • GET ที่ปลายทาง /users/:userId (รับผู้ใช้เฉพาะ)
  • PATCH ที่จุดปลาย /users/:userId (อัปเดตข้อมูลสำหรับผู้ใช้เฉพาะ)
  • DELETE ที่ปลายทาง /users/:userId (ลบผู้ใช้เฉพาะ)

เราจะใช้โทเค็นเว็บ JSON (JWT) สำหรับโทเค็นการเข้าถึง ด้วยเหตุนี้ เราจะสร้างทรัพยากรอื่นที่เรียกว่า auth ซึ่งคาดว่าจะได้รับอีเมลและรหัสผ่านของผู้ใช้ และในทางกลับกัน จะสร้างโทเค็นที่ใช้สำหรับการตรวจสอบสิทธิ์ในการดำเนินการบางอย่าง (บทความที่ยอดเยี่ยมของ Dejan Milosevic เกี่ยวกับ JWT สำหรับแอปพลิเคชัน REST ที่ปลอดภัยใน Java มีรายละเอียดเพิ่มเติมเกี่ยวกับเรื่องนี้ หลักการก็เหมือนกัน)

ตั้งค่าการสอน REST API

ก่อนอื่น ตรวจสอบให้แน่ใจว่าคุณได้ติดตั้ง Node.js เวอร์ชันล่าสุดแล้ว สำหรับบทความนี้ ฉันจะใช้เวอร์ชัน 14.9.0; มันอาจใช้งานได้กับรุ่นเก่ากว่า

ถัดไป ตรวจสอบให้แน่ใจว่าคุณได้ติดตั้ง MongoDB แล้ว เราจะไม่อธิบายรายละเอียดเฉพาะของ Mongoose และ MongoDB ที่ใช้ในที่นี้ แต่เพื่อให้พื้นฐานใช้งานได้ เพียงเริ่มเซิร์ฟเวอร์ในโหมดโต้ตอบ (เช่น จากบรรทัดคำสั่งเป็น mongo ) แทนที่จะเป็นบริการ นั่นเป็นเพราะว่า ณ จุดหนึ่งในบทช่วยสอนนี้ เราจะต้องโต้ตอบกับ MongoDB โดยตรง แทนที่จะใช้โค้ด Node.js ของเรา

หมายเหตุ: ด้วย MongoDB คุณไม่จำเป็นต้องสร้างฐานข้อมูลเฉพาะอย่างที่อาจมีในสถานการณ์ RDBMS บางอย่าง การเรียกแทรกครั้งแรกจากโค้ด Node.js ของเราจะทริกเกอร์การสร้างโดยอัตโนมัติ

บทช่วยสอนนี้ไม่มีโค้ดทั้งหมดที่จำเป็นสำหรับโครงการที่ทำงาน มีจุดประสงค์ให้คุณโคลน repo ที่แสดงร่วมและเพียงแค่ทำตามไฮไลท์ในขณะที่คุณอ่าน แต่คุณยังสามารถคัดลอกในไฟล์เฉพาะและตัวอย่างจาก repo ได้ตามต้องการ หากต้องการ

ไปที่โฟลเดอร์ rest-api-tutorial/ ที่เป็นผลลัพธ์ในเทอร์มินัลของคุณ คุณจะเห็นว่าโครงการของเรามีสามโฟลเดอร์โมดูล:

  • common (การจัดการบริการที่ใช้ร่วมกันทั้งหมด และข้อมูลที่ใช้ร่วมกันระหว่างโมดูลผู้ใช้)
  • users (ทุกอย่างเกี่ยวกับผู้ใช้)
  • auth (จัดการการสร้าง JWT และโฟลว์การเข้าสู่ระบบ)

ตอนนี้ให้รัน npm install (หรือ yarn ถ้ามี)

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

การสร้างโมดูลผู้ใช้

เราจะใช้ Mongoose ซึ่งเป็นไลบรารี่การสร้างแบบจำลองข้อมูลวัตถุ (ODM) สำหรับ MongoDB เพื่อสร้างโมเดลผู้ใช้ภายในสคีมาผู้ใช้

ขั้นแรก เราต้องสร้าง Mongoose schema ใน /users/models/users.model.js :

 const userSchema = new Schema({ firstName: String, lastName: String, email: String, password: String, permissionLevel: Number });

เมื่อเรากำหนดสคีมาแล้ว เราสามารถแนบสคีมากับโมเดลผู้ใช้ได้อย่างง่ายดาย

 const userModel = mongoose.model('Users', userSchema);

หลังจากนั้น เราสามารถใช้โมเดลนี้เพื่อปรับใช้การดำเนินการ CRUD ทั้งหมดที่เราต้องการภายในปลายทาง Express ของเรา

เริ่มต้นด้วยการดำเนินการ "สร้างผู้ใช้" โดยกำหนดเส้นทางใน users/routes.config.js :

 app.post('/users', [ UsersController.insert ]);

สิ่งนี้ถูกดึงเข้าสู่แอพ Express ของเราในไฟล์ index.js หลัก ออบเจ็กต์ UsersController นำเข้าจากคอนโทรลเลอร์ของเรา โดยที่เราแฮชรหัสผ่านอย่างเหมาะสม ซึ่งกำหนดไว้ใน /users/controllers/users.controller.js :

 exports.insert = (req, res) => { let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512',salt) .update(req.body.password) .digest("base64"); req.body.password = salt + "$" + hash; req.body.permissionLevel = 1; UserModel.createUser(req.body) .then((result) => { res.status(201).send({id: result._id}); }); };

ณ จุดนี้ เราสามารถทดสอบโมเดล Mongoose ของเราได้ด้วยการรันเซิร์ฟเวอร์ ( npm start ) และส่งคำขอ POST ไปยัง /users ด้วยข้อมูล JSON บางส่วน:

 { "firstName" : "Marcos", "lastName" : "Silva", "email" : "[email protected]", "password" : "s3cr3tp4sswo4rd" }

มีเครื่องมือหลายอย่างที่คุณสามารถใช้ได้ อาการนอนไม่หลับ (อธิบายด้านล่าง) และบุรุษไปรษณีย์เป็นเครื่องมือ GUI ยอดนิยม และ curl เป็นตัวเลือก CLI ทั่วไป คุณยังสามารถใช้ JavaScript เช่น จากคอนโซลเครื่องมือการพัฒนาในตัวของเบราว์เซอร์:

 fetch('http://localhost:3600/users', { method: 'POST', headers: { "Content-type": "application/json" }, body: JSON.stringify({ "firstName": "Marcos", "lastName": "Silva", "email": "[email protected]", "password": "s3cr3tp4sswo4rd" }) }) .then(function(response) { return response.json(); }) .then(function(data) { console.log('Request succeeded with JSON response', data); }) .catch(function(error) { console.log('Request failed', error); });

ณ จุดนี้ ผลลัพธ์ของโพสต์ที่ถูกต้องจะเป็นเพียง id จากผู้ใช้ที่สร้าง: { "id": "5b02c5c84817bf28049e58a3" } เราจำเป็นต้องเพิ่มเมธอด createUser ให้กับโมเดลด้วย users/models/users.model.js :

 exports.createUser = (userData) => { const user = new User(userData); return user.save(); };

เรียบร้อย ตอนนี้เราต้องดูว่ามีผู้ใช้อยู่หรือไม่ สำหรับสิ่งนั้น เราจะใช้คุณลักษณะ "รับผู้ใช้ด้วย id" สำหรับปลายทางต่อไปนี้: users/:userId

ขั้นแรก เราสร้างเส้นทางใน /users/routes/config.js :

 app.get('/users/:userId', [ UsersController.getById ]);

จากนั้น เราสร้างคอนโทรลเลอร์ใน /users/controllers/users.controller.js :

 exports.getById = (req, res) => { UserModel.findById(req.params.userId).then((result) => { res.status(200).send(result); }); };

และสุดท้าย เพิ่มเมธอด findById ให้กับโมเดลใน /users/models/users.model.js :

 exports.findById = (id) => { return User.findById(id).then((result) => { result = result.toJSON(); delete result._id; delete result.__v; return result; }); };

คำตอบจะเป็นดังนี้:

 { "firstName": "Marcos", "lastName": "Silva", "email": "[email protected]", "password": "Y+XZEaR7J8xAQCc37nf1rw==$p8b5ykUx6xpC6k8MryDaRmXDxncLumU9mEVabyLdpotO66Qjh0igVOVerdqAh+CUQ4n/E0z48mp8SDTpX2ivuQ==", "permissionLevel": 1, "id": "5b02c5c84817bf28049e58a3" }

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

ทำซ้ำรูปแบบที่วางไว้ด้านบน ตอนนี้เราสามารถเพิ่มฟังก์ชันการทำงานเพื่ออัปเดตผู้ใช้ได้ เราจะใช้การดำเนินการ PATCH เนื่องจากจะทำให้เราสามารถส่งเฉพาะฟิลด์ที่เราต้องการเปลี่ยน ดังนั้นเส้นทางจะเป็น PATCH ไปยัง /users/:userid และเราจะส่งฟิลด์ใด ๆ ที่เราต้องการเปลี่ยน นอกจากนี้เรายังต้องใช้การตรวจสอบเพิ่มเติม เนื่องจากการเปลี่ยนแปลงควรจำกัดเฉพาะผู้ใช้ที่มีปัญหาหรือผู้ดูแลระบบ และมีเพียงผู้ดูแลระบบเท่านั้นที่สามารถเปลี่ยน permissionLevel เราจะข้ามขั้นตอนนั้นไปก่อนและกลับไปแก้ไขเมื่อเราใช้โมดูลการตรวจสอบสิทธิ์ สำหรับตอนนี้คอนโทรลเลอร์ของเราจะมีลักษณะดังนี้:

 exports.patchById = (req, res) => { if (req.body.password){ let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest("base64"); req.body.password = salt + "$" + hash; } UserModel.patchUser(req.params.userId, req.body).then((result) => { res.status(204).send({}); }); };

โดยค่าเริ่มต้น เราจะส่งรหัส HTTP 204 โดยไม่มีเนื้อหาตอบกลับเพื่อระบุว่าคำขอสำเร็จ

และเราจะต้องเพิ่มเมธอด patchUser ให้กับโมเดล:

 exports.patchUser = (id, userData) => { return User.findOneAndUpdate({ _id: id }, userData); };

รายชื่อผู้ใช้จะถูกนำไปใช้เป็น GET ที่ /users/ โดยตัวควบคุมต่อไปนี้:

 exports.list = (req, res) => { let limit = req.query.limit && req.query.limit <= 100 ? parseInt(req.query.limit) : 10; let page = 0; if (req.query) { if (req.query.page) { req.query.page = parseInt(req.query.page); page = Number.isInteger(req.query.page) ? req.query.page : 0; } } UserModel.list(limit, page).then((result) => { res.status(200).send(result); }) };

วิธีการของแบบจำลองที่เกี่ยวข้องจะเป็น:

 exports.list = (perPage, page) => { return new Promise((resolve, reject) => { User.find() .limit(perPage) .skip(perPage * page) .exec(function (err, users) { if (err) { reject(err); } else { resolve(users); } }) }); };

การตอบสนองรายการที่เป็นผลลัพธ์จะมีโครงสร้างดังต่อไปนี้:

 [ { "firstName": "Marco", "lastName": "Silva", "email": "[email protected]", "password": "z4tS/DtiH+0Gb4J6QN1K3w==$al6sGxKBKqxRQkDmhnhQpEB6+DQgDRH2qr47BZcqLm4/fphZ7+a9U+HhxsNaSnGB2l05Oem/BLIOkbtOuw1tXA==", "permissionLevel": 1, "id": "5b02c5c84817bf28049e58a3" }, { "firstName": "Paulo", "lastName": "Silva", "email": "[email protected]", "password": "wTsqO1kHuVisfDIcgl5YmQ==$cw7RntNrNBNw3MO2qLbx959xDvvrDu4xjpYfYgYMxRVDcxUUEgulTlNSBJjiDtJ1C85YimkMlYruU59rx2zbCw==", "permissionLevel": 1, "id": "5b02d038b653603d1ca69729" } ]

และส่วนสุดท้ายที่จะดำเนินการคือ DELETE ที่ /users/:userId

ตัวควบคุมของเราสำหรับการลบจะเป็น:

 exports.removeById = (req, res) => { UserModel.removeById(req.params.userId) .then((result)=>{ res.status(204).send({}); }); };

เหมือนเดิม ตัวควบคุมจะส่งคืนรหัส HTTP 204 และไม่มีเนื้อหาเป็นการยืนยัน

วิธีการของแบบจำลองที่เกี่ยวข้องควรมีลักษณะดังนี้:

 exports.removeById = (userId) => { return new Promise((resolve, reject) => { User.deleteMany({_id: userId}, (err) => { if (err) { reject(err); } else { resolve(err); } }); }); };

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

การสร้างโมดูลการตรวจสอบสิทธิ์

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

ขั้นแรก เราจะสร้างปลายทางสำหรับคำขอ POST ไปยังทรัพยากร /auth เนื้อหาคำขอจะมีอีเมลผู้ใช้และรหัสผ่าน:

 { "email" : "[email protected]", "password" : "s3cr3tp4sswo4rd2" }

ก่อนที่เราจะติดต่อกับผู้ควบคุม เราควรตรวจสอบผู้ใช้ใน /authorization/middlewares/verify.user.middleware.js :

 exports.isPasswordAndUserMatch = (req, res, next) => { UserModel.findByEmail(req.body.email) .then((user)=>{ if(!user[0]){ res.status(404).send({}); }else{ let passwordFields = user[0].password.split('$'); let salt = passwordFields[0]; let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest("base64"); if (hash === passwordFields[1]) { req.body = { userId: user[0]._id, email: user[0].email, permissionLevel: user[0].permissionLevel, provider: 'email', name: user[0].firstName + ' ' + user[0].lastName, }; return next(); } else { return res.status(400).send({errors: ['Invalid email or password']}); } } }); };

เมื่อทำเสร็จแล้ว เราสามารถไปยังคอนโทรลเลอร์และสร้าง JWT:

 exports.login = (req, res) => { try { let refreshId = req.body.userId + jwtSecret; let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512', salt).update(refreshId).digest("base64"); req.body.refreshKey = salt; let token = jwt.sign(req.body, jwtSecret); let b = Buffer.from(hash); let refresh_token = b.toString('base64'); res.status(201).send({accessToken: token, refreshToken: refresh_token}); } catch (err) { res.status(500).send({errors: err}); } };

แม้ว่าเราจะไม่รีเฟรชโทเค็นในบทช่วยสอนนี้ แต่คอนโทรลเลอร์ได้รับการตั้งค่าเพื่อให้สามารถใช้งานรุ่นดังกล่าวเพื่อให้ง่ายต่อการนำไปใช้ในการพัฒนาในภายหลัง

ทั้งหมดที่เราต้องการตอนนี้คือการสร้างเส้นทางและเรียกใช้มิดเดิลแวร์ที่เหมาะสมใน /authorization/routes.config.js :

 app.post('/auth', [ VerifyUserMiddleware.hasAuthValidFields, VerifyUserMiddleware.isPasswordAndUserMatch, AuthorizationController.login ]);

การตอบสนองจะมี JWT ที่สร้างขึ้นในฟิลด์ accessToken:

 { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1YjAyYzVjODQ4MTdiZjI4MDQ5ZTU4YTMiLCJlbWFpbCI6Im1hcmNvcy5oZW5yaXF1ZUB0b3B0YWwuY29tIiwicGVybWlzc2lvbkxldmVsIjoxLCJwcm92aWRlciI6ImVtYWlsIiwibmFtZSI6Ik1hcmNvIFNpbHZhIiwicmVmcmVzaF9rZXkiOiJiclhZUHFsbUlBcE1PakZIRG1FeENRPT0iLCJpYXQiOjE1MjY5MjMzMDl9.mmNg-i44VQlUEWP3YIAYXVO-74803v1mu-y9QPUQ5VY", "refreshToken": "U3BDQXBWS3kyaHNDaGJNanlJTlFkSXhLMmFHMzA2NzRsUy9Sd2J0YVNDTmUva0pIQ0NwbTJqOU5YZHgxeE12NXVlOUhnMzBWMGNyWmdOTUhSaTdyOGc9PQ==" }

เมื่อสร้างโทเค็นแล้ว เราสามารถนำมาใช้ภายในส่วนหัว Authorization โดยใช้แบบฟอร์ม Bearer ACCESS_TOKEN

การสร้างการอนุญาตและการตรวจสอบมิดเดิลแวร์

สิ่งแรกที่เราควรกำหนดคือใครสามารถ users ทรัพยากรผู้ใช้ได้ นี่คือสถานการณ์ที่เราจะต้องจัดการ:

  • สาธารณะสำหรับการสร้างผู้ใช้ (ขั้นตอนการลงทะเบียน) เราจะไม่ใช้ JWT สำหรับสถานการณ์นี้
  • ส่วนตัวสำหรับผู้ใช้ที่เข้าสู่ระบบและสำหรับผู้ดูแลระบบเพื่ออัปเดตผู้ใช้นั้น
  • ส่วนตัวสำหรับผู้ดูแลระบบเท่านั้นสำหรับการลบบัญชีผู้ใช้

เมื่อระบุสถานการณ์เหล่านี้แล้ว ก่อนอื่นเราจะต้องใช้มิดเดิลแวร์ที่จะตรวจสอบผู้ใช้เสมอหากพวกเขาใช้ JWT ที่ถูกต้อง มิดเดิลแวร์ใน /common/middlewares/auth.validation.middleware.js สามารถทำได้ง่ายๆ ดังนี้:

 exports.validJWTNeeded = (req, res, next) => { if (req.headers['authorization']) { try { let authorization = req.headers['authorization'].split(' '); if (authorization[0] !== 'Bearer') { return res.status(401).send(); } else { req.jwt = jwt.verify(authorization[1], secret); return next(); } } catch (err) { return res.status(403).send(); } } else { return res.status(401).send(); } };

เราจะใช้รหัสข้อผิดพลาด HTTP เพื่อจัดการข้อผิดพลาดของคำขอ:

  • HTTP 401 สำหรับคำขอที่ไม่ถูกต้อง
  • HTTP 403 สำหรับคำขอที่ถูกต้องด้วยโทเค็นที่ไม่ถูกต้อง หรือโทเค็นที่ถูกต้องด้วยการอนุญาตที่ไม่ถูกต้อง

เราสามารถใช้ตัวดำเนินการระดับบิต AND (bitmasking) เพื่อควบคุมการอนุญาต หากเราตั้งค่าการอนุญาตที่จำเป็นแต่ละรายการเป็นกำลัง 2 เราสามารถถือว่าแต่ละบิตของจำนวนเต็ม 32 บิตเป็นสิทธิ์เดียว ผู้ดูแลระบบสามารถมีสิทธิ์ทั้งหมดโดยตั้งค่าการอนุญาตเป็น 2147483647 ผู้ใช้นั้นสามารถเข้าถึงเส้นทางใดก็ได้ อีกตัวอย่างหนึ่ง ผู้ใช้ที่ตั้งค่าการอนุญาตเป็น 7 จะมีสิทธิ์ในบทบาทที่ทำเครื่องหมายด้วยบิตสำหรับค่า 1, 2 และ 4 (สองตัวยกกำลัง 0, 1 และ 2)

มิดเดิลแวร์สำหรับสิ่งนั้นจะมีลักษณะดังนี้:

 exports.minimumPermissionLevelRequired = (required_permission_level) => { return (req, res, next) => { let user_permission_level = parseInt(req.jwt.permission_level); let user_id = req.jwt.user_id; if (user_permission_level & required_permission_level) { return next(); } else { return res.status(403).send(); } }; };

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

ตอนนี้ เราต้องเพิ่มมิดเดิลแวร์การพิสูจน์ตัวตนไปยังเส้นทางโมดูลของผู้ใช้ใน /users/routes.config.js :

 app.post('/users', [ UsersController.insert ]); app.get('/users', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(PAID), UsersController.list ]); app.get('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(FREE), PermissionMiddleware.onlySameUserOrAdminCanDoThisAction, UsersController.getById ]); app.patch('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(FREE), PermissionMiddleware.onlySameUserOrAdminCanDoThisAction, UsersController.patchById ]); app.delete('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(ADMIN), UsersController.removeById ]);

สรุปการพัฒนาพื้นฐานของ REST API ของเรา สิ่งที่ต้องทำคือการทดสอบทั้งหมด

วิ่งและทดสอบอาการนอนไม่หลับ

Insomnia เป็นไคลเอนต์ REST ที่ดีพร้อมเวอร์ชันฟรีที่ดี แนวทางปฏิบัติที่ดีที่สุดคือการรวมการทดสอบโค้ดและใช้งานการรายงานข้อผิดพลาดที่เหมาะสมในโปรเจ็กต์ แต่ไคลเอ็นต์ REST ของบริษัทอื่นนั้นยอดเยี่ยมสำหรับการทดสอบและใช้งานโซลูชันของบุคคลที่สามเมื่อไม่มีการรายงานข้อผิดพลาดและการดีบั๊กของบริการ เราจะใช้มันที่นี่เพื่อเล่นบทบาทของแอปพลิเคชันและรับข้อมูลเชิงลึกเกี่ยวกับสิ่งที่เกิดขึ้นกับ API ของเรา

ในการสร้างผู้ใช้ เราเพียงแค่โพส POST ฟิลด์ที่จำเป็นไปยังปลายทางที่เหมาะสม และเก็บ ID ที่สร้างขึ้นเพื่อใช้ในภายหลัง

ขอข้อมูลที่เหมาะสมสำหรับการสร้างผู้ใช้

API จะตอบสนองด้วย ID ผู้ใช้:

ตอบกลับการยืนยันด้วย ID ผู้ใช้

ตอนนี้เราสามารถสร้าง JWT โดยใช้ /auth/ endpoint:

ขอข้อมูลเข้าสู่ระบบ

เราควรได้รับโทเค็นเป็นคำตอบของเรา:

การยืนยันที่มี JSON Web Token . ที่สอดคล้องกัน

คว้า accessToken นำหน้าด้วย Bearer (จำช่องว่าง) และเพิ่มไปยังส่วนหัวของคำขอภายใต้ Authorization :

การตั้งค่าส่วนหัวเพื่อถ่ายโอนมี JWT . ที่ตรวจสอบสิทธิ์

หากเราไม่ดำเนินการตอนนี้เมื่อเราได้ใช้มิดเดิลแวร์การอนุญาต ทุกคำขออื่นนอกเหนือจากการลงทะเบียนจะส่งกลับรหัส HTTP 401 ด้วยโทเค็นที่ถูกต้อง อย่างไรก็ตาม เราได้รับการตอบสนองต่อไปนี้จาก /users/:userId :

ตอบกลับแสดงรายการข้อมูลสำหรับผู้ใช้ที่ระบุ

ดังที่ได้กล่าวไว้ก่อนหน้านี้ เรากำลังแสดงทุกสาขา เพื่อจุดประสงค์ทางการศึกษาและเพื่อความเรียบง่าย รหัสผ่าน (ที่แฮชหรืออย่างอื่น) ไม่ควรปรากฏในการตอบสนอง

ลองรับรายชื่อผู้ใช้:

ขอรายชื่อผู้ใช้ทั้งหมด

เซอร์ไพรส์! เราได้รับการตอบสนอง 403

การดำเนินการถูกปฏิเสธเนื่องจากขาดระดับการอนุญาตที่เหมาะสม

ผู้ใช้ของเราไม่มีสิทธิ์เข้าถึงปลายทางนี้ เราจะต้องเปลี่ยนระดับการ permissionLevel ของผู้ใช้ของเราจาก 1 เป็น 7 (หรือถึง 5 ระดับก็ได้ เนื่องจากระดับการอนุญาตแบบฟรีและแบบชำระเงินจะแสดงเป็น 1 และ 4 ตามลำดับ) เราสามารถทำได้ด้วยตนเองใน MongoDB ที่พรอมต์แบบโต้ตอบ เช่นนี้ (โดยที่ ID เปลี่ยนเป็นผลลัพธ์ในเครื่องของคุณ):

 db.users.update({"_id" : ObjectId("5b02c5c84817bf28049e58a3")},{$set:{"permissionLevel":5}})

จากนั้น เราต้องสร้าง JWT ใหม่

หลังจากทำเสร็จแล้ว เราได้รับการตอบสนองที่เหมาะสม:

ตอบกลับผู้ใช้และข้อมูลทั้งหมด

ต่อไป มาทดสอบฟังก์ชันการอัพเดทโดยส่งคำขอ PATCH พร้อมบางฟิลด์ไปยังปลายทาง /users/:userId ของเรา:

คำขอที่มีข้อมูลบางส่วนที่จะอัปเดต

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

ตอบสนองหลังจากการเปลี่ยนแปลงสำเร็จ

สุดท้ายเราต้องลบผู้ใช้ เราจะต้องสร้างผู้ใช้ใหม่ตามที่อธิบายไว้ข้างต้น (อย่าลืมจด ID ผู้ใช้) และตรวจสอบให้แน่ใจว่าเรามี JWT ที่เหมาะสมสำหรับผู้ใช้ที่เป็นผู้ดูแลระบบ ผู้ใช้ใหม่จะต้องตั้งค่าการอนุญาตเป็น 2053 (นั่นคือ ADMIN ผู้ดูแลระบบ — บวก 5 ก่อนหน้าของเรา) เพื่อให้สามารถดำเนินการลบได้ เมื่อเสร็จแล้วและสร้าง JWT ใหม่ เราจะต้องอัปเดตส่วนหัวคำขอ Authorization ของเรา:

ขอตั้งค่าการลบผู้ใช้

ส่งคำขอ DELETE ไปยัง /users/:userId เราควรได้รับการตอบกลับ 204 เป็นการยืนยัน เราสามารถยืนยันได้อีกครั้งโดยขอ /users/ เพื่อแสดงรายการผู้ใช้ที่มีอยู่ทั้งหมด

ขั้นตอนต่อไปสำหรับ REST API ของคุณ

ด้วยเครื่องมือและวิธีการที่กล่าวถึงในบทช่วยสอนนี้ คุณน่าจะสร้าง REST API ที่เรียบง่ายและปลอดภัยบน Node.js ได้แล้ว แนวทางปฏิบัติที่ดีที่สุดมากมายที่ไม่จำเป็นต่อกระบวนการถูกข้ามไป ดังนั้นอย่าลืม:

  • ใช้การตรวจสอบความถูกต้องที่เหมาะสม (เช่น ตรวจสอบให้แน่ใจว่าอีเมลของผู้ใช้ไม่ซ้ำกัน)
  • ใช้การทดสอบหน่วยและการรายงานข้อผิดพลาด
  • ป้องกันไม่ให้ผู้ใช้เปลี่ยนระดับการอนุญาตของตนเอง
  • ป้องกันแอดมินไม่ให้ลบตัวเอง
  • ป้องกันการเปิดเผยข้อมูลที่ละเอียดอ่อน (เช่น รหัสผ่านที่แฮช)
  • ย้ายข้อมูลลับ JWT จาก common/config/env.config.js ไปยังกลไกการกระจายความลับแบบ off-repo ที่ไม่ใช่สภาพแวดล้อม

แบบฝึกหัดสุดท้ายสำหรับผู้อ่านคือการแปลง codebase จากการใช้ JavaScript ที่สัญญาไปเป็นเทคนิค async/await

สำหรับผู้ที่อาจสนใจ ตอนนี้มีโครงการเวอร์ชัน TypeScript ให้ใช้งานแล้ว

ที่เกี่ยวข้อง: 5 สิ่งที่คุณไม่เคยทำกับข้อกำหนด REST