เนื้อหาในตอนต่อไปนี้ เราจะกลับมาพูดถึงการใช้งาน API ใน Express
ซึ่งเราได้แนวทางการใช้งานเบื้องต้น ทั้งการใช้งานร่วมกับฐานข้อมูล
การป้องกันการเข้าถึงหรือสิทธิ์การใช้งานด้วย JWT Token เหล่านี้เป็นต้น
สามารถย้อนกลับไปทบทวนได้
สิ่งหนึ่งที่เราไม่ได้พูดถึง คือเวลาที่เราทำการเรียกใช้งาน API ไม่ว่ากรณี Method ใดๆ
ที่ผ่านมา เราทำผ่าน Domain เดียวตลอด นั่นก็คือผ่าน localhost แต่ในสถานการณ์การใช้งานจริง
เมื่อเราจำเป็นต้องนำ API ไปใช้งานร่วมกับ Mobile App ที่รันผ่านมือถือ หรือใช้งานจาก Domian หรือ
เว็บไซต์อื่น เราจะติดปัญหาเรื่อง Cross Domain Orgin นั่นคือ โดยทั่วไป เราไม่สามารถเรียกใช้งาน API
จากเว็บไซต์ หรือโดเมนอื่นได้ จึงเป็นที่มา สำหรับเนื้อหาในการใช้งาน CORS module เพือนำมาใช้งาน
ให้ API รองรับการเรียกใช้งานจาก Domain อื่นๆ หรือเงื่อนไขเพิ่มเติมตามต้องการ
เหมือนเคย CORS คืออะไร ปัญหาโดยละเอียดของ CORS เป็นแบบไหน ส่วนนี้ ต้องหาข้อมูล และทำความ
เข้าไปเพิ่มเติมด้วยตัวเอง keyword : Cross-Origin Resource Sharing (CORS)
เตรียมข้อมูลสำหรับทดสอบ CORS
สำหรับข้อมูลที่เราจะทดสอบการใช้งาน CORS จะใช้เป็นข้อมูลจังหวัดในประเทศไทย ซึ่งในการใช้งานใน MongoDB
เราจำเป็นต้องทำการ import เข้ามาใน provinces collection ก่อน แล้วเราจะสร้าง provinces API ในโปรเจ็ค Express
ของเรา หากเรามีข้อมูลจังหวัดในฐานข้อมูล MySQL ก็สามารถใช้คำสั่ง PHP สร้าง provinces.json สำหรับ ทำการ import
เข้ามาในฐานข้อมูล MongoDB ได้ดังนี้
สมมติเราใช้ตารางข้อมูลจังหวัดใน MySQL จากข้อมูลนี้ http://niik.in/que_2398_6277
ไฟล์ genMongoDBjson.php
<?php /** Error reporting */ error_reporting(E_ALL); ini_set('display_errors', TRUE); ini_set('display_startup_errors', TRUE); $filename = "provinces.json"; // กำหนดชื่อไฟล์ที่ต้องการ header('Content-disposition: attachment; filename='.$filename); header('Content-type: application/json'); // โค้ดไฟล์ dbconnect.php ดูได้ที่ http://niik.in/que_2398_5642 require_once("dbconnect.php"); ?> <?php $json_data = array(); $sql = " SELECT province_id, province_name, province_name_eng FROM tbl_provinces ORDER BY province_id "; $result = $mysqli->query($sql); if($result && $result->num_rows > 0){ while($row = $result->fetch_assoc()){ $json_data[] = array( "_id" => (int)$row['province_id'], "name_th" => $row['province_name'], "name_en" => $row['province_name_eng'] ); } } // แปลง array เป็นรูปแบบ json string if(isset($json_data)){ $json= json_encode($json_data, JSON_UNESCAPED_UNICODE); $patternFind = array('/},{/','/^\[/','/\]$/'); $patternReplace = array("}\r\n{","",""); $json = preg_replace($patternFind, $patternReplace, $json); echo $json; } ?>
เราจะได้ไฟล์ provinces.json สำหรับ import เข้าไปใน MongoDB รูปแบบดังนี้
จากรูปแบบไฟล์ข้างต้น จะเห็นว่า เป็นโครงสร้างข้อมูลที่ไม่ใช่ JSON Data ที่ถูกรูปแบบเสียทีเดียว เพราะแต่ละรายการ
ไม่ได้ถูกคั่นด้วย (,) แต่ใช้เป็นขึ้นบรรทัดใหม่แทน
เมื่อเราได้ไฟล์ provinces.json สำหรับ import แล้ว ให้ copy ไฟล์ไปไว้ในโฟลเดอร์ที่เราต้องการ ในที่นี้เอาไปใว้ใน
โฟลเดอร์ C:\data\json
เราจะทำการ import เข้าไปใน MongoDB Server ที่อยู่ในเครื่องผ่าน shell หรือ command line ด้วยรูปแบบคำสั่งดังนี้
กรณีบน cloud server
mongoimport --host <HOST>:<PORT> --ssl --username mangouser --password <PASSWORD> --authenticationDatabase admin --db <DATABASE> --collection <COLLECTION> --type <FILETYPE> --file <FILENAME>
กรณีที่เครื่อง
mongoimport --db testdb --collection provinces ^ --authenticationDatabase admin --username <user> --password <password> ^ --drop --file c:/data/json/provinces.json
แต่เนื่องจากในเครื่องเรา ไม่ได้กำหนด username หรือ password ดังนั้นเราตัดส่วนนี้ออก จะได้เป็น
mongoimport --db testdb --collection provinces ^ --drop --file c:/data/json/provinces.json
รูปแบบคำสั่งข้างต้น เราสามารถ copy แล้วไปวางใน command line จากที่ลองใช้ ถ้าใช้ใน VSCode จะไม่สามารถ
ใช้งานได้ เนื่องจาก จะด้องทำให้อยู่ในบรรทัดเดียวก่อน โดยไม่มี ^ แต่ถ้าใช้งานใน command prompt สามารถ ใช้งานได้เลย
กรณี ใช้งานใน VSCode สามารถกำหนดเป็นดังนี้
mongoimport --db testdb --collection provinces --drop --file c:/data/json/provinces.json
การทำงานคือ จะทำการ import ข้อมูลจากไฟล์ provinces.json ไปไว้ใน testdb database ใน pronvices collection
ซึ่งหากมีข้อมูลเดิมอยู่ จะทำการลบข้อมูลเก่าทิ้งแล้วเพิ่มเข้าไปใหม่ (--drop)
ตอนนี้เราได้ข้อมูล provinces ในฐานข้อมูล MongoDB สำหรับสร้าง API เรียบร้อยแล้ว
การสร้าง Provinces API
สำหรับการสร้างหรือใช้งาน API นั่น เราเคยแนะนำไปแล้วในหลายบทความที่ผ่านมา ในหัวข้อนี้ จะเป็นแนวทางเพิ่มเติม
สำหรับการสร้าง API จะนำไปเป็นแนวทางในการใช้งานหรือไม่ก็ได้ ซึ่งโดยทั่วไปแล้วเมื่อเราสร้างเว็บไซต์หรือโปรเจ็คใดๆ
ไปสักระยะ เราจะพบว่าเริ่มมีความซับซ้อนและองค์ประกอบต่างๆ เพิ่มมากขึ้นเรื่อยๆ ดังนั้น การจะจำแนกหรือแบ่งโครงสร้าง
แต่ละส่วนให้ชัดเจน ก็จะช่วยให้เราสามารถพัฒนาต่อเนื่องได้อย่างสะดวกขึ้น
เช่นเดียวกับการโปรเจ็ค Express เราอาจจะมีการพัฒนาในส่วนของการใช้งานเป็น Web App และก็ส่วนของการใช้งานเป็น
API Service สำหรับนำไปใช้งานใน Mobile App หรือใช้งานผ่านเว็บไซต์หรือ Application อื่นๆ ที่ผ่านมา เรามีการใช้งาน
การสร้าง API ปนรวมอยู่กับการสร้าง Routes ของเว็บไซต์ ซึ่งในบางครั้ง อาจจะทำให้เราสับสนในกระบวนการทำงานได้ ดังนั้น
ในที่นี้ เราจะสร้าง API เสมือนเป็นอีก App ที่เป็นส่วนย่อย โดยจะมีโครงสร้าง คล้ายๆ กับที่เราใช้งานสำหรับทำ Web App
ดูโครงสร้างประกอบ
จะเห็นว่า เราสร้างโฟลเดอร์ชื่อ "api" เป็นโฟลเดอร์ app ย่อยสำหรับจัดการเฉพาะในส่วนของการใช้งาน API เท่านั้น โดย
ภายในก็จะมีโฟลเดอร์ "models", "routes" และ "validator" มีไฟล์ index.js เป็นต้นทางหลักสำหรับเรียกใช้งาน API Routes
Path อื่นๆ หรือก็คือที่รวม API Routes ทั้งหมด
เช่นเดียวกับ Web Routes หลัก ที่เรามีโฟลเดอร์ "models", "routes" และ "validator" เหมือนกับ API Routes
แต่ใน Web Routes เรามีการใช้งาน Template ในโฟลเดอร์ "views" เพิ่มเข้ามาด้วย ซึ่งใน API Routes จะไม่มีส่วนนี้
ทั้ง Web Routes และ API Routes ใช้งานการตั้งค่าที่โฟลเดอร์ "config" ร่วมกัน และ API module จะถูกเรียกใช้งานในไฟล์
app.js เพียงไฟล์เดียวที่ชื่อ index.js แนวทางโครงสร้างคร่าวๆ ก็จะประมาณนี้
สำหรับการสร้าง Provinces API ในที่นี้เราจะใช้งานแค่ GET method ดึอดังข้อมูลจังหวัดทั้งหมด กับดึงข้อมูลเฉพาะจังหวัด
ตามค่า id ที่ต้องการ เราจะได้ไฟล์ Provinces model จัดการกับข้อมูลผ่านฐานข้อมูล MongoDB ดังนี้
ไฟล์ provinces.js [api/models/provinces.js]
const db = require('../../config/db') const Provinces = { get:((id, req, res)=>{ // ฟังก์ชั่นแสดงข้อมูลจังหวัดอิงจาก id return new Promise((resolve, reject)=>{ db.then((db)=>{ db.collection('provinces') .find({_id:+id}) // แสดงข้อมูลจังหวัดตามค่า id ที่เป็น int สิ่งเข้ามา .sort({_id:1}) // เรียกจาก ค่า _id ASC .toArray( (error, results) => { if(!error){ if(results.length > 0){ resolve(results) }else{ reject(null) } }else{ reject(null) } }) }) }) }), list:((req, res) => { // ฟังก์ชั่นแสดงข้อมูลจังหวัดทั้งหมด return new Promise((resolve, reject)=>{ db.then((db)=>{ db.collection('provinces') .find() .sort({_id:1}) // เรียกจาก ค่า _id ASC (-1 == DESC) .toArray( (error, results) => { if(!error){ if(results.length > 0){ resolve(results) }else{ reject(null) } }else{ reject(null) } }) }) }) }) } module.exports = Provinces
ต่อมาก็ไฟล์ API Routes จะได้เป็นดังนี้
ไฟล์ provinces.js [api/routes/provinces.js]
const express = require('express') const router = express.Router() const Provinces = require('../models/provinces') // ใช้งาน Provinces Model router.route('/provinces?') .all((req, res, next) => { next() }) .get((req, res, next) => { // เรียกใช้ฟังก์ชั่น list() แสดงข้อมูงจังหวัดทั้งหมด Provinces.list(req, res).then( (results)=>{ return res.json(results) }, (error)=>{ return res.json({}) } ) }) .post((req, res, next) => { return res.json({}) }) router.route('/province/:id') .all((req, res, next) => { next() }) .get((req, res, next) => { let id = req.params.id // เรียกใช้ฟังก์ชั่น get() แสดงข้อมูงจังหวัดตามค่า _id ที่ต้องการ Provinces.get(id, req, res).then( (results)=>{ return res.json(results) }, (error)=>{ return res.json({}) } ) }) .put((req, res, next) => { return res.json({}) }) .delete((req, res, next) => { return res.json({}) }) module.exports = router
รวม API Routes ทั้งหมดไว้ที่ไฟล์ API Routes หลัก จะได้
ไฟล์ index.js [api/index.js]
const express = require('express') const router = express.Router() const provinceApi = require('./routes/provinces') // const anotherApi = require('./routes/anotherapi') router.use('/', [ provinceApi // anotherApi, ] ) module.exports = router
จะเห็นว่าในไฟล์ API Routes หลัก เราสามารถเรียกใช้งาน API module ต่างๆ โดยเพิ่มค่าได้ตามต้องการ ไม่ต้องไปเพิ่ม
ปนกันในไฟล์ app.js ทำให้โค้ดของเราดูเป็นสัดส่วนมากขึ้น
จากนั้นในไฟล์ app.js เราก็เรียกใช้งานเฉพาะ API Routes หลักแค่ module เดียวก็จะได้เป็น
ไฟล์ app.js
const express = require('express') // ใช้งาน module express const app = express() // สร้างตัวแปร app เป็น instance ของ express const path = require('path') // เรียกใช้งาน path module const cookieParser = require('cookie-parser') const session = require('express-session') const store = require('./config/storeDb') const { authorize } = require('./config/auth') const { useSession } = require('./config/session') const csrf = require('csurf') const createError = require('http-errors') // เรียกใช้งาน http-errors module const port = 3000 // port // ส่วนของการใช้งาน router module ต่างๆ const indexRouter = require('./routes/index') const loginRouter = require('./routes/login') const registerRouter = require('./routes/register') const dashboardRouter = require('./routes/dashboard') const changePasswordRouter = require('./routes/changepassword') // ส่วนของการใช้งาน API router const apiRouter = require('./api/index') // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); app.set('view options', {delimiter: '?'}); // app.set('env','production') app.use(express.json()) app.use(express.urlencoded({ extended: false })) app.use(cookieParser()) app.use(express.static(path.join(__dirname, 'public'))) //app.set('trust proxy', 1) // trust first proxy app.use(session({ name:'sid', // ถ้าไม่กำหนด ค่าเริ่มต้นเป็น 'connect.sid' secret: 'my ses secret', store:store, resave: true, saveUninitialized: true })) app.use(useSession) app.use(csrf()) // กำหนด Web Routes app.use('/', indexRouter) app.use('/login', authorize('/me', false), loginRouter) app.use('/register', authorize('/me', false), registerRouter) app.use('/me', authorize('/login', true), dashboardRouter) app.use('/changepassword', authorize('/login', true), changePasswordRouter) // กำหนด API Routes app.use('/api', apiRouter) // ทำงานทุก request ที่เข้ามา app.use(function(req, res, next) { var err = createError(404) next(err) }) // ส่วนจัดการ CSRF error app.use(function (err, req, res, next) { // ถ้าไม่ใช้ CSRF error ให้ข้ามไปส่วนจัดการ error ปกติ if (err.code !== 'EBADCSRFTOKEN') return next(err) // ถ้าเป็น CSRF token errors ก็เช่น // res.status(403) // res.send('form tampered with') // ในที่นี้ เราให้ไปยังหน้าฟอร์มนั้น และแสดงข้อความ Invalid csrf token // โดยใช้ req.originalUrl เพื่อลิ้งค์ไปยัง path ของหน้าฟอร์มที่ error res.cookie('flash_message', 'Invalid csrf token',{maxAge:3000}) return res.redirect(req.originalUrl) }) // ส่วนจัดการ error app.use(function (err, req, res, next) { // กำหนด response local variables res.locals.pageData = { title:'Error Page' } res.locals.message = err.message res.locals.error = req.app.get('env') === 'development' ? err : {} // กำหนด status และ render หน้า error page res.status(err.status || 500) // ถ้ามี status หรือถ้าไม่มีใช้เป็น 500 res.render('pages/error') }) app.listen(port, function() { console.log(`Example app listening on port ${port}!`) })
ทดสอบเรียกใช้งาน Provinces API ของเรา จะได้ผลลัพธ์ดังนี้
ก่อนจะไปต่อ เราขอปรับส่วนของ Web Routes ในลักษณะเช่นเดียวกับ API Routes ดังนี้
เปลี่ยนไฟล์หน้าแรกจาก index.js เป็น home.js รวมถึงเปลี่ยนการเเรียกใช้งานไฟล์ template จาก index.ejs เป็น
home.ejs จะได้เป็น home.js เป็นดังนี้
ไฟล์ home.js [routes/home.js]
const express = require('express') const router = express.Router() router.get('/', function(req, res, next) { res.locals.pageData = { title:'Home Page' } if(!req.cookies.my_ck1){ res.cookie('my_ck1', 'cookie_1',{maxAge:60000}) } //req.session.isLogined = true res.render('pages/home') }) module.exports = router
จากนั้นไฟล์ index.js เราจะใช้เป็น Web Routes หลัก จะได้เป็นดังนี้
ไฟล์ index.js [routes/index.js]
const express = require('express') const router = express.Router() const { authorize } = require('../config/auth') // เรียกใช้ Web Router const homeRouter = require('./home') const loginRouter = require('./login') const registerRouter = require('./register') const dashboardRouter = require('./dashboard') const changePasswordRouter = require('./changepassword') // กำหนด Web Routes router.use('/', homeRouter) .use('/login', authorize('/me', false), loginRouter) .use('/register', authorize('/me', false), registerRouter) .use('/me', authorize('/login', true), dashboardRouter) .use('/changepassword', authorize('/login', true), changePasswordRouter) module.exports = router
สุดท้ายเราจะได้ไฟล์ app.js เป็นดังนี้
ไฟล์ app.js
const express = require('express') // ใช้งาน module express const app = express() // สร้างตัวแปร app เป็น instance ของ express const path = require('path') // เรียกใช้งาน path module const cookieParser = require('cookie-parser') const session = require('express-session') const store = require('./config/storeDb') const { useSession } = require('./config/session') const csrf = require('csurf') const createError = require('http-errors') // เรียกใช้งาน http-errors module const port = process.env.PORT || 3000 // port // ส่วนของการใช้งาน router module ต่างๆ const indexRouter = require('./routes/index') // ส่วนของการใช้งาน API router const apiRouter = require('./api/index') // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); app.set('view options', {delimiter: '?'}); // app.set('env','production') app.use(express.json()) app.use(express.urlencoded({ extended: false })) app.use(cookieParser()) app.use(express.static(path.join(__dirname, 'public'))) //app.set('trust proxy', 1) // trust first proxy app.use(session({ name:'sid', // ถ้าไม่กำหนด ค่าเริ่มต้นเป็น 'connect.sid' secret: 'my ses secret', store:store, resave: true, saveUninitialized: true })) app.use(useSession) app.use(/^\/(?!api).*/,csrf()) // ไม่ใช้งานกับ API Routes // กำหนด Web Routes app.use('/', indexRouter) // กำหนด API Routes app.use('/api', apiRouter) // ทำงานทุก request ที่เข้ามา app.use(function(req, res, next) { var err = createError(404) next(err) }) // ส่วนจัดการ CSRF error app.use(function (err, req, res, next) { // ถ้าไม่ใช้ CSRF error ให้ข้ามไปส่วนจัดการ error ปกติ if (err.code !== 'EBADCSRFTOKEN') return next(err) // ถ้าเป็น CSRF token errors ก็เช่น // res.status(403) // res.send('form tampered with') // ในที่นี้ เราให้ไปยังหน้าฟอร์มนั้น และแสดงข้อความ Invalid csrf token // โดยใช้ req.originalUrl เพื่อลิ้งค์ไปยัง path ของหน้าฟอร์มที่ error res.cookie('flash_message', 'Invalid csrf token',{maxAge:3000}) return res.redirect(req.originalUrl) }) // ส่วนจัดการ error app.use(function (err, req, res, next) { // กำหนด response local variables res.locals.pageData = { title:'Error Page' } res.locals.message = err.message res.locals.error = req.app.get('env') === 'development' ? err : {} // กำหนด status และ render หน้า error page res.status(err.status || 500) // ถ้ามี status หรือถ้าไม่มีใช้เป็น 500 res.render('pages/error') }) app.listen(port, function() { console.log(`Example app listening on port ${port}!`) })
สังเกตว่าส่วนของการใช้งาน csrf เราต้องปรับไม่ให้ใช้งานกับ API Routes โดยกำหนด Routes Path ให้กับ
การใช้งาน csrf ในรูปแบบ RegExp ในรูปแบบ /^\/(?!api).*/ ซึ่งความหมายคือ ใช้กับ path ที่ไม่ได้ขึ้นด้นด้วย "/api"
หรือ "api" เป็นต้น
เท่านี้ เราก็จะได้ส่วนของ Web Routes และ API Routes แยกเป็นสัดส่วนชัดเจน รองรับขนาดของ Application ที่
อาจจะเพิ่มขึ้นในอนาคต
เงื่อนไข Same-Origin และ CORS policy
กลับมาที่ Provinces API ของเรา จากหัวข้อที่ผ่านมา เมื่อเราเรียกใช้งาน Provinces API เราก็สามารถดึงข้อมูลจาก
API มาแสดงได้ ดูตัวอย่างการทดสอบผ่าน console โดยการใช้งาน คำสั่ง Fetch() API ซึ่งเป็นรูปแบบหนึ่งที่คล้ายการการ
ใช้งาน Ajax ผ่าน XMLHttpRequest ดูการใช้งานเพิ่มเติมได้ที่ Fetch API
fetch('http://localhost:3000/api/province/5') .then(function(response) { return response.json(); }) .then(function(myJson) { console.log(JSON.stringify(myJson)); });
จะเห็นว่าตอนนี้เราเรียกใช้งาน API จากเว็บไซต์ หรือ Domain เดียวกันกับ API ได้ โดยสามารถดึงข้อมูล API มาแสดง
ตามรูปด้านบน ทั้งนี้ก็ด้วยเงื่อนไขของ Same-Origin Policy นั่นคือ Same-Origin Policy เป็นเครื่องมือที่ช่วยในการป้องกัน
การโจมตีจากไฟล์หรือ script ที่มาจากเว็บไซต์หรือ Domain อื่นๆ การที่เราสามารถดึงช้อมูล หรือใช้งาน script ผ่านเว็บไซต์
หรือ Domain เดียวกันได้ ก็เพราะ Same-Origin Policy กำหนดให้เราสามารถใช้งานได้นั่นเอง
เราลองเปิดหน้าเว็บไซต์ www.google.com แล้วลองเรียกใช้งานคำสั่ง Fectch API เช่นเดียวกัน จะได้ผลลัพธ์ ดังนี้
Access to fetch at 'http://localhost:3000/api/province/5' from origin 'https://www.google.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
ในตอนนี้ เป็นการเรียกใช้งาน API จากเว็บไซต์ Google ซึ่เป็นเว็บไซต์หรือ Domain อื่น การที่ เราไม่สามารถเรียกใช้งาน
API ผ่าน Google ได้ก็เพราะเงื่อนไข CORS policy (Cross-Origin Resource Sharing) ซึ่งใน CORS นั่น หากเราต้องการ
อนุญาตให้สามารถเรียกใช้งาน API ผ่านเว็บไซต์หรือ Domain อื่นได้ เราจำเป็นต้องกำหนด HTTP headers เพิ่มเติม อย่างใน
error ที่แจ้งด้านบน ก็ระบุว่า ไม่มีการกำหนด 'Access-Control-Allow-Origin' ให้กับ Response header ดังนั้น
ใน Provinces API เราจะมีการใช้งาน CORS module เพิ่มเติมดังนี้
การติดตั้ง CORS Module
npm install cors --save
การใช้งาน CORS ใน API
จากนั้นเราจะทำการใช้งาน CORS ใน API ของเรา ซึ่งก็คือในไฟล์ index.js เบื้องต้นเราจะยังไม่ปรับ options ใดๆ
จะใช้ค่าเริ่มต้น จะได้เป็นดังนี้
ไฟล์ index.js [api/routes/index.js]
const express = require('express') const router = express.Router() const cors = require('cors') // เรียกใช้งาน core module const provinceApi = require('./routes/provinces') // const anotherApi = require('./routes/anotherapi') // ติดตั้ง cors middleware router.use(cors()) router.use('/', [ provinceApi // anotherApi, ] ) module.exports = router
เราลองไปทดสอบเรียกใช้งาน API ผ่านเว็บไซต์ Google อีกครั้ง ผลลัพธ์ที่ได้จะเป็นดังรูปด้านล่าง
จะเห็นว่า ตอนนี้เราสามารถเรียกใช้งาน API จากเว็บไซต์หรือ Domain อื่นได้แล้ว ข้อมูลก็จะแสดงเหมือนเราเรียกใช้งาน
ผ่านเว็บไซต์เดียวกับ API ดูส่วนของ Response Headers ก่อนและหลังมีการใช้งาน CORS module
สังเกตว่า หลังจากมีการใช้งาน CORS แล้ว ตัว Response Header มีการกำหนด Access-Control-Allow-Origin เท่ากับ
( * ) ซึ่งเป็นค่าเริ่มต้นของ CORS นั่นคือ สามารถเรียกใช้งาน API นี้จากที่ใดๆ ก็ได้ เรามาดูค่า default อื่นๆ เพิ่มเติม เมื่อมี
การกำหนดใช้งาน CORS module จะเป็นดังนี้
{ "origin": "*", // เรียกใช้งานจากที่ใดๆ ก็ได้ เช่น เว็บไซต์อื่น โดเมนอื่น "methods": "GET,HEAD,PUT,PATCH,POST,DELETE", // method ที่รองรับ "preflightContinue": false, "optionsSuccessStatus": 204 // กรณีมี OPTIONS method ให้สถานะเป็น 204 No Content. }
เราสามารถกำหนด API Routes เฉพาะที่ต้องการใช้งาน ได้ดังนี้
// ใช้ cors เฉพาะ Provinces API ทั้งหมด router.use('/provinces?', cors()) // ใช้ cors เฉพาะ GET Method ใน Provinces API router.get('/provinces?', cors())
การกำหนด CORS Options
เราสามารถกำหนด options ด้วยตัวเองหากไม่ต้องการใช้ค่าเริ่มต้น โดยสามารถกำหนดค่าต่างๆ ได้ดังนี้
origin
เป็นการกำหนดค่า "Access-Control-Allow-Origin" CORS header โดยสามารถกำหนดเป็นค่า
<Boolean>
เป็น true | false โดยถ้าเป็น true จะเป็นการใช้ค่าตาม req.header('Origin') ส่งมา นั่นคือ
ค่า orgin จะเปลี่ยนไปตาม req.header('Origin') นั่น เช่นสมมติ เราเรียกจากเว็บไซต์ https://www.ninenik.com
ก็จะได้ Access-Control-Allow-Origin: https://www.ninenik.com เป็นต้น
หากเรากำหนดเป็น false ก็จะหมายถึง ปิดการใช้งาน cors module ตัวอย่าง
let corsOptions = { origin: true } router.use(cors(corsOptions)) // หรือ let corsOptions = { origin: false } router.use(cors(corsOptions))
<String> เช่น
let corsOptions = { origin: '*' } router.use(cors(corsOptions)) // หรือ let corsOptions = { origin: 'https://www.ninenik.com' } router.use(cors(corsOptions))
<RegExp> เช่น
// สามารถเรียกได้จาก ทั้งmที่จากเว็บไซต์ ที่ลงท้ายด้วย ninenik.com เช่น press.ninenik.com let corsOptions = { origin: /\.ninenik\.com$/ } router.use(cors(corsOptions))
<Array> เช่น กำหนดหลายโดนเมน โดยสามารถใช้ Array ของ String ร่วมกับ RegExp
let whitelist = ['https://www.ninenik.com', /\.ebiwayo\.com$/] let corsOptions = { origin: whitelist } router.use(cors(corsOptions))
<Function> เช่น
let whitelist = ['https://www.ninenik.com', 'http://www.ebiwayo.com'] let corsOptions = { origin: ((origin, callback)=>{ if (whitelist.indexOf(origin) !== -1) { callback(null, true) } else { callback(new Error('Not allowed by CORS')) } }) } router.use(cors(corsOptions))
การใช้งานในรูปแบบฟังก์ชั่น ทำให้เราสามารถกำหนดเงื่อนไขเพิ่มเติมได้ตามต้องการ ในข้างตันเป็นการเปรียบเทียบ
ค่าของ array เว็บไซต์ที่อนุญาตให้สามารถใช้งาน API ได้ ถ้าค่าที่ส่งจาก req.header('Origin') ตรงกับค่าใดๆ ในลิส
รายการ array ก็จะกำหนด origin: true
methods
เป็นการกำหนดค่า "Access-Control-Allow-Methods" CORS header โดยสามารถกำหนดเป็นค่าในรูปแบบ
‘GET,PUT,POST’ หรือแบบ Array เป็น ['GET', 'PUT', 'POST'] ในส่วนนี้ถ้าไม่กำหนด ค่าเริ่มต้นจะเป็น
"GET,HEAD,PUT,PATCH,POST,DELETE" สมมติเรากำหนดแค่ 3 ค่าในรูปแบบ Array จะได้เป็นดังนี้
let corsOptions = { origin: true, methods:['GET', 'PUT', 'POST'] } router.use(cors(corsOptions))
การกำหนด Methods ที่อนุญาตนั้น เป็นการบอกว่า Methods ที่รองรับการใช้งานกรณีเงื่อนไข CORS Policy มีอะไรบ้าง
อย่างข้างต้น ถ้าเราใช้งาน DELETE method ก็จะไม่สามารถเรียกใช้งานแบบ CORS ได้
allowedHeaders
เป็นการกำหนดค่า "Access-Control-Allow-Headers" CORS header โดยสามารถกำหนดเป็นค่าในรูปแบบ
‘Content-Type,Authorization’ หรือแบบ Array เป็น ['Content-Type', 'Authorization'] ตัวอย่างเช่น
let corsOptions = { origin: true, methods:['GET', 'PUT', 'POST'], allowedHeaders:['Content-Type', 'Authorization'] } router.use(cors(corsOptions))
ปกติ หากเราไม่ได้กำหนดค่า allowedHeaders แล้ว CORS จะใช้ค่าอ้างอิงจากค่า Request Header ที่ส่งมา
แต่เมื่อใด ที่เรากำหนดค่าลงไป จะหมายถึงว่า เราไม่สามารถกำหนด Request Header นอกเหนือจากที่อนุญาตได้
อย่างในการกำหนดด้านบน เราอนุญาตแค่ 'Content-Type' กับ 'Authorization' นั่นคือจะไม่มีการส่งค่ามาก็ได้ หรือถ้ามี
การส่งค่ามา ต้องเป็นค่าตามที่กำหนดเท่านั้น เช่น Content-Type อย่างเดียว หรือ Authorization อย่างเดียว หรือไม่ก็
ส่งมาทั้งสองค่า แต่ถ้าเราส่งค่าอื่นที่ไม่ได้กำหนดมาจะเกิด error ขึ้น ดังนี้
ตัวอย่างเช่น เราส่ง X-Csrf-Token ใน header เข้าไป
fetch('http://localhost:3000/api/provinces', { method: 'POST', body: JSON.stringify({username: 'example'}), cache: 'no-cache', headers:{ 'Content-Type': 'application/json', 'X-Csrf-Token': 'i8XNjC4b8KVok4uw5RftR38Wgp2BFwql', } }).then(res => res.json()) .then(response => console.log('Success:', JSON.stringify(response))) .catch(error => console.error('Error:', error));
ก็จะเกิด error ในลักษณะดังรูปด้านล่าง
นอกจากนั้น ให้สังเกตว่า เมื่อใดก็ตาม ที่เรามีการส่งค่า Reqeust Header กรณีพิเศษ เข้าไป จะเกิด OPTIONS methods
ขึ้นด้วยเสมอควบคู่ไปกับ method หลักที่เราเรียกใช้งาน
กรณีที่จะไม่เกิด OPTIONS method ขึ้น หรือที่เรียกว่า Simple Requests จะประกอบไปด้วย
1. เรียกใช้งาน 3 methods นี้ อย่างใดอย่างหนึ่ง HEAD | GET | POST
2. กำหนดหรือใช้งาน Reqeust Header ด้วยค่าเหล่านี้
- Cache-Control
- Expires
- Last-Modified
- Pragma
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type, กรณีกำหนดหรือใช้งาน 3 ค่านี้เท่านั้น:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
กรณีไม่เข้าเงื่อนไขข้างต้น จะเกิด OPTIONS mehod ขึ้น เป็น Request แรก ซึ่งเรียกว่า Preflight Request
ซึ่งเป็นลักษณะการถามสิทธิ์การอนุญาตจาก server เพื่อทำการ Reqeust จริง
ใน cors module นั่น มีการกำหนดให้ไม่ต้องส่ง Preflight Response ไปทำงานในฟังก์ชั่นต่อ เป็นค่าเริ่มต้น
โดยกำหนดไว้ใน "preflightContinue": false ดังนั้น ถ้าเราไม่ได้ใช้งาน เราไม่จำเป็นต้องกำหนด options()
mehod เพิ่มเข้าไปใน routes แต่ถ้าเรากำหนดเป็น true เช่น
let corsOptions = { origin: true, methods:['GET', 'PUT', 'POST'], allowedHeaders:['Content-Type', 'Authorization'], preflightContinue: true } router.use(cors(corsOptions))
เราต้องไปเพิ่ม options() method เข้าไปใน routes เพื่อให้สามารถทำงานได้ เช่น
router.route('/provinces?') .all((req, res, next) => { next() }) .options((req, res, next) => { const reqMethod = req.headers['access-control-request-method'] const reqHeader = req.headers['access-control-request-headers'] if ((['GET', 'PUT', 'POST'].indexOf(reqMethod) !== -1) && (reqHeader === 'authorization,content-type')){ return res.status(204).json() } return res.status(400).json({message:'400 Bad Request'}) }) .get((req, res, next) => { return res.json({}) }) .post((req, res, next) => { return res.json({}) })
อย่างไรก็ตามกรณีมีการใช้งาน OPTIONS mehod ซึ่งสามารถถูก cached โดยการกำหนดด้วย maxAge property
เป็นการกำหนดค่า "Access-Control-Max-Age" CORS header โดยสามารถกำหนดค่าเป็นวินาที เช่น 60 ตัวอย่าง
let corsOptions = { origin: true, methods:['GET', 'PUT', 'POST'], allowedHeaders:['Content-Type', 'Authorization'], maxAge:60, preflightContinue: true } router.use(cors(corsOptions))
กรณีกำหนดค่า maxAge เท่ากับ 60 ข้างต้น เป็นการ cahched ค่า OPTIONS method เป็นเวลา 60 วินาที นั่นคือ
OPTIONS method จะถูกส่งครั้งแรก คร้้งเดียว จากนั้นทำการ cached ค่าไว้ จนครบ 60 วินาทีจึงทำการส่งค่าไปใหม่
โดยทั่วไปแล้ว เราจะไม่กำหนดการใช้งาน Preflight Request ไม่มีการเพิ่ม options() method ให้กับ router
และจะใช้การตั้งค่า โดยค่าหลักที่จะกำหนดจะมี อยู่ 3 ค่าคือ orign, methods และ allowedHeaders
ในที่นี้เราจะไม่ใช้การตั้งค่าใดๆ แต่จะใช้ค่าเริ่มต้นทั้งหมด โดยกำหนดแค่บรรทัดเดียว
// ติดตั้ง cors middleware router.use(cors())
หากต้องการกำหนด options ให้กับการกำหนด cors middleware เพิ่มเติม สามารถทำตามแนวทางด้านบน
หวังว่าเนื้อหาต่อไปนี้ จะเป็นแนวทางสำหรับใช้งานต่อไป