Cross-Domain Ajax Requests

Mở đầu

Same-origin policy là một khái niệm quan trọng trong mô hình bảo mật thông tin web. Đây là một chính sách dành cho các browser, browser chỉ được cho phép các đoạn script ở trang web thứ nhất truy cập vào dữ liệu ở trang web thứ hai khi mà hai trang đó có cùng nguồn (same-origin). Chính sách này ra đời nhằm mục đích ngăn cản các script độc hại ở một trang lấy các thông tin nhạy cảm ở một trang khác. Tuy nhiên nó cũng gây ra một số khó khăn trong quá trình phát triển web như không cho phép gửi AJAX request từ một trang đến trang khác không cùng domain để lấy dữ liệu. Việc gửi cross-domain AJAX request đôi khi là cần thiết, chẳng hạn khi bạn có một API server và muốn cung cấp cho các dịch vụ khác truy cập qua JavaScript. Ở bài viết mình xin giới thiệu một số cách để thực hiện AJAX request giữa các domain khác nhau, hi vọng sẽ giúp ích cho các bạn.

1. JSONP (JSON with padding)

Tuy chúng ta không thể gửi được AJAX request từ một domain này đến một domain khác, nhưng chúng ta có thể dùng thẻ <script> với thuộc tính src để load JavaScript từ bất kì domain nào. JSONP hoạt động trên nguyên tắc này, nó tự động tạo ra thẻ <script> để load code JavaScript từ phía server và thực thi đoạn code đó.

Dưới đây là một ví dụ đơn giản của việc dùng JSONP để thực thi cross-domain AJAX

<div id="results"></div>
$.ajax({
  url: 'http://other-domain.com/data',
  dataType: 'jsonp',
  jsonpCallback: 'handleResponse' // chỉ định tên của callback function
});

function handleResponse(response) {
  $('#results').html(response);
}

Lúc này cặp thẻ <script> với nội dung như dưới sẽ được tạo ra

<script src="http://other-domain.com/data?callback=handleResponse&_=1437907666366"></script>

Ở phía server sẽ cần phải thay đổi dạng của response, nếu như bình thường server trả về response là HTML với nội dung

<p>GET request handled at 2015-07-26 18:06:25</p>

thì để dùng JSONP server phải trả về JavaScript với nội dung

handleResponse('<p>GET request handled at 2015-07-26 18:06:25</p>')

Ở ví dụ trên nếu như không chỉ định tên của callback function thì jQuery sẽ tự sinh ngẫu nhiên tên function (ví dụ: jQuery1111015991538995876908_1437907666364), khi đó code JavaScript có thể được viết lại ngắn hơn 1 chút

$.ajax({
  url: 'http://other-domain.com/data',
  dataType: 'jsonp'
})
.done(function(response) {
  $('#results').html(response);
});

Ưu điểm và nhược điểm

Ưu điểm:

  • Dễ dàng implement ở phía client
  • Sử dụng được với tất cả các browser

Nhược điểm

  • Cần sự hỗ trợ phía server
  • Chỉ gửi được GET request
  • Không thể gửi header kèm theo

2. CORS

Cross-origin resource sharing (CORS) là một cơ chế đặc biệt cho phép resource đặt tại một domain này có thể được request từ một domain khác với domain đó. CORS tận dụng HTTP headers để hỗ trợ browser quyết định xem trường hợp nào có thể cho phép thực hiện cross-domain request và trường hợp nào không. Về cơ bản, khi chúng ta thực hiện 1 cross-origin request, browser sẽ gửi kèm header Origin có giá trị là domain hiện tại. Ví dụ:

Origin: http://my-domain.com

Ở phía server khi tiếp nhận request sẽ kiểm tra xem domain này có được phép truy cập không, nếu được thì server trả về response với header Access-Control-Allow-Origin. Giá trị của header này cũng chính là domain nơi mà script gửi cross-origin request được thực thi, trong trường hợp này là

Access-Control-Allow-Origin: http://my-domain.com

Khi nhận được response từ server, browser kiểm tra xem header Access-Control-Allow-Origin có tồn tại hay không và có trùng với domain hiện tại hay không, nếu ok thì sẽ cho phép xử lí response.

Để có thể dùng được CORS thì chúng ta cần sự hỗ trợ từ cả 2 phía client và server.

  • Về phía client, CORS hiện nay đã được hỗ trợ bởi hầu hết các trình duyệt, chi tiết các bạn có thể xem ở hình dưới (nguồn: http://caniuse.com/#search=cors)

    CORS Support

    Khi thực hiện CORS request, chúng ta dùng XMLHttpRequest trên Firefox 3.5+, Chrome, Safari 4+, IE 10+ và XDomainRequest trên IE 8, 9. Do jQuery đã hỗ trợ XMLHttpRequest một cách tự động nên chúng ta có thể tạo AJAX request như bình thường mà không phải thay đổi code JavaScript.

	$.ajax({
	  url: 'http://other-domain.com/data'
	})

Đối với XDomainRequest thì xử lí hơi phức tạp hơn một chút, do không được jQuery hỗ trợ nên chúng ta cần dùng plugin ngoài nếu như không muốn phải tạo XDomainRequest một cách thủ công. Có một thư viện hỗ trợ rất tốt việc này đó là jquery.xdomainrequest.js. Khi đã dùng thư viện này thì chúng ta cũng có thể tạo AJAX request một cách bình thường, tuy nhiên XDomainRequest có những hạn chế như sau:

  • Protocol chỉ có thể là HTTP hoặc HTTPS

  • Scheme của URL nơi gửi request và nơi nhận request phải giống nhau (Vd: script đặt ở http://my-domain.com sẽ không thể gửi request đến https://other-domain.com do hai URL khác scheme)

  • HTTP method chỉ có thể là GET hoặc POST. Khi gửi POST request, Content-Type của header chỉ có thể là text/plain

  • Không thể gửi kèm custom header

  • Về phía server, cần phải tiến hành kiểm tra xem domain của request có hợp lệ hay không và gửi về header Access-Control-Allow-Origin kèm theo response. Nếu như server được phát triển bằng Rails thì chúng ta có thể dùng gem rack-cors. Sau khi cài gem, chúng ta cần thêm đoạn code sau đây vào file config/application.rb

config.middleware.insert_before 0, 'Rack::Cors' do
      allow do
        origins 'http://my-domain.com' # cho phép request từ my-domain.com, để cho phép tất cả các domain dùng '*'
        resource '*',                  # cho phép request đến tất cả các resource
        headers: :any,                 # những header được phép sử dụng
        methods: [:get, :post, :delete, :put, :patch, :options, :head] # những HTTP method được phép sử dụng
      end
    end

Ưu điểm và nhược điểm

Ưu điểm:

  • Dễ dàng implement ở phía client
  • Sử dụng được với hầu hết các browser
  • Gửi được request với tất cả các loại HTTP method (trừ IE 8, 9; tùy setting phía server)
  • Gửi được custom header kèm theo (trừ IE 8, 9; tùy setting phía server)

Nhược điểm

  • Cần sự hỗ trợ phía server
  • Trên IE 8, 9 có nhiều hạn chế
  • IE 7 trở xuống không hỗ trợ (who cares? (yaoming))

3. Proxy

Về mặt ý tưởng đây có lẽ là cách đơn giản nhất. Nếu như không được phép thực hiện cross-domain AJAX request thì chúng ta chuyển sang same-domain request (haha). Nguyên lý là chúng ta sẽ chuẩn bị một proxy server đặt tại domain giống với domain gửi AJAX request, proxy này có nhiệm vụ nhận request và chuyển hướng request sang domain bên ngoài. Do thực hiện request đến domain bên ngoài ở phía server nên chúng ta sẽ không gặp phải bất kì hạn chế nào liên quan đến same-origin policy.

Proxy

Việc chuẩn bị một proxy nói chung là phức tạp nhưng để demo cách thức hoạt động của phương pháp này mình xin được đưa ra 1 ví dụ với Rails. Đây là 1 cách implement đơn giản, proxy thực chất là một controller đặt tại http://my-domain.com/ajax, có nhiệm vụ nhận và chuyển hướng request đến http://other-domain.com.

# routes.rb
match "ajax/:path", to: 'ajax#create', via: :all, constraints: {path: /.*/}
class AjaxController < ActionController::Base
  skip_before_action :verify_authenticity_token

  def create
    # get request url
    path = params[:path].split('/').map{|i| ERB::Util.url_encode(i)}.join('/')
    url = 'http://other-domain.com' + '/' + path

    # remove some unnecessary parameters
    data = params.except(:controller, :action, :path)

    # get custom headers
    request_headers = {
      'CustomHeader' => request.headers['CustomHeader']
    }

    # send cross-domain request
    case env['REQUEST_METHOD']
    when 'GET'
      response = RestClient.get(url + '?' + data.to_query, request_headers)
    when 'POST'
      response = RestClient.post(url, data, request_headers)
    when 'PUT', 'PATCH'
      response = RestClient.put(url, data, request_headers)
    when 'DELETE'
      response = RestClient.delete(url + '?' + data.to_query, request_headers)
    end

    # return response
    render html: response.body.html_safe
  end
end

Khi gửi AJAX request thì chúng ta chỉ cần thay đổi url là được, lúc này request đã trở thành same-domain request (yeah)

$.ajax({
  url: 'http://my-domain.com/ajax/data'
})

Ưu điểm và nhược điểm

Ưu điểm:

  • Sử dụng được với tất cả các browser
  • Gửi được request với tất cả các loại HTTP method
  • Gửi được custom header kèm theo

Nhược điểm

  • Implement ở phía server phức tạp
  • Tốc độ chậm do phải request 2 lần

Tổng kết

tl;dr

Bài viết cũng đã khá dài nên có lẽ xin được kết thúc tại đây. Thay cho lời kết, mình xin tổng kết lại các đặc điểm của từng phương pháp trong bảng dưới để các bạn tiện tham khảo.

JSONP CORS (XMLHttpRequest) CORS (XDomainRequest) Proxy
Implement phía client Đơn giản Đơn giản Đơn giản Không cần
Implement phía server Đơn giản Đơn giản Đơn giản Phức tạp
Các trình duyệt hỗ trợ Tất cả Hầu hết IE 8, 9 Tất cả
HTTP method GET Tất cả GET, POST Tất cả
Gửi kèm header Không thể Có thể Có thể (*) Có thể

(*) Không thể gửi custom header

Cảm ơn các bạn đã theo dõi bài viết. (thanks)


Tài liệu tham khảo

  1. 4 jQuery Cross-Domain AJAX Request methods
  2. IE8 and CORS
  3. How to use CORS requests in Internet Explorer 9 and below
  4. XDomainRequest - Restrictions, Limitations and Workarounds