การใช้งาน CORS แก้ปัญหา Cross Domain Origin ใน Express

เขียนเมื่อ 4 ปีก่อน โดย Ninenik Narkdee
expressjs cors nodejs

คำสั่ง การ กำหนด รูปแบบ ตัวอย่าง เทคนิค ลูกเล่น การประยุกต์ การใช้งาน เกี่ยวกับ expressjs cors nodejs

ดูแล้ว 10,305 ครั้ง


เนื้อหาในตอนต่อไปนี้ เราจะกลับมาพูดถึงการใช้งาน 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

    ในขั้นตอนแรก ให้เราทำการติดตั้ง CORS Module มาใช้งานในโปรเจ็ค Express ของเรา ด้วยคำสั่ง
 
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 เพิ่มเติม สามารถทำตามแนวทางด้านบน
 
    หวังว่าเนื้อหาต่อไปนี้ จะเป็นแนวทางสำหรับใช้งานต่อไป


กด Like หรือ Share เป็นกำลังใจ ให้มีบทความใหม่ๆ เรื่อยๆ น่ะครับ



อ่านต่อที่บทความ









เนื้อหาที่เกี่ยวข้อง






เนื้อหาพิเศษ เฉพาะสำหรับสมาชิก

กรุณาล็อกอิน เพื่ออ่านเนื้อหาบทความ

ยังไม่เป็นสมาชิก

สมาชิกล็อกอิน



( หรือ เข้าใช้งานผ่าน Social Login )




URL สำหรับอ้างอิง











เว็บไซต์ของเราให้บริการเนื้อหาบทความสำหรับนักพัฒนา โดยพึ่งพารายได้เล็กน้อยจากการแสดงโฆษณา โปรดสนับสนุนเว็บไซต์ของเราด้วยการปิดการใช้งานตัวปิดกั้นโฆษณา (Disable Ads Blocker) ขอบคุณครับ