Hiểu tường tận về CORS
Bài đăng này đã không được cập nhật trong 4 năm
Cross-Origin Resource Sharing (CORS) là một cơ chế sử dụng header HTTP mở rộng, để Browser xác định có cho phép một webapp trên một origin origin, có thể access đến resource tại origin khác hay không
Nhiều bạn chắc sẽ thắc mắc origin là gì?
origin hiểu nôm na là những thuộc tính xác định 1 URL, cụ thể là: scheme (protocol), host (domain), và port
Ví dụ xét URL: https://domain-a.com:3000
thì
- protocol: https
- domain: domain-a.com
- port: 3000
2 Orign được xem là tương đồng khi chúng giống nhau cả 3 thuộc tính trên.
Một ví dụ về request cross-origin: một front-end code javaScript chạy trên https://domain-a.com
sử dụng XMLHttpRequest
để request tài nguyên ở một server khác https://domain-b.com/data.json
.
Vì lý do security, các browser sẽ cấm request HTTP cross-orign như thế này. Ví dụ XMLHttpRequest
và Fetch API request này tuân theo same-origin policy - là 1 chính sách về bảo mật. Có nghĩa là các webapp sử dụng các APIs này sẽ chỉ có thể request tài nguyên từ các app mà có cùng origin với nhau, trừ khi trong response từ server (nơi chứa tài nguyên) có chứa CORS headers đúng! (Đúng thế nào thì phần sau sẽ rõ nhé!)
Khái quát về CORS
Tiêu chuẩn Cross-Origin Resource Sharing hoạt động bằng cách add HTTP headers mới, cho phép server mô tả những origin nào được phép truy cập thông tin từ web browser.
Ngoài ra, với các HTTP methods mà có khả năng làm thay đổi data/ ảnh hưởng server ví dụ POST
với MIME types, Browser sẽ tiến hành gửi đi các request "preflight" trước, và sau đó nếu nhận được sự chấp thuận từ server, browser sẽ gửi đi request "thật" .
Server còn có thể báo cho client biết rằng "credentials" ví dụ như Cookies và HTTP Authentication có thể được gửi kèm cùng Request hay không.
Ví dụ về các tình huống access
Simple requests
Một số request sẽ không kích hoạt CORS preflight. Chúng được gọi chung là “simple requests”, bao gồm:
Ví dụ, web content trên site https://foo.example
muốn gọi đến contrent ở domain https://bar.other
. Code dưới đây mô tả cách request trên foo.example
:
const xhr = new XMLHttpRequest();
const url = 'https://bar.other/resources/public-data/';
xhr.open('GET', url);
xhr.onreadystatechange = someHandler;
xhr.send();
Code này sẽ thực thi việc get data đơn giản giữa client và server, sử dụng CORS headers để xác định quyền hạn
Dưới đây là nội dung browser gửi lên server
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
có thể thấy param Origin
trong header chỉ server xuất phát của request.
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
[…XML Data…]
Trong response, server trả về Access-Control-Allow-Origin
header. Ta cùng đối chiếu cặp quan hệ Origin
và Access-Control-Allow-Origin
. Trường hợp này, server trả về Access-Control-Allow-Origin: *
, có nghĩa tài nguyên của server này có thể đc access bởi từ bất kì domain nào.
Nếu tài nguyên ở https://bar.other
chỉ cho phép duy nhất request từ https://foo.example
, thì response sẽ là
Access-Control-Allow-Origin: https://foo.example
Preflighted requests
Kông giống “simple requests” (được trình bày ở phía trên), "preflighted" requests đầu tiên sẽ gửi một HTTP request với method OPTIONS
đến server tài nguyên, để xác định trước xem là request thật, có thể gọi được hay không.
Dưới đây là một ví dụ của mọt request mà sẽ kích hoạt một preflight request gọi trước.
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://bar.other/resources/post-here/');
xhr.setRequestHeader('X-PINGOTHER', 'pingpong');
xhr.setRequestHeader('Content-Type', 'application/xml');
xhr.onreadystatechange = handler;
xhr.send('<person><name>Arun</name></person>');
ví dụ trên, tạo một XML body gửi đi một POST
request. do request sử dụng Content-Type
là application/xml
, và header đã được custom, request này sẽ cần một preflight request gọi đi trước.
Chú ý: Như trình bày dưới đây, request POST
thật, sẽ không bao gồm Access-Control-Request-*
header,
các header này chỉ chỉ có trong OPTIONS
request.
Cùng xem luồng trao đổi giữa client và server, lần gọi đầu tiên chính là preflight request!
OPTIONS /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
đây là preflight response!
HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Sau khi preflight request được hoàn thành, request thật sẽ được gửi đi:
POST /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: https://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: https://foo.example
Pragma: no-cache
Cache-Control: no-cache
<person><name>Arun</name></person>
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain
[Some XML payload]
Nhìn vào preflight request ta có thể thấy, browser gửi đi trước các methods mà sẽ được sử dụng trong request thật để server xem xét là có nhận request thật không. ví dụ request headers: POST, X-PINGOTHER, Content-Type
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
Trong preflight response, server trả về nội dung cho thấy method POST
và các headers khác X-PINGOTHER, Content-Type
được chấp thuận:
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Requests with credentials
Có thể nói khả năng hay nhất của XMLHttpRequest
hay Fetch và CORS đó là tạo các credential request, cho phép nhận diện thông tin HTTP cookies và HTTP Authentication.
Trong ví dụ này, content trong http://foo.example
trong một GET request có mang theo Cookies đến http://bar.other
const invocation = new XMLHttpRequest();
const url = 'http://bar.other/resources/credentialed-content/';
function callOtherDomain() {
if (invocation) {
invocation.open('GET', url, true);
invocation.withCredentials = true;
invocation.onreadystatechange = handler;
invocation.send();
}
}
Line 7 có flag withCredentials
mang boolean value. Browser sau khi gửi request này đi sẽ reject không xử lý tiếp trong trường hợp response trả về không có Access-Control-Allow-Credentials
: true
.
Dưới đây là sample request gửi đi và response nhận lại từ server.
GET /resources/credentialed-content/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Referer: http://foo.example/examples/credential.html
Origin: http://foo.example
Cookie: pageAccess=2
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:34:52 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 106
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
[text/plain payload]
Line 10 có set cookie để request content trên http://bar.other
, giả sử bar.other mà không response với Access-Control-Allow-Credentials
: true
(line 17) thì nội dung data nhận được trong response sẽ Browser khước từ, và không phản ánh lên client foo.example.
reference: Cross-Origin Resource Sharing (CORS)
All rights reserved