สรุป CORS แบบสั้น ๆ
CORS ย่อมาจาก Cross-Origin Resource Sharing
Origin คือ header ตัวนึง ที่บอกว่า request ถูกเรียกจากที่ไหน จะต่างกับ Referer โดยที่ Origin จะไม่มี path มาด้วย
Origin ประกอบด้วย 3 ส่วน คือ
- Scheme เช่น http, https
- Hostname เช่น example.com, www.example.com
- Port เช่น 3000, 8000, 8080, 9000
ถ้าเข้าเว็บจาก file://
Origin จะเป็น null
Origin: null
Origin: <scheme>://<hostname>[:<port>]
Origin ที่ต่างกัน
http://example.com
กับ https://example.com
เป็นคนละ Origin กัน เพราะ scheme ไม่เหมือนกัน คือ http
กับ https
http://example.com
กับ http://www.example.com
เป็นคนละ Origin กัน เพราะ hostname ไม่เหมือนกัน คือ example.com
กับ www.example.com
http://example.com:3000
กับ http://example.com:8080
เป็นคนละ Origin กัน เพราะ port ไม่เหมือนกัน คือ 3000
กับ 8000
http://example.com
กับ http://example.com:80
เป็น Origin เดียวกัน เพราะ http
ถ้าไม่มี port จะถือว่าเป็น port 80
RFC 6454 3.2.1
เมื่อเกิด CORS
เมื่อเราต้องการ fetch resource จาก origin นึง ไปอีก origin นึง จะเกิดการเรียก resource ข้าม origin กัน
Browser จะส่ง OPTIONS
request ไปถาม server อีก origin ก่อน ว่าจะอนุญาตให้ origin ต้นทางสามารถเข้าไปเรียก resource ได้ไหม
หมายความว่า ถ้า browser ไม่ support CORS ก็จะสามารถเรียกได้เลย โดยไม่ติด CORS หรือ request เกิดจาก Server-Server คุยกัน ก็จะไม่มี CORS เพราะ คนส่ง CORS คือ Browser
ตัวอย่างเช่น
ถ้าเราอยู่ที่ https://example.com
แล้วต้องการส่ง request ไปที่ https://api.example.com
fetch('https://api.example.com/api1', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': 'key-1'
},
body: JSON.stringify(data)
})
สิ่งที่เกิดขึ้นคือ Browser จะส่ง preflight request ไปถามที่ api.example.com
ก่อนว่า
- จะให้ Origin
https://example.com
ส่ง request ไปไหม ?
- จะให้ส่ง Method
POST
ไปไหม ?
- จะให้ส่ง Cookie (
credentials: 'include'
) ไปไหม ?
- จะให้ส่ง Header
Content-Type
กับ X-Api-Key
ไปไหม ?
OPTIONS /api1 HTTP/1.1
Host: api.example.com
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, X-Api-Key
ถ้า Server เราจะต้องการจะตอบ Browser ว่า
- ที่ path นี้ ให้ Origin
https://example.com
ส่ง request เข้ามาได้
- ที่ path นี้ ให้ส่ง Method
GET
กับ POST
เข้ามาได้
- ที่ path นี้ ให้ JavaScript ส่ง Header
Content-Type
กับ X-Api-Key
เข้ามาได้
- ที่ path นี้ ให้ส่ง Cookie เข้ามาได้
- ที่ path นี้ ให้ Browser จำคำตอบข้างบนไว้ 3600 วินาที จะได้ไม่ต้องมาถามซ้ำ
Server จะต้องตอบ Browser กลับไปว่า
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, X-Api-Key
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600
Vary: Origin
Vary ใช้ในการบอก Reverse Proxy (Cache, CDN) ว่า ถ้าจะ cache response นี้ ให้ cache แยกตาม Origin เช่นเราอนุญาตให้ example.com
เข้าได้ แต่ไม่ให้ evil.com
เข้า แต่ถ้าเราไม่บอก CDN ว่าถ้าจะ cache ให้ cache แยกตาม Origin ตอน evil.com
เข้ามา อาจจะได้ผลลัพท์ของ example.com
กลับไป
ถ้าเราไม่อยากให้ Browser ส่ง request เข้ามาหล่ะ ?
ก็แค่ตอบ error อะไรกลับไปก็ได้ เช่น
HTTP/1.1 403 Forbidden
Content-Type: text/plain
Content-Length: 9
Forbidden
นอกจากนี้เมื่อ Browser ส่ง Request จริงเข้ามา เราจะยังต้องตอบ browser บางคำตอบซ้ำ
เช่น หลังจากที่ Browser ได้คำตอบว่า api.example.com
อนุญาตให้ https://example.com
ส่ง request เข้ามาได้
Browser จะส่ง Request ที่เราเขียนใน JavaScript เข้ามาว่า
POST /api1 HTTP/1.1
Host: api.example.com
Origin: https://example.com
Cookie: sess=1234
Content-Length: 16
Content-Type: application/json
X-Api-Key: key-1
{"text":"hello"}
เราจะต้องแนบ header กลับไปบอก browser ต่อว่า
ให้ https://example.com
อ่านนะ เผื่อ response ของอีก Origin ถูก cache ไว้ browser จะไม่ส่ง response นี้ไปให้ JavaScript ถึงแม้ว่าเรา inspect ดูแล้วจะได้ success ก็ตาม
ให้ส่ง Cookie เข้ามาได้ (ถึงแม้ว่า Server จะประมวลผลทุกอย่างเสร็จแล้ว แต่ถ้า Browser ส่ง Cookie ไป แล้วไม่เห็น header นี้ใน response อีกรอบ ก็จะไม่ส่ง response ไปให้ JavaScript)
ผลลัพท์นี้ ให้ JavaScript อ่าน Header X-Request-Id
ได้
ถ้าเราไม่บอก Browser ถึงแม้เราจะส่ง header นี้กลับไปใน response แต่เราจะไม่สามารถเขียน JavaScript เข้าไปอ่าน header ตัวนี้ได้
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Request-Id
X-Request-Id: req-123
Vary: Origin
Content-Type: application/json
Content-Length: 11
{"ok":true}
สำคัญมาก ๆ กับการ Allow all origin
บางครั้งเราต้องการให้ทุก Origin เข้ามาได้ เราสามารถใส่
Access-Control-Allow-Origin: *
แต่เราจะ ไม่ สามารถทำแบบนี้ได้
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
เพราะถ้าเรา login ที่เว็บ https://bank.com
แล้วทุกเว็บจะสามารถส่ง request มาโอนเงินออกจากบัญชีเราได้หมดเลย เพราะทุกเว็บที่ส่ง request มาที่ api ของ bank.com
browser จะแนบ Cookie ที่ได้ตอน login ที่เว็บ bank.com
มาด้วย
บางคนอยากให้ทุกเว็บส่ง Cookie เข้ามาได้ ก็ต้องลักไก่ เอา Origin ที่มาจาก request ใส่เข้าไปใน Allow Origin แทน แต่ต้องคิดดี ๆ ก่อนทำ ไม่ฉะนั้นทุกเว็บจะได้ Cookie ไปใช้ได้เลย
สรุป
CORS คือสิ่งที่ Browser คุยกับ Server เพื่อถามว่า
- จะให้ JavaScript ที่อยู่คนละ Origin กับ Server ส่ง Request ไปหาได้ไหม
- ถ้าได้ จะให้ JavaScript ส่งอะไรไปหา Server บ้าง
- แล้ว Response ที่ Server ส่งกลับมา จะให้ JavaScript อ่านอะไรได้บ้าง
References