ในบทความนี้ เราจะเห็นวิธีเปิดใช้งาน CORS (การแชร์ทรัพยากรข้ามแหล่งที่มา) ด้วยคุกกี้ HTTPOnly เพื่อรักษาความปลอดภัยโทเค็นการเข้าถึงของเรา
ปัจจุบัน เซิร์ฟเวอร์แบ็กเอนด์และไคลเอ็นต์ฟรอนต์เอนด์ถูกปรับใช้บนโดเมนต่างๆ ดังนั้นเซิร์ฟเวอร์จึงต้องเปิดใช้งาน CORS เพื่อให้ไคลเอ็นต์สื่อสารกับเซิร์ฟเวอร์บนเบราว์เซอร์ได้
นอกจากนี้ เซิร์ฟเวอร์กำลังใช้การพิสูจน์ตัวตนแบบไร้สัญชาติเพื่อความสามารถในการปรับขนาดที่ดีขึ้น โทเค็นจะถูกจัดเก็บและดูแลบนฝั่งไคลเอ็นต์ แต่ไม่ใช่บนฝั่งเซิร์ฟเวอร์เหมือนเซสชัน เพื่อความปลอดภัย การจัดเก็บโทเค็นในคุกกี้ HTTPOnly จะดีกว่า
เหตุใดคำขอข้ามแหล่งที่มาจึงถูกบล็อก
สมมติว่าแอปพลิเคชันส่วนหน้าของเราปรับใช้ที่ https://app.admintrick.com.com สคริปต์ที่โหลดใน https://app.admintrick.com.com สามารถขอเฉพาะทรัพยากรที่มีต้นกำเนิดเดียวกันเท่านั้น
เมื่อใดก็ตามที่เราพยายามส่งคำขอข้ามต้นทางไปยังโดเมนอื่น https://api.admintrick.com.com หรือพอร์ตอื่น https://app.admintrick.com.com:3000 หรือรูปแบบอื่น http://app.admintrick.com.com คำขอข้ามที่มาจะถูกบล็อกโดยเบราว์เซอร์
แต่ทำไมคำขอเดียวกันที่ถูกบล็อกโดยเบราว์เซอร์ถูกส่งจากเซิร์ฟเวอร์แบ็กเอนด์โดยใช้คำขอ curl หรือส่งโดยใช้เครื่องมือเช่นบุรุษไปรษณีย์โดยไม่มีปัญหา CORS เพื่อความปลอดภัยในการปกป้องผู้ใช้จากการโจมตี เช่น CSRF(Cross-Site Request Forgery)
มาดูตัวอย่างกัน สมมติว่าผู้ใช้รายใดลงชื่อเข้าใช้บัญชี PayPal ของตนเองในเบราว์เซอร์ หากเราสามารถส่งคำขอข้ามต้นทางไปยัง paypal.com จากสคริปต์ที่โหลดบนโดเมนอื่นที่ประสงค์ร้าย.com โดยไม่มีข้อผิดพลาด/การบล็อก CORS เหมือนที่เราส่งคำขอต้นทางเดียวกัน
ผู้โจมตีสามารถส่งหน้า https://malicious.com/transfer-money-to-attacker-account-from-user-paypal-account ที่เป็นอันตรายได้อย่างง่ายดาย โดยแปลงเป็น URL แบบสั้นเพื่อซ่อน URL จริง เมื่อผู้ใช้คลิกลิงก์ที่เป็นอันตราย สคริปต์ที่โหลดในโดเมน malware.com จะส่งคำขอข้ามที่มาไปยัง PayPal เพื่อโอนจำนวนผู้ใช้ไปยังบัญชี PayPal ของผู้โจมตีจะถูกดำเนินการ ผู้ใช้ทุกคนที่ลงชื่อเข้าใช้บัญชี PayPal และคลิกลิงก์ที่เป็นอันตรายนี้จะเสียเงิน ใครๆ ก็ขโมยเงินได้ง่ายๆ โดยที่ผู้ใช้บัญชี PayPal ไม่รู้
ด้วยเหตุผลข้างต้น เบราว์เซอร์จึงบล็อกคำขอข้ามต้นทางทั้งหมด
CORS (การแบ่งปันทรัพยากรแบบข้ามที่มา) คืออะไร
CORS เป็นกลไกการรักษาความปลอดภัยตามส่วนหัวที่ใช้โดยเซิร์ฟเวอร์เพื่อบอกให้เบราว์เซอร์ส่งคำขอข้ามต้นทางจากโดเมนที่เชื่อถือได้
เซิร์ฟเวอร์ที่เปิดใช้งานด้วยส่วนหัว CORS ที่ใช้เพื่อหลีกเลี่ยงคำขอข้ามต้นทางที่ถูกบล็อกโดยเบราว์เซอร์
CORS ทำงานอย่างไร?
เนื่องจากเซิร์ฟเวอร์ได้กำหนดโดเมนที่เชื่อถือได้ในการกำหนดค่า CORS แล้ว เมื่อเราส่งคำขอไปยังเซิร์ฟเวอร์ การตอบกลับจะบอกเบราว์เซอร์ว่าโดเมนที่ร้องขอนั้นเชื่อถือได้หรือไม่อยู่ในส่วนหัว
คำขอ CORS สองประเภทมีอยู่:
- ของ่ายๆ
- คำขอเที่ยวบินล่วงหน้า
คำของ่ายๆ:
- เบราว์เซอร์ส่งคำขอไปยังโดเมนข้ามต้นทางที่มีต้นทาง (https://app.admintrick.com.com)
- เซิร์ฟเวอร์ส่งการตอบกลับที่เกี่ยวข้องกลับด้วยวิธีการที่อนุญาตและต้นทางที่อนุญาต
- หลังจากได้รับคำขอ เบราว์เซอร์จะตรวจสอบค่าส่วนหัวต้นทางที่ส่ง (https://app.admintrick.com.com) และรับค่า access-control-allow-origin (https://app.admintrick.com.com) ว่าเหมือนกันหรือ ตัวแทน
. มิฉะนั้น มันจะส่งข้อผิดพลาด CORS
- คำขอเที่ยวบินล่วงหน้า:
- ขึ้นอยู่กับพารามิเตอร์คำขอที่กำหนดเองจากคำขอข้ามต้นทางเช่นเมธอด (PUT, DELETE) หรือส่วนหัวที่กำหนดเองหรือประเภทเนื้อหาอื่น ฯลฯ เบราว์เซอร์จะตัดสินใจส่งคำขอตัวเลือก preflight เพื่อตรวจสอบว่าคำขอจริงปลอดภัยหรือไม่ หรือไม่.
หลังจากได้รับการตอบสนอง (รหัสสถานะ: 204 ซึ่งหมายถึงไม่มีเนื้อหา) เบราว์เซอร์จะตรวจสอบพารามิเตอร์การอนุญาตการควบคุมการเข้าถึงสำหรับคำขอจริง หากเซิร์ฟเวอร์อนุญาตพารามิเตอร์คำขอ ส่งและรับคำขอข้ามต้นทางจริง
หาก access-control-allow-origin: * แสดงว่าอนุญาตการตอบกลับสำหรับต้นทางทั้งหมด แต่มันไม่ปลอดภัยถ้าไม่จำเป็น
วิธีเปิดใช้งาน CORS
หากต้องการเปิดใช้งาน CORS สำหรับโดเมนใดๆ ให้เปิดใช้งานส่วนหัว CORS เพื่ออนุญาตที่มา วิธีการ ส่วนหัวที่กำหนดเอง ข้อมูลประจำตัว ฯลฯ
- เบราว์เซอร์อ่านส่วนหัว CORS จากเซิร์ฟเวอร์และอนุญาตคำขอจริงจากไคลเอนต์หลังจากตรวจสอบพารามิเตอร์คำขอแล้วเท่านั้น
- Access-Control-Allow-Origin: เพื่อระบุโดเมนที่แน่นอน (https://app.geekflate.com, https://lab.admintrick.com.com) หรือสัญลักษณ์แทน
- Access-Control-Allow-Methods: เพื่ออนุญาตวิธี HTTP (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) ที่เราต้องการเท่านั้น
- Access-Control-Allow-Headers: เพื่ออนุญาตเฉพาะส่วนหัว (Authorization, csrf-token)
- Access-Control-Allow-Credentials: ค่าบูลีนที่ใช้เพื่ออนุญาต cross-origin-credentials (คุกกี้, ส่วนหัวการให้สิทธิ์)
Access-Control-Max-Age: บอกให้เบราว์เซอร์แคชการตอบสนอง preflight ในบางครั้ง
Access-Control-Expose-Headers: ระบุส่วนหัวที่สามารถเข้าถึงได้โดยสคริปต์ฝั่งไคลเอ็นต์
สำหรับการเปิดใช้งาน CORS ใน apache และเว็บเซิร์ฟเวอร์ Nginx ให้ทำตามบทช่วยสอนนี้
const express = require('express'); const app = express() app.get('/users', function (req, res, next) { res.json({msg: 'user get'}) }); app.post('/users', function (req, res, next) { res.json({msg: 'user create'}) }); app.put('/users', function (req, res, next) { res.json({msg: 'User update'}) }); app.listen(80, function () { console.log('CORS-enabled web server listening on port 80') })
เปิดใช้งาน CORS ใน ExpressJS
มาดูตัวอย่างแอป ExpressJS ที่ไม่มี CORS:
npm install cors
ในตัวอย่างข้างต้น เราได้เปิดใช้งานปลายทาง API ของผู้ใช้สำหรับเมธอด POST, PUT, GET แต่ไม่ใช่เมธอด DELETE
เพื่อให้ง่ายต่อการเปิดใช้งาน CORS ในแอป ExpressJS คุณสามารถติดตั้ง cors
app.use(cors({ origin: '*' }));
Access-Control-Allow-Origin
app.use(cors({ origin: 'https://app.admintrick.com.com' }));
เปิดใช้งาน CORS สำหรับโดเมนทั้งหมด
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ] }));
การเปิดใช้งาน CORS สำหรับโดเมนเดียว
หากคุณต้องการอนุญาต CORS สำหรับแหล่งกำเนิด https://app.admintrick.com.com และ https://lab.admintrick.com.com
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'] }));
Access-Control-Allow-Methods
สำหรับการเปิดใช้งาน CORS สำหรับวิธีการทั้งหมด ให้ข้ามตัวเลือกนี้ในโมดูล CORS ใน ExpressJS แต่สำหรับการเปิดใช้งานวิธีการเฉพาะ (GET, POST, PUT)
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'] }));
Access-Control-Allow-Headers
ใช้เพื่ออนุญาตให้ส่วนหัวอื่นที่ไม่ใช่ค่าเริ่มต้นส่งพร้อมกับคำขอจริง
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true }));
Access-Control-Allow-Credentials
ข้ามสิ่งนี้หากคุณไม่ต้องการบอกเบราว์เซอร์ให้อนุญาตข้อมูลรับรองตามคำขอแม้ใน withCredentials จะถูกตั้งค่าเป็น true
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true, maxAge: 600 }));
Access-Control-Max-Age
เพื่อให้เบราว์เซอร์ใกล้ชิดกับแคชข้อมูลการตอบสนองของ preflight ในแคชสำหรับวินาทีที่ระบุ ข้ามสิ่งนี้หากคุณไม่ต้องการแคชการตอบกลับ
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true, maxAge: 600, exposedHeaders: ['Content-Range', 'X-Content-Range'] }));
การตอบสนอง preflight ที่แคชไว้จะใช้งานได้เป็นเวลา 10 นาทีในเบราว์เซอร์
app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true, maxAge: 600, exposedHeaders: ['*', 'Authorization', ] }));
การเข้าถึง-การควบคุม-เปิดเผย-ส่วนหัว
ถ้าเราใส่ไวด์การ์ด
ใน ExposedHeaders จะไม่เปิดเผยส่วนหัวการให้สิทธิ์ เลยต้องเปิดเผยให้ชัดเหมือนข้างล่างนี้
ด้านบนจะแสดงส่วนหัวและส่วนหัวการอนุญาตทั้งหมดด้วย
- คุกกี้ HTTP คืออะไร
- คุกกี้คือข้อมูลชิ้นเล็กๆ ที่เซิร์ฟเวอร์จะส่งไปยังเบราว์เซอร์ไคลเอ็นต์ ในคำขอในภายหลัง เบราว์เซอร์จะส่งคุกกี้ทั้งหมดที่เกี่ยวข้องกับโดเมนเดียวกันในทุกคำขอ
- คุกกี้มีแอตทริบิวต์ซึ่งสามารถกำหนดเพื่อให้คุกกี้ทำงานแตกต่างออกไปตามที่เราต้องการ
- ชื่อ ชื่อคุกกี้.
- ค่า: ข้อมูลของคุกกี้ที่เกี่ยวข้องกับชื่อคุกกี้
- โดเมน: คุกกี้จะถูกส่งไปยังโดเมนที่กำหนดเท่านั้น
- เส้นทาง: คุกกี้ที่ส่งหลังจากเส้นทางคำนำหน้า URL ที่กำหนดไว้เท่านั้น สมมติว่าเราได้กำหนดเส้นทางคุกกี้ของเราเช่น path=’admin/’ ไม่ได้ส่งคุกกี้สำหรับ URL https://admintrick.com.com/expire/ แต่ส่งด้วยคำนำหน้า URL https://admintrick.com.com/admin/
- อายุสูงสุด/หมดอายุ (ตัวเลขเป็นวินาที): คุกกี้จะหมดอายุเมื่อใด อายุการใช้งานของคุกกี้ทำให้คุกกี้ใช้งานไม่ได้หลังจากเวลาที่กำหนด [Strict, Lax, None]HTTPOnly(Boolean): เซิร์ฟเวอร์แบ็กเอนด์สามารถเข้าถึงคุกกี้ HTTPOnly นั้นได้ แต่ไม่สามารถเข้าถึงสคริปต์ฝั่งไคลเอ็นต์เมื่อเป็นจริง ปลอดภัย (บูลีน): คุกกี้จะส่งผ่านโดเมน SSL/TLS เมื่อเป็นจริงเท่านั้นsameSite(สตริง
): ใช้เพื่อเปิดใช้งาน/จำกัดคุกกี้ที่ส่งผ่านคำขอข้ามไซต์ หากต้องการทราบรายละเอียดเพิ่มเติมเกี่ยวกับคุกกี้ sameSite ดูที่
MDN
. ยอมรับสามตัวเลือก เข้มงวด, หละหลวม, ไม่มี ตั้งค่าความปลอดภัยของคุกกี้เป็น true สำหรับการกำหนดค่าคุกกี้ sameSite=None
เหตุใดจึงใช้คุกกี้ HTTPOnly สำหรับโทเค็น
การจัดเก็บโทเค็นการเข้าถึงที่ส่งจากเซิร์ฟเวอร์ในที่เก็บข้อมูลฝั่งไคลเอ็นต์ เช่น ที่จัดเก็บในตัวเครื่อง ฐานข้อมูลที่จัดทำดัชนี และคุกกี้ (ไม่ได้ตั้งค่า HTTPOnly เป็น true) มีความเสี่ยงต่อการโจมตี XSS มากกว่า สมมติว่าหน้าใดหน้าหนึ่งของคุณอ่อนแอต่อการโจมตี XSS ผู้โจมตีอาจใช้โทเค็นของผู้ใช้ที่จัดเก็บไว้ในเบราว์เซอร์ในทางที่ผิด
คุกกี้ HTTPOnly ถูกตั้งค่า/รับโดยเซิร์ฟเวอร์/แบ็กเอนด์เท่านั้น แต่ไม่ใช่ในฝั่งไคลเอ็นต์
- สคริปต์ฝั่งไคลเอ็นต์จำกัดการเข้าถึงคุกกี้ HTTPonly นั้น ดังนั้นคุกกี้ HTTPOnly จึงไม่เสี่ยงต่อการโจมตี XSS และมีความปลอดภัยมากขึ้น เพราะเข้าถึงได้โดยเซิร์ฟเวอร์เท่านั้น
- เปิดใช้งานคุกกี้ HTTPOnly ในแบ็กเอนด์ที่เปิดใช้งาน CORS
- การเปิดใช้งานคุกกี้ใน CORS จำเป็นต้องมีการกำหนดค่าด้านล่างในแอปพลิเคชัน/เซิร์ฟเวอร์
- ตั้งค่าส่วนหัว Access-Control-Allow-Credentials เป็น true
Access-Control-Allow-Origin และ Access-Control-Allow-Headers ไม่ควรเป็นสัญลักษณ์แทน
const express = require('express'); const app = express(); const cors = require('cors'); app.use(cors({ origin: [ 'https://app.geekflare.com', 'https://lab.geekflare.com' ], methods: ['GET', 'PUT', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], credentials: true, maxAge: 600, exposedHeaders: ['*', 'Authorization' ] })); app.post('/login', function (req, res, next) { res.cookie('access_token', access_token, { expires: new Date(Date.now() + (3600 * 1000 * 24 * 180 * 1)), //second min hour days year secure: true, // set to true if your using https or samesite is none httpOnly: true, // backend only sameSite: 'none' // set to none for cross-request }); res.json({ msg: 'Login Successfully', access_token }); }); app.listen(80, function () { console.log('CORS-enabled web server listening on port 80') });
.
แอตทริบิวต์ SameSite ของคุกกี้ควรเป็น None
สำหรับการเปิดใช้งานค่า sameSite เป็น none ให้ตั้งค่าการรักษาความปลอดภัยเป็น true: เปิดใช้งานแบ็กเอนด์ด้วยใบรับรอง SSL/TLS เพื่อทำงานในชื่อโดเมน
มาดูตัวอย่างโค้ดที่กำหนดโทเค็นการเข้าถึงในคุกกี้ HTTPOnly หลังจากตรวจสอบข้อมูลรับรองการเข้าสู่ระบบ
คุณสามารถกำหนดค่าคุกกี้ CORS และ HTTPOnly ได้โดยใช้สี่ขั้นตอนข้างต้นในภาษาแบ็กเอนด์และเว็บเซิร์ฟเวอร์ของคุณ
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://api.admintrick.com.com/user', true); xhr.withCredentials = true; xhr.send(null);
คุณสามารถทำตามบทช่วยสอนนี้สำหรับ apache และ Nginx เพื่อเปิดใช้งาน CORS โดยทำตามขั้นตอนข้างต้น
fetch('http://api.admintrick.com.com/user', { credentials: 'include' });
withCredentials สำหรับคำขอข้ามต้นทาง
$.ajax({ url: 'http://api.admintrick.com.com/user', xhrFields: { withCredentials: true } });
ข้อมูลประจำตัว (คุกกี้ การอนุญาต) ที่ส่งไปพร้อมกับคำขอต้นทางเดียวกันโดยค่าเริ่มต้น สำหรับ cross-origin เราต้องระบุ withCredentials ให้เป็นจริง
axios.defaults.withCredentials = true
XMLHttpRequest API
เรียก API
JQuery AjaxAxiosบทสรุป ฉันหวังว่าบทความข้างต้นจะช่วยให้คุณเข้าใจวิธีการทำงานของ CORS และเปิดใช้งาน CORS สำหรับคำขอข้ามต้นทางในเซิร์ฟเวอร์ เหตุใดการจัดเก็บคุกกี้ใน HTTPOnly จึงปลอดภัยและการใช้ withCredentials ในไคลเอนต์สำหรับคำขอข้ามต้นทาง