CORS: cơ chế truy cập tài nguyên không cùng nguồn gốc
Một nhu cầu rất thông dụng với các developer web đó là truy truy vấn qua API. Tuy nhiên, việc truy vấn và xử lý dữ liệu từ API nhiều khi cũng rất khó khăn, rất nhiều lập trình viên phải đối mặt với các vấn đề liên quan đến CORS.
CORS (Cross-origin resource sharing) là một cơ chế cho phép nhiều tài nguyên khác nhau (fonts, JavaScript, v.v…) của một trang web có thể được truy vấn từ domain khác với domain của trang đó.
Tại sao chúng ta cần CORS
Lý do cần đến CORS trước hết là bởi vì same-origin policy. Đây là một chính sách liên quan đến bảo mật được cài đặt vào toàn bộ các trình duyệt hiện nay. Chính sách này ngăn chặn việc truy cập tài nguyên của các domain khác một cách vô tội vạ.
Hãy thử tưởng tượng một kịch bản như sau:
- Bạn truy cập một trang web độc hại.
- Trang web đó sử dụng JavaScript để truy cập tin nhắn Facebook của bạn ở địa chỉ
https://facebook.com/messages
. - Do bạn đã đăng nhập Facebook từ trước, nếu không có same-origin policy, trang web độc hại kia có thể thoải mái lấy dữ liệu của bạn và bất cứ điều gì chúng muốn.
Same-origin policy chính là để ngăn chặn những tình huống như trên, giúp người dùng an toàn hơn khi lướt web. Bạn có thể thử trên web console và sẽ nhận được lỗi ngay:
$.get('https://facebook.com/messages')
Access to XMLHttpRequest at 'https://facebook.com/messages' from
origin 'xxx' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested
resource.
Truy cập URL trên từ bất kỳ domain nào ngoài facebook.com
bạn cũng sẽ nhận được lỗi như vậy. Đó chính là nhờ same-origin policy.
Thế nhưng trong thế giới web, không phải lúc nào tài nguyên từ domain hiện tại cũng là đủ cho một ứng dụng web. Rất nhiều lập trình viên thường xuyên phải thực hiện truy vấn đến các domain khác, đặc biệt là khi làm việc với các API.
Có nhiều cách thức khác nhau để truy vấn đến các domain khác, đầu tiên đó là sử dụng JSON-P tuy nhiên phương pháp này cũng có nhiều hạn chế liên quan đến bảo mật. Một phương thức khác đó là sử dụng proxy nhưng đây cũng không phải là một phương pháp dễ dàng thực hiện do việc cài đặt và cấu hình cũng rất phức tạp.
Đó là lúc chúng ta cần đến CORS. CORS sử dụng các HTTP header để “thông báo” cho trình duyệt rằng, một ứng dụng web chạy ở origin này (thường là domain này) có thể truy cập được các tài nguyên ở origin khác (domain khác). Một ứng dụng web sẽ thực hiện truy vấn HTTP cross-origin nếu nó yêu cầu đến các tài nguyên ở origin khác với origin nó đang chạy (khác giao thức, domain, port). Sự khác biệt về giao thức ở đây là khác biệt kiểu như HTTP với FTP chứ không phải HTTP và HTTPS (dù nhiều trình duyệt không cho phép trộn lẫn các tài nguyên truy cập bằng HTTP và HTTPS nhưng đó là vấn đề khác, không liên quan đến CORS).
Các trường hợp cần đến CORS rất phổ biến trong thực tế. Một ví dụ rất điển hình như sau: một ứng dụng web chạy ở domain foo.com
và nó cần truy vấn đến bar.com
để lấy một vài dữ liệu (thường được thực hiện bởi JavaScript bằng cách sử dụng XMLHttpRequest).
Các trình duyệt đều cài đặt same-origin policy và tuân thủ nó rất chặt chẽ. Cài đặt XMLHttpRequest và kể cả Fetch API cũng đều tuân thủ chính sách này. Do đó những truy vấn như ở trên sẽ không thu được kết quả gì, trừ khi máy chủ trả về response có các header CORS phù hợp.
Như vậy, bằng việc sử dụng CORS, chúng ta có thể thúc đẩy việc giao tiếp trong ứng dụng web dễ dàng hơn rất nhiều.
Các truy vấn dùng CORS
Các truy vấn sau bắt buộc phải sử dụng CORS, theo tiêu chuẩn quốc tế.
- Các truy vấn bằng XMLHttpRequest hoặc Fetch API đến một domain khác, như trong ví dụ ở trên.
- WebGL Texture
- Ảnh, video được vẽ vào canvas sử dụng
drawImage
. - Web fonts truy vấn đến domain khác qua
@fontface
của CSS, trong đó trang web chỉ có thể sử dụng font dạng True Type nếu được cho phép.
Làm thế nào để sử dụng CORS
Một hiểu lầm khá phổ biến, nhất là với các lập trình viên mới làm việc với API lại được làm việc với API của các hãng lớn, tài liệu đầy đủ, đó là cho rằng CORS là công việc của frontend. Nhưng thực ra CORS hoàn toàn là công việc của backend.
Các lập trình viên frontend thường không cần phải thao tác nhiều nếu cần dùng đến các truy vấn CORS (trừ một số ngoại lệ như không được sử dụng thư viện hoặc phải hỗ trợ IE 8). Khi một trình duyệt gửi một truy vấn đến máy chủ, nó sẽ tự động thiết lập một số HTTP header (ví dụ Origin
) chứa các thông tin về nguồn gốc của truy vấn đó.
Về phía máy chủ, sau khi có được thông tin về nguồn gốc của truy vấn, nó có thể lựa chọn không phải hồi truy vấn đó, trả về lỗi hoặc trả về dữ liệu cần thiết. Trong trường hợp trả về dữ liệu, máy chủ cần thiết lập các HTTP header sao cho trình duyệt hiểu rằng truy vấn đó đã được chấp nhận.
Như vậy,chúng ta có thể thấy rằng, CORS giúp thúc đấy quá trình trao đổi dữ liệu giữa trình duyệt và máy chủ. CORS hoàn toàn không có liên quan gì đến việc trao đổi trực tiếp giữa ứng dụng web mà một máy chủ web khác, ví dụ backend của ứng dụng đó truy cập đến tài nguyên trên một origin khác, nó cũng không cần đến CORS.
Tạo truy vấn CORS bằng XMLHttpRequest
Trong phần này chúng ta sẽ tìm hiểu cách tạo ra các truy vấn CORS bằng JavaScript. CORS được hỗ trợ bởi hầu hết các trình duyệt hiện đại. Riêng với IE, nó chỉ hỗ trợ từ IE 8 trở lên mà thôi.
Tạo truy vấn
Các trình duyệt Chrome, Firefox, Safari đều sử dụng version mới của XMLHttpRequest do đó việc truy vấn CORS diễn ra hết sức thuận lợi. IE thì sử dụng XDomainRequest, nó hoạt động gần giống với XMLHttpRequest nhưng có nhiều hạn chế hơn.
Chúng ta có thể bắt đầu bằng cách tạo ra các object cần thiết. Dưới đây là một đoạn code như thế:
const createCORSRequest = (method, url) => {
let xhr = new XMLHttpRequest();
if ('withCredentials' in xhr) {
// Kiểm tra XMLHttpRequest object có thuộc tính
// withCredentials hay không
// Thuộc tính này chỉ có ở XMLHttpRequest2
xhr.open(method, url, true);
} else if (typeof XDomainRequest != 'undefined') {
// Kiểm tra XDomainRequest
// Đây là đối tượng chỉ có ở IE và
// là cách để IE thực hiện truy vấn CORS
xhr = new XDomainRequest();
xhr.open(method, url);
} else {
xhr = null;
}
return xhr;
}
const request = createCORSRequest('GET',
'https://jsonplaceholder.typicode.com/posts/1');
if (!request) {
throw new Error('CORS is not supported');
}
Sau khi tạo được đối tượng XMLHttpRequest rồi thì chúng ta cần một số event handler, trong trường hợp này, chúng ta chỉ cần quan tâm 2 event onload
và onerror
là đủ. Ngoài ra còn một số event khác như ontimeout
, onprogress
không được sử dụng nhiều lắm.
request.onload = () => {
const responseText = request.responseText;
console.log(responseText);
}
request.onerror = () => {
console.log('Error');
}
Thực ra các trình duyệt khác nhau lại có cách cài đặt rất khác nhau với event onerror
. Ví dụ, Firefox trả về status là 0 và statusText
luôn rỗng với mọi lỗi. Ngoài ra, các trình duyệt cũng thường không cho phép truy cập đến nội dung cụ thể của lỗi đã xảy ra, chúng ta chỉ biết rằng đã có lỗi mà thôi.
withCredentials
Mặc định, các truy vấn CORS không gửi hoặc thiết lập bất cứ cookie nào trên trình duyệt. Nếu muốn sử dụng cookie trong truy vấn đó, chúng ta phải đặt thuộc tính withCredentials
của truy vấn bằng true
:
xhr.withCredentials = true;
Tuy nhiên, đó cũng mới chỉ là một nửa mà thôi. Nửa còn lại thuộc về phía máy chủ, đó là HTTP header Access-Control-Allow-Credentials
phải là true (chúng ta sẽ tìm hiểu ở phần sau).
Với giá trị withCredentials
bằng true
, cookie sẽ được tự động thêm vào cũng như thiết lập nếu có phản hồi từ máy chủ. Lưu ý rằng, cookie trong trường hợp này là third-party cookie và việc lưu trữ, truy cập cookie vẫn hoàn toàn thuận theo same-origin policy, do đó, chúng ta không thể truy cập cookie bằng document.cookie
được. Nó hoàn toàn được xử lý tự động bởi trình duyệt.
Gửi truy vấn
Sau khi mọi việc đã hoàn tất, việc cuối cùng chúng ta cần làm là gửi truy vấn đi nữa mà thôi:
request.send();
Lúc này truy vấn sẽ được gửi đến máy chủ, và nếu máy chủ đó chấp nhận CORS thì nó sẽ trả về response tương ứng. Hoạt động của truy vấn lúc này hoàn toàn giống với truy vấn có chúng origin thông thường.
Tạo truy vấn CORS bằng jQuery
Hàm $.ajax
của jQuery có thể được sử dụng cho các truy vấn thông thường lẫn truy vấn CORS (cookie cũng được hỗ trợ mặc định). Do đó nếu sử dụng jQuery thì công việc của lập trình viên cũng khá dễ dàng. Tuy nhiên, cần lưu ý một số điều như sau:
- Truy vấn CORS của jQuery không hỗ trợ object XDomainRequest của IE, chúng ta cần sử dụng thêm plugin để hỗ trợ việc này.
- Giá trị
$.support.cors
sẽ được gán làtrue
nếu trình duyệt hỗ trợ CORS (với IE sẽ làfalse
). Giá trị này có thể được sử dụng để kiểm tra xem CORS có được hỗ trợ hay không.
Dưới đây là một đoạn code sử dụng jQuery để tạo truy vấn CORS:
$.ajax({
type: 'GET',
url: 'https://jsonplaceholder.typicode.com/posts/1',
success: data => {
console.log(data);
},
error: () => {
console.log('Error');
}
})
Tạo truy vấn CORS bằng Fetch API
Chúng ta cũng có thể sử dụng Fetch API để tạo truy vấn CORS. Tuy nhiên, fetch
mới chỉ xuất hiện từ ES6 nên nhiều trình duyệt vẫn chưa hỗ trợ nó (cụ thể là IE tất cả các phiên bản đều không hỗ trợ).
Fetch API cho chúng ta một phương thức đơn giản để tạo các truy vấn, và nó đã cài đặt sẵn việc hỗ trợ CORS nên chúng ta cũng có thể thao tác rất đơn giản, giống như jQuery vậy. Tuy nhiên, kết quả trả về của fetch
là một Promise do đó các thao tác xử lý kết quả sẽ khác nhiều jQuery.
Lập trình với fetch rất đơn giản, thậm chí còn đơn giản hơn của với jQuery:
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(console.log)
Cấu hình máy chủ hỗ trợ CORS
Đây là phần phức tạp nhất, cũng là phần quan trọng nhất đối với CORS. Như đã nói ở trên, thực ra việc hỗ trợ CORS hay không phụ thuộc hoàn toàn vào máy chủ chứ không phải client.
Có hai loại truy vấn CORS: loại truy vấn “đơn giản” và “không đơn giản”.
Một truy vấn đơn giản hoàn toàn không cần đến CORS preflight. Một truy vấn sẽ được gọi là đơn giản nếu nó thoả mãn những điều kiện sau:
- Phương thức của truy vấn là một trong các loại
GET
,HEAD
,POST
. - Giá trị của
Content-Type
phải là một trong số các loạiapplication/x-www-form-urlencoded
,multipart/form-data
,text/plain
. - Không có event handler nào với event
XMLHttpRequest.upload
. - Không sử dụng đối tượng
ReadableStream
trong truy vấn. - Các HTTP header sau phải khớp:
Accept
Accept-Language
Content-Language
Last-Event-ID
Những truy vấn này được gọi là “đơn giản” bởi chúng có thể được coi là truy vấn thông thường từ trình duyệt mà không cần đến CORS, giống như submit một form HTML thông thường chẳng hạn.
Những truy vấn không phải “đơn giản” sẽ là truy vấn không đơn giản, và chúng cần CORS preflight. CORS preflight có nghĩa là trước khi truy vấn được gửi, nó cần phải gửi một truy vấn trước bằng phương thức OPTIONS
. Mục đích của truy vấn “preflight” này là nhằm kiểm tra xem truy vấn thực sự có an toàn để gửi và nhận hay không.
Đối với truy vấn đơn giản
Một truy vấn CORS đơn giản như đã nói ở trên, có thể có gói tin HTTP dạng như sau:
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/...
Với các phương thức khác, gói tin HTTP cũng tương tự như vậy. Lưu ý rằng, một truy vấn CORS hợp lệ luôn luôn có Origin
ở trong header. Giá trị của header này hoàn toàn được thiết lập tự động bởi trình duyệt, và không ai có thể thay đổi nó được. Giá trị của header này sẽ bao gồm scheme (http
), domain (api.bob.com
) và cổng (trong trường hợp dùng cổng mặc định thì không cần, ví dụ http dùng cổng 80). Giá trị của header chính là biểu thị nguồn gốc của truy vấn.
Một điểm lưu ý nữa là sự xuất hiện của header Origin
không đồng nghĩa với việc truy vấn đó là cross origin. Dù tất cả các truy vấn cross origin đều có header này, nhưng một số truy vấn same origin cũng có header này. Điều đó phụ thuộc vào từng trình duyệt cụ thể.
Ví dụ, Firefox không có header này cho các truy vấn same origin nhưng Chrome và Safari vẫn thêm header nay khi truy vấn same origin nhưng sử dụng các phương thức POST
, PUT
hoặc DELETE
. Đây là một điểm cần lưu ý với các lập trình viên backend, vì nếu không để ý có thể không thêm origin của chính app trong danh sách các domain được chấp nhận, điều đó khiến cho chính truy vấn same origin lại gặp lỗi.
Dưới đây là response của máy chủ phản hồi cho một truy vấn CORS hợp lệ:
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Tất cả các header liên quan đến CORS đều có phần đầu tiên là Access-Control-
. Ý nghĩa của từng header như sau:
Access-Control-Allow-Origin
(bắt buộc): đây là header phải có trong mọi response cho một truy vấn CORS hợp lệ. Nếu không có header này, truy vấn sẽ bị lỗi, giá trị của nó có thể là giá trị của headerOrigin
được gửi lên hoặc*
biểu thị cho mọi origin.Access-Control-Allow-Credentials
(tuỳ chọn): Mặc định, cookie sẽ không được sử dụng trong các truy vấn CORS. Header này sẽ biểu thị giá trị logic rằng có thể sử dụng cookie hay không. Giá trị duy nhất của header này làtrue
. Nếu không muốn sử dụng cookie thì thông thường người ta sẽ bỏ header này trong response chứ không phải đặt giá trị nó làfalse
. Lưu ý rằng, header này chỉ hoạt động nếu phía client cũng đặt giá trịwithCredentials = true
như đã nói ở phần trước.Access-Control-Expose-Headers
(tuỳ chọn): Một đối tượng XMLHttpRequest có một phương thứcgetResponseHeader
, phương thức này sẽ trả về giá trị của một header cụ thể trong response. Với các truy vấn CORS, phương thức này chỉ có thể truy cập được một số header đơn giản mà thôi. Nếu muốn phương thức này có thể truy cập nhiều header hơn, chúng ta cần đến giá trị của header này. Giá trị của header này là một danh sách các header có thể truy cập được, ngăn cách bằng dấu phẩy.
Đối với truy vấn cần preflight
Không phải truy vấn nào cũng là đơn giản do việc trao đổi dữ liệu giữa trình duyệt và máy chủ diễn ra rất đa dạng. Các phương thức PUT
hay DELETE
cũng thường xuyên được sử dụng. Ngoài ra kiểu dữ liệu JSON (Content-Type: application/json
) cũng là lựa chọn của nhiều lập trình viên. Trong những trường hợp như vậy, trước khi truy vấn chính được thực hiện thì một truy vấn gọi là preflight sẽ được gửi đi trước.
Ở phía frontend, các truy vấn đơn giản hay phức tạp đều trông sẽ giống nhau. Truy vấn preflight hoàn toàn được thực hiện ngầm và trong suốt với người dùng. Truy vấn preflight sẽ được gửi đi trước nhằm xác định xem truy vấn thực sự có thể thực hiện được hay không.
Sau khi có được phản hồi tích cực, trình duyệt sẽ gửi truy vấn thực sự. Kết quả của truy vấn preflight có thể được cache nên nó không cần phải thực hiện cho mọi truy vấn.
Dưới đây là một gói tin HTTP cho truy vấn preflight:
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/...
Tương tự như truy vấn đơn giản, truy vấn này cũng tự động được thêm header Origin
. Truy vấn preflight sẽ được thực hiện bằng phương thức OPTIONS
với một số header đặc thù:
Access-Control-Request-Method
: Đây là phương thức HTTP dùng trong truy vấn thực sự. Giá trị của header luôn luôn phải có, ngay cả khi các phương thức đó cũng là phương thức của một truy vấn đơn giản.Access-Control-Request-Headers
: Đây là danh sách (ngăn cách bằng dấu phẩy) các header được thêm vào truy vấn.
Truy vấn preflight là một cách để hỏi máy chủ rằng, liệu truy vấn thực sự có thể thực hiện được hay không. Mà máy chủ dựa vào hai header này để quyết định xem có chấp nhận truy vấn hay không. Nếu chấp nhận, máy chủ sẽ phản hồi như sau:
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Trong đó, response có thể có những header như sau:
Access-Control-Allow-Origin
(bắt buộc): Tương tự như trường hợp truy vấn CORS đơn giản.Access-Control-Allow-Methods
(bắt buộc): Là một danh sách (ngăn cách bằng dấu phẩy) các phương thức HTTP được chấp nhận. Dù truy vấn preflight có hỏi về một phương thức cụ thể của truy vấn tiếp theo, giá trị của header này trong responses có thể bao gồm tất cả các phương thức được chấp nhận.Access-Control-Allow-Headers
(bắt buộc nếu truy vấn có headerAccess-Control-Request-Headers
): Là danh sách các header (ngăn cách bằng dấu phẩy) được hỗ trợ. Tương tự như header trước, giá trị của header này cũng có thể bao gồm tất cả các header được chấp nhận.Access-Control-Allow-Credentials
(tuỳ chọn): Tương tự như trường hợp truy vấn CORS đơn giản.Access-Control-Max-Age
(tuỳ chọn): Truy vấn preflight không nhất thiết phải được thực hiện cho mọi truy vấn, mà kết quả của nó có thể cache được. Giá trị của header này chính là số giây mà giá trị của truy vấn preflight có thể được cache.
Một khi truy vấn preflight có được phản hồi và được chấp nhận, trình duyệt sẽ thực hiện truy vấn thực sự. Truy vấn lúc này tương tự như truy vấn CORS đơn giản và quá trình xử lý cũng như phản hồi hoàn toàn tương tự như vậy.
Nếu muốn từ chối truy vấn CORS, máy chủ có thể phần hồi một gói tin HTTP bình thường (mã 200) nhưng không có chứa HTTP header nào liên quan đến CORS. Trong trường hợp truy vấn preflight nhận được phản hồi như vậy, trình duyệt sẽ hiểu là truy vấn không được chấp nhận và nó sẽ không gửi thêm truy vấn nào nữa.
Về phía client, nếu trong trường hợp không thực hiện được truy vấn, event onerror
sẽ được gọi. Tuy nhiên, như đã nó ở trên, trình duyệt cũng không thể truy cập được nhiều thông tin về lỗi đó, chỉ đơn giản là biết có lỗi mà thôi.
Hỗ trợ CORS của các framework
Ruby on Rails
Ruby on Rails cho phép chúng ta thiết lập cũng như thay đổi các header trong response khá dễ dàng, do đó, muốn chấp nhận truy vấn CORS, chúng ta có thể đơn giản là làm như sau:
skip_before_filter :verify_authenticity_token
before_action :cors_preflight_check
after_action :cors_set_access_control_headers
def cors_set_access_control_headers
headers["Access-Control-Allow-Origin"] = "*"
headers["Access-Control-Allow-Methods"] = "GET, POST"
headers["Access-Control-Max-Age"] = "1728000"
end
def cors_preflight_check
headers["Access-Control-Allow-Origin"] = "*"
headers["Access-Control-Allow-Methods"] = "GET, POST"
headers["Access-Control-Allow-Headers"] =
"X-Requested-With, X-Prototype-Version"
headers["Access-Control-Max-Age"] = "1728000"
end
Django
Với Django chúng ta phải sử dụng thêm một package, đó là Django CORS headers. Package này sẽ giúp chúng ta thiết lập các header cần thiết cho một truy vấn CORS, đồng thời cho chúng ta khả năng cấu hình URL nào cho phép CORS, URL nào thì không.
Với package này, chúng ta có thể cấu hình sao cho chỉ có API mới hỗ trợ CORS như sau:
CORS_ORIGIN_ALLOW_ALL = True
CORS_URLS_REGEX = r'^/api/v1/.*$'
Ngoài ra còn rất nhiều cấu hình khác nữa, cho phép chúng ta chỉ chấp nhận truy vấn CORS từ một vài origin nhất định chẳng hạn (CORS_ORIGIN_REGEX_WHITELIST
). Nội dung chi tiết xin mới các bạn xem cụ thể tại README của package đó.
Flask
Tương tự như Django, với Flask, chúng ta cũng phải sử dụng thêm một package, đó là Flask-CORS mới có thể chấp nhận các truy vấn CORS được.
from flask import Flask
from flask.ext.cors import CORS, cross_origin
app = Flask(__name__)
app.config['SECRET_KEY'] = 'The quick brown fox jumps over the lazy dog'
app.config['CORS_HEADERS'] = 'Content-Type'
cors = CORS(app, resources={r'/foo/*': {'origins': '*'}})
@app.route('/foo')
@cross_origin()
def foo():
return 'Hello, world! CORS works'
if __name__ == '__main__':
app.run()
Kết luận
Trên đây là tất cả những gì cơ bản về CORS, nó giúp các ứng dụng web dễ dàng hơn trong việc trao đổi thông tin cũng như hiển thị nội dung, tăng khả năng tương tác giữa các dịch vụ trên Internet. Hy vọng bài viết giúp ích cho mọi người trong quá trình làm việc.
Welcome
Đây là thế giới của manhhomienbienthuy (naa). Chào mừng đến với thế giới của tôi!
Bài viết liên quan
Bài viết mới
Chuyên mục
Lưu trữ theo năm
Thông tin liên hệ
Cảm ơn bạn đã quan tâm blog của tôi. Nếu có bất điều gì muốn nói, bạn có thể liên hệ với tôi qua các mạng xã hội, tạo discussion hoặc report issue trên Github.