Cách quản lý yêu cầu chuyển hướng sau cuộc gọi jQuery Ajax


1369

Tôi đang sử dụng $.post()để gọi một servlet bằng Ajax và sau đó sử dụng đoạn HTML kết quả để thay thế một divphần tử trong trang hiện tại của người dùng. Tuy nhiên, nếu hết thời gian, máy chủ sẽ gửi một lệnh chuyển hướng để đưa người dùng đến trang đăng nhập. Trong trường hợp này, jQuery đang thay thế thành divphần này bằng nội dung của trang đăng nhập, buộc mắt người dùng phải chứng kiến ​​một cảnh hiếm thực sự.

Làm cách nào tôi có thể quản lý một lệnh chuyển hướng từ một cuộc gọi Ajax với jQuery 1.2.6?


1
(không phải là một câu trả lời như vậy) - Tôi đã làm điều này trong quá khứ bằng cách chỉnh sửa thư viện jquery và thêm một kiểm tra cho trang đăng nhập trên mỗi XHR hoàn tất. Không phải là giải pháp tốt nhất bởi vì nó sẽ phải được thực hiện mỗi khi bạn nâng cấp, nhưng nó giải quyết được vấn đề.
Sugendran

1
Xem câu hỏi liên quan: stackoverflow.com/questions/5941933/ từ
Nutel

Việc HttpContext.Response.AddHeadervà kiểm tra tại ajaxsetup sucess là cách để đi
LCJ

4
Tại sao máy chủ không thể trả về 401? Trong trường hợp đó, bạn có thể có $ .ajaxSetup toàn cầu và sử dụng mã trạng thái để chuyển hướng trang.
Vishal

1
liên kết này doanduyhai.wordpress.com/2012/04/21/ từ cho tôi giải pháp phù hợp
pappu_kutty

Câu trả lời:


697

Tôi đã đọc câu hỏi này và thực hiện cách tiếp cận đã được nêu liên quan đến việc đặt mã trạng thái HTTP phản hồi thành 278 để tránh trình duyệt xử lý trong suốt các chuyển hướng. Mặc dù điều này đã làm việc, tôi có một chút không hài lòng vì nó là một chút hack.

Sau khi đào sâu hơn, tôi đã bỏ cách tiếp cận này và sử dụng JSON . Trong trường hợp này, tất cả các phản hồi cho các yêu cầu AJAX đều có mã trạng thái 200 và phần thân của phản hồi chứa một đối tượng JSON được xây dựng trên máy chủ. JavaScript trên máy khách sau đó có thể sử dụng đối tượng JSON để quyết định những gì nó cần làm.

Tôi đã có một vấn đề tương tự như của bạn. Tôi thực hiện một yêu cầu AJAX có 2 phản hồi có thể có: một phản hồi chuyển hướng trình duyệt sang một trang mới và một yêu cầu thay thế một biểu mẫu HTML hiện có trên trang hiện tại bằng một trang mới. Mã jQuery để làm điều này trông giống như:

$.ajax({
    type: "POST",
    url: reqUrl,
    data: reqBody,
    dataType: "json",
    success: function(data, textStatus) {
        if (data.redirect) {
            // data.redirect contains the string URL to redirect to
            window.location.href = data.redirect;
        } else {
            // data.form contains the HTML for the replacement form
            $("#myform").replaceWith(data.form);
        }
    }
});

Đối tượng JSON "dữ liệu" được xây dựng trên máy chủ để có 2 thành viên: data.redirectdata.form. Tôi thấy cách tiếp cận này tốt hơn nhiều.


58
Như đã nêu trong giải pháp trong stackoverflow.com/questions/503093/ , tốt hơn là sử dụng window.location.replace (data.redirect); hơn window.location.href = data.redirect;
Carles Barrobés

8
Bất kỳ lý do nào sẽ tốt hơn nếu sử dụng mã HTTP trên hành động. Ví dụ mã 307 là Chuyển hướng tạm thời HTTP?
Sergei Golos

15
@Sergei Golos lý do là nếu bạn thực hiện chuyển hướng HTTP, thì chuyển hướng thực sự không bao giờ đến cuộc gọi lại thành công ajax. Trình duyệt xử lý chuyển hướng cung cấp 200 mã với nội dung đích của chuyển hướng.
Miguel Silva

2
mã máy chủ sẽ trông như thế nào? để có giá trị trả về của data.form và data.redirect. về cơ bản làm thế nào để tôi xác định nếu tôi đặt một chuyển hướng trong đó?
Trả thù

6
Câu trả lời này sẽ hữu ích hơn nếu nó chỉ ra cách thực hiện trên máy chủ.
Pedro Hoehl Carvalho

243

Tôi đã giải quyết vấn đề này bằng cách:

  1. Thêm một tiêu đề tùy chỉnh để đáp ứng:

    public ActionResult Index(){
        if (!HttpContext.User.Identity.IsAuthenticated)
        {
            HttpContext.Response.AddHeader("REQUIRES_AUTH","1");
        }
        return View();
    }
  2. Liên kết một hàm JavaScript với ajaxSuccesssự kiện và kiểm tra xem tiêu đề có tồn tại không:

    $(document).ajaxSuccess(function(event, request, settings) {
        if (request.getResponseHeader('REQUIRES_AUTH') === '1') {
           window.location = '/';
        }
    });

6
Thật là một giải pháp tuyệt vời. Tôi thích ý tưởng về giải pháp một cửa. Tôi cần kiểm tra trạng thái 403, nhưng tôi có thể sử dụng liên kết ajaxSuccess trên cơ thể cho việc này (đó là điều tôi thực sự đang tìm kiếm.) Cảm ơn.
Bretticus

4
Tôi vừa làm điều này và thấy rằng tôi cần ajaxComplete nơi tôi đang sử dụng hàm $ .get () và bất kỳ trạng thái nào ngoài 200 không được kích hoạt. Trên thực tế, có lẽ tôi chỉ có thể ràng buộc với ajaxError. Xem câu trả lời của tôi dưới đây để biết thêm chi tiết.
Bretticus

2
Nó ổn trong nhiều trường hợp, nhưng nếu khung của bạn xử lý ủy quyền thì sao?
jwaliszko

13
Tôi thích cách tiếp cận tiêu đề nhưng cũng nghĩ - như @ mwoods79 - rằng kiến ​​thức về nơi chuyển hướng đến không nên bị trùng lặp. Tôi đã giải quyết điều đó bằng cách thêm một tiêu đề GIẢM GIÁ thay vì boolean.
rintcius

3
Cẩn thận để đặt tiêu đề trên phản hồi SAU khi chuyển hướng. Như được mô tả trong các câu trả lời khác trên trang này, chuyển hướng có thể trong suốt đối với trình xử lý ajaxSucces. Do đó, tôi đã bao gồm tiêu đề trên phản hồi GET của trang đăng nhập (cuối cùng và chỉ kích hoạt ajaxSuccess trong kịch bản của tôi).
sieppl

118

Không có trình duyệt xử lý phản hồi 301 và 302 chính xác. Và trên thực tế, tiêu chuẩn thậm chí còn nói rằng họ nên xử lý chúng một cách "minh bạch", điều này gây đau đầu cho các nhà cung cấp Thư viện Ajax. Trong Ra-Ajax, chúng tôi buộc phải sử dụng mã trạng thái phản hồi HTTP 278 (chỉ một số mã thành công "không sử dụng") để xử lý các chuyển hướng trong suốt từ máy chủ ...

Điều này thực sự làm tôi khó chịu và nếu ai đó ở đây có một chút "lôi kéo" trong W3C, tôi sẽ đánh giá cao rằng bạn có thể cho W3C biết rằng chúng tôi thực sự cần phải tự xử lý mã 301 và 302 ...! ;)


3
Tôi cho một động thái rằng 278 nên tách ra khỏi HTTP Spec chính thức.
Chris Marisic

2
Không phải nó đã xử lý chúng một cách minh bạch sao? Nếu một tài nguyên đã được di chuyển, xử lý nó một cách minh bạch có nghĩa là lặp lại yêu cầu trên URL được cung cấp. Đó là những gì tôi mong đợi khi sử dụng API XMLHttpRequest.
Philippe Rathé

@ PhilippeRathé Đồng ý. Xử lý minh bạch chỉ là những gì tôi muốn. Và tôi không biết tại sao nó được coi là xấu.
smwikipedia

@smwikipedia Để sắp xếp chuyển hướng đánh dấu trong phần chính mà không chuyển hướng trang.
cmc

nếu ai đó ở đây có một số "sức hút" trong W3C, tôi sẽ đánh giá cao rằng bạn có thể cho W3C biết rằng chúng tôi thực sự cần phải tự xử lý mã 301 và 302 ...! ;)
Tommy.Tang

100

Giải pháp cuối cùng đã được thực hiện là sử dụng trình bao bọc cho chức năng gọi lại của lệnh gọi Ajax và trong kiểm tra trình bao bọc này để biết sự tồn tại của một phần tử cụ thể trên đoạn mã HTML được trả về. Nếu phần tử được tìm thấy thì trình bao bọc thực hiện chuyển hướng. Nếu không, trình bao bọc chuyển tiếp cuộc gọi đến chức năng gọi lại thực tế.

Ví dụ, chức năng bao bọc của chúng tôi là một cái gì đó như:

function cbWrapper(data, funct){
    if($("#myForm", data).length > 0)
        top.location.href="login.htm";//redirection
    else
        funct(data);
}

Sau đó, khi thực hiện cuộc gọi Ajax, chúng tôi đã sử dụng một cái gì đó như:

$.post("myAjaxHandler", 
       {
        param1: foo,
        param2: bar
       },
       function(data){
           cbWrapper(data, myActualCB);
       }, 
       "html"
);

Điều này hiệu quả với chúng tôi vì tất cả các lệnh gọi Ajax luôn trả về HTML bên trong phần tử DIV mà chúng tôi sử dụng để thay thế một phần của trang. Ngoài ra, chúng tôi chỉ cần chuyển hướng đến trang đăng nhập.


4
Lưu ý rằng điều này có thể được rút ngắn thành hàm cbWrapper (func) {return function (data) {if ($ ("# myForm", data) .size ()> 0) top.location.href = "login"; khác func (dữ liệu); }}. Sau đó, bạn chỉ cần cbWrapper (myActualCB) khi gọi .post. Đúng, mã trong các bình luận là một mớ hỗn độn nhưng cần lưu ý :)
Simen Echholt

kích thước được khấu hao để bạn có thể sử dụng .length ở đây thay cho kích thước
sunil

66

Tôi thích phương pháp của Timmerz với một chút chanh. Nếu bạn đã bao giờ có được trả lại contentType của text / html khi bạn đang mong JSON , bạn có nhiều khả năng bị chuyển hướng. Trong trường hợp của tôi, tôi chỉ cần tải lại trang và nó được chuyển hướng đến trang đăng nhập. Ồ, và kiểm tra xem trạng thái jqXHR là 200, có vẻ ngớ ngẩn, bởi vì bạn đang ở trong chức năng lỗi, phải không? Nếu không, các trường hợp lỗi hợp pháp sẽ buộc tải lại lặp lại (oops)

$.ajax(
   error:  function (jqXHR, timeout, message) {
    var contentType = jqXHR.getResponseHeader("Content-Type");
    if (jqXHR.status === 200 && contentType.toLowerCase().indexOf("text/html") >= 0) {
        // assume that our login has expired - reload our current page
        window.location.reload();
    }

});

1
cảm ơn rất nhiều Brian, câu trả lời của bạn là tốt nhất cho kịch bản của tôi, mặc dù tôi muốn nếu có một kiểm tra an toàn hơn như so sánh url / trang nào đang chuyển hướng đến thay vì kiểm tra "kiểu nội dung" đơn giản. Tôi không thể tìm thấy trang nào đang chuyển hướng từ đối tượng jqXHR.
Johnny

Tôi đã kiểm tra trạng thái 401 và sau đó chuyển hướng. Hoạt động như một nhà vô địch.
Eric

55

Sử dụng $.ajax()cuộc gọi cấp thấp :

$.ajax({
  url: "/yourservlet",
  data: { },
  complete: function(xmlHttp) {
    // xmlHttp is a XMLHttpRquest object
    alert(xmlHttp.status);
  }
});

Hãy thử điều này để chuyển hướng:

if (xmlHttp.code != 200) {
  top.location.href = '/some/other/page';
}

Tôi đã cố gắng để tránh những thứ cấp thấp. Dù sao, giả sử tôi sử dụng một cái gì đó giống như những gì bạn mô tả, làm cách nào để buộc chuyển hướng trình duyệt một khi tôi phát hiện ra rằng mã HTTP là 3xx? Mục tiêu của tôi là chuyển hướng người dùng, không chỉ để thông báo rằng phiên của anh ấy / cô ấy đã hết hạn.
Elliot Vargas

4
Btw, $ .ajax () không phải là rất, mức độ rất thấp. Nó chỉ ở mức độ thấp về mặt jQuery vì có $ .get, $ .post, v.v ... đơn giản hơn nhiều so với $ .ajax và tất cả các tùy chọn của nó.
Đến

16
Oh Boy! Xin lỗi tôi đã phải "không chấp nhận" câu trả lời của bạn, nó vẫn rất hữu ích. Điều quan trọng là, các chuyển hướng được tự động quản lý bởi XMLHttpRequest, do đó tôi LUÔN LUÔN nhận được 200 mã trạng thái sau khi chuyển hướng (thở dài!). Tôi nghĩ rằng tôi sẽ phải làm một cái gì đó khó chịu như phân tích cú pháp HTML và tìm kiếm một điểm đánh dấu.
Elliot Vargas

1
Chỉ tò mò, nếu phiên kết thúc tại máy chủ, điều đó không có nghĩa là máy chủ sẽ gửi SESSIONID khác? chúng ta không thể phát hiện ra điều đó sao?
Salamander2007

10
LƯU Ý: Điều này không hoạt động cho các chuyển hướng. ajax sẽ chuyển đến trang mới và trả lại mã trạng thái của nó.

33

Tôi chỉ muốn chia sẻ cách tiếp cận của mình vì điều này có thể giúp được ai đó:

Về cơ bản tôi đã bao gồm một mô-đun JavaScript xử lý các công cụ xác thực như hiển thị tên người dùng và trường hợp này cũng xử lý chuyển hướng đến trang đăng nhập .

Kịch bản của tôi: Về cơ bản, chúng tôi có một máy chủ ISA ở giữa lắng nghe tất cả các yêu cầu và trả lời với tiêu đề 302 và vị trí cho trang đăng nhập của chúng tôi.

Trong mô-đun JavaScript của tôi, cách tiếp cận ban đầu của tôi là một cái gì đó như

$(document).ajaxComplete(function(e, xhr, settings){
    if(xhr.status === 302){
        //check for location header and redirect...
    }
});

Vấn đề (như nhiều người đã đề cập ở đây) là trình duyệt tự xử lý chuyển hướng trong khi đó ajaxCompletecuộc gọi lại của tôi không bao giờ được gọi, nhưng thay vào đó tôi nhận được phản hồi của trang Đăng nhập đã được chuyển hướng rõ ràng là một status 200. Vấn đề: làm thế nào để bạn phát hiện xem phản hồi 200 thành công là trang đăng nhập thực tế của bạn hay chỉ là một số trang tùy ý khác ??

Giải pháp

Vì tôi không thể nắm bắt được 302 phản hồi chuyển hướng, tôi đã thêm một LoginPagetiêu đề trên trang đăng nhập của mình có chứa url của chính trang đăng nhập. Trong mô-đun bây giờ tôi lắng nghe tiêu đề và thực hiện chuyển hướng:

if(xhr.status === 200){
    var loginPageRedirectHeader = xhr.getResponseHeader("LoginPage");
    if(loginPageRedirectHeader && loginPageRedirectHeader !== ""){
        window.location.replace(loginPageRedirectHeader);
    }
}

... và nó hoạt động như bùa mê :). Bạn có thể tự hỏi tại sao tôi bao gồm url trong LoginPagetiêu đề ... về cơ bản vì tôi không tìm thấy cách nào để xác định url GETkết quả từ chuyển hướng vị trí tự động từ xhrđối tượng ...


+1 - nhưng các tiêu đề tùy chỉnh được cho là bắt đầu bằng X-, do đó, một tiêu đề tốt hơn để sử dụng sẽ là X-LoginPage: http://example.com/login.
uınbɐɥs

6
@ShaquinTrifonoff Không còn nữa. Tôi đã không sử dụng tiền tố X bởi vì vào tháng 6 năm 2011, một tài liệu ITEF đã đề xuất sự phản đối của họ và thực sự, với tháng 6 năm 2012 , không có chính thức nào cho thấy các tiêu đề tùy chỉnh không còn được thêm tiền tố X-nữa.
Juri

Chúng tôi cũng có một máy chủ ISA và tôi cũng gặp vấn đề tương tự. Thay vì làm việc xung quanh nó trong mã, chúng tôi đã sử dụng các hướng dẫn trong kb2596444 để định cấu hình ISA để dừng chuyển hướng.
đá scott

29

Tôi biết chủ đề này đã cũ, nhưng tôi sẽ đưa ra một cách tiếp cận khác mà tôi đã tìm thấy và mô tả trước đây ở đây . Về cơ bản tôi đang sử dụng ASP.MVC với WIF (nhưng điều này không thực sự quan trọng đối với bối cảnh của chủ đề này - câu trả lời là đầy đủ cho dù sử dụng khung nào. Đầu mối không thay đổi - xử lý các vấn đề liên quan đến lỗi xác thực trong khi thực hiện các yêu cầu ajax ) .

Cách tiếp cận được hiển thị dưới đây có thể được áp dụng cho tất cả các yêu cầu ajax ngoài hộp (nếu chúng không xác định lại trước sự kiện Gửi rõ ràng).

$.ajaxSetup({
    beforeSend: checkPulse,
    error: function (XMLHttpRequest, textStatus, errorThrown) {
        document.open();
        document.write(XMLHttpRequest.responseText);
        document.close();
    }
});

Trước khi bất kỳ yêu cầu ajax nào được thực hiện, CheckPulsephương thức được gọi (phương thức điều khiển có thể là bất cứ điều gì đơn giản nhất):

[Authorize]
public virtual void CheckPulse() {}

Nếu người dùng không được xác thực (mã thông báo đã hết hạn) thì phương thức đó không thể được truy cập (được bảo vệ bởi Authorizethuộc tính). Vì khung xử lý xác thực, trong khi mã thông báo hết hạn, nó đặt trạng thái http 302 cho phản hồi. Nếu bạn không muốn trình duyệt của mình xử lý 302 phản hồi trong suốt, hãy xem trình duyệt trong Global.asax và thay đổi trạng thái phản hồi - ví dụ: 200 OK. Ngoài ra, thêm tiêu đề, hướng dẫn bạn xử lý phản hồi đó theo cách đặc biệt (sau này ở phía máy khách):

protected void Application_EndRequest()
{
    if (Context.Response.StatusCode == 302
        && (new HttpContextWrapper(Context)).Request.IsAjaxRequest())
    {                
        Context.Response.StatusCode = 200;
        Context.Response.AddHeader("REQUIRES_AUTH", "1");
    }
}

Cuối cùng ở phía khách hàng kiểm tra tiêu đề tùy chỉnh như vậy. Nếu hiện tại - chuyển hướng đầy đủ đến trang đăng nhập nên được thực hiện (trong trường hợp của tôi window.locationđược thay thế bằng url từ yêu cầu được xử lý tự động bởi khung của tôi).

function checkPulse(XMLHttpRequest) {
    var location = window.location.href;
    $.ajax({
        url: "/Controller/CheckPulse",
        type: 'GET',
        async: false,
        beforeSend: null,
        success:
            function (result, textStatus, xhr) {
                if (xhr.getResponseHeader('REQUIRES_AUTH') === '1') {
                    XMLHttpRequest.abort(); // terminate further ajax execution
                    window.location = location;
                }
            }
    });
}

Tôi đã khắc phục sự cố bằng cách sử dụng sự kiện PostAuthenticateRequest thay vì sự kiện EndRequest.
Frinavale

@JaroslawWaliszko Tôi đã dán sự kiện sai vào câu trả lời cuối cùng của tôi! Ý tôi là sự kiện PreSendRequestHeaders ..... không phải PostAuthenticateRequest! >> đỏ mặt << cảm ơn bạn đã chỉ ra sai lầm của tôi.
Frinavale

@JaroslawWaliszko khi sử dụng WIF, bạn cũng có thể trả về 401 phản hồi cho các yêu cầu AJAX và để javascript của bạn xử lý chúng. Ngoài ra, bạn cho rằng tất cả 302 yêu cầu xác thực có thể không đúng trong mọi trường hợp. Tôi đã thêm một câu trả lời nếu có ai quan tâm.
Cướp

26

Tôi nghĩ rằng một cách tốt hơn để xử lý việc này là tận dụng các mã phản hồi giao thức HTTP hiện có, cụ thể 401 Unauthorized.

Đây là cách tôi giải quyết nó:

  1. Phía máy chủ: Nếu phiên hết hạn và yêu cầu là ajax. gửi tiêu đề mã phản hồi 401
  2. Phía khách hàng: Liên kết với các sự kiện ajax

    $('body').bind('ajaxSuccess',function(event,request,settings){
    if (401 == request.status){
        window.location = '/users/login';
    }
    }).bind('ajaxError',function(event,request,settings){
    if (401 == request.status){
        window.location = '/users/login';
    }
    });

IMO này chung chung hơn và bạn không viết một số thông số / tiêu đề tùy chỉnh mới. Bạn cũng không cần phải sửa đổi bất kỳ cuộc gọi ajax nào hiện có.

Chỉnh sửa: Nhận xét của Per @ Rob bên dưới, 401 (mã trạng thái HTTP cho lỗi xác thực) phải là chỉ báo. Xem 403 Bị cấm so với phản hồi HTTP trái phép để biết thêm chi tiết. Như đã nói, một số khung web sử dụng 403 cho cả lỗi xác thực VÀ ủy quyền - vì vậy hãy điều chỉnh cho phù hợp. Cảm ơn Rob.


Tôi sử dụng cách tiếp cận tương tự. JQuery có thực sự gọi ajaxSuccess trên mã lỗi 403 không? Tôi nghĩ rằng chỉ có phần ajaxError là thực sự cần thiết
Marius Balčytis

24

Tôi đã giải quyết vấn đề này như thế này:

Thêm phần mềm trung gian để xử lý phản hồi, nếu đó là chuyển hướng cho yêu cầu ajax, hãy thay đổi phản hồi thành phản hồi bình thường với url chuyển hướng.

class AjaxRedirect(object):
  def process_response(self, request, response):
    if request.is_ajax():
      if type(response) == HttpResponseRedirect:
        r = HttpResponse(json.dumps({'redirect': response['Location']}))
        return r
    return response

Sau đó, trong ajaxComplete, nếu phản hồi chứa chuyển hướng, thì đó phải là chuyển hướng, vì vậy hãy thay đổi vị trí của trình duyệt.

$('body').ajaxComplete(function (e, xhr, settings) {
   if (xhr.status == 200) {
       var redirect = null;
       try {
           redirect = $.parseJSON(xhr.responseText).redirect;
           if (redirect) {
               window.location.href = redirect.replace(/\?.*$/, "?next=" + window.location.pathname);
           }
       } catch (e) {
           return;
       }
   }
}

20

Tôi có một giải pháp đơn giản phù hợp với mình, không cần thay đổi mã máy chủ ... chỉ cần thêm một muỗng hạt nhục đậu khấu ...

$(document).ready(function ()
{
    $(document).ajaxSend(
    function(event,request,settings)
    {
        var intercepted_success = settings.success;
        settings.success = function( a, b, c ) 
        {  
            if( request.responseText.indexOf( "<html>" ) > -1 )
                window.location = window.location;
            else
                intercepted_success( a, b, c );
        };
    });
});

Tôi kiểm tra sự hiện diện của thẻ html, nhưng bạn có thể thay đổi indexOf để tìm kiếm bất kỳ chuỗi duy nhất nào tồn tại trong trang đăng nhập của bạn ...


Điều này dường như không hoạt động đối với tôi, nó tiếp tục gọi hàm được xác định bằng lệnh gọi ajax, có vẻ như nó không ghi đè phương thức thành công.
adriaanp

Dường như không còn hiệu quả với tôi nữa, ít nhất là nữa. Giải thích có thể có tại đây: stackoverflow.com/a/12010724/260665
Raj Pawan Gumdal

20

Một giải pháp khác tôi tìm thấy (đặc biệt hữu ích nếu bạn muốn thiết lập một hành vi toàn cầu) là sử dụng $.ajaxsetup()phương thức này cùng với thuộc statusCodetính . Giống như những người khác đã chỉ ra, không sử dụng mã trạng thái chuyển hướng ( 3xx), thay vào đó hãy sử dụng 4xxmã trạng thái và xử lý phía máy khách chuyển hướng.

$.ajaxSetup({ 
  statusCode : {
    400 : function () {
      window.location = "/";
    }
  }
});

Thay thế 400bằng mã trạng thái bạn muốn xử lý. Giống như đã đề cập 401 Unauthorizedcó thể là một ý tưởng tốt. Tôi sử dụng 400vì nó rất không đặc biệt và tôi có thể sử dụng 401cho các trường hợp cụ thể hơn (như thông tin đăng nhập sai). Vì vậy, thay vì chuyển hướng trực tiếp, phần phụ trợ của bạn sẽ trả về 4xxmã lỗi khi hết thời gian và bạn xử lý phía máy khách chuyển hướng. Hoạt động hoàn hảo với tôi ngay cả với các khung như backbone.js


Đề cập đến chức năng trên trang ở đâu?
Viking

@Vikrant Nếu tôi hiểu chính xác câu hỏi của bạn, bạn có thể gọi hàm ngay sau khi jQuery được tải và trước khi bạn thực hiện các yêu cầu thực tế của mình.
morten.c

19

Hầu hết các giải pháp đã cho đều sử dụng cách giải quyết, sử dụng tiêu đề bổ sung hoặc mã HTTP không phù hợp. Những giải pháp đó có thể sẽ hoạt động nhưng cảm thấy hơi 'hacky'. Tôi đã đưa ra một giải pháp khác.

Chúng tôi đang sử dụng WIF được định cấu hình để chuyển hướng (passiveRedirectEnables = "true") trên phản hồi 401. Chuyển hướng là hữu ích khi xử lý các yêu cầu thông thường nhưng sẽ không hoạt động đối với các yêu cầu AJAX (vì các trình duyệt sẽ không thực hiện 302 / redirect).

Sử dụng mã sau trong global.asax của bạn, bạn có thể vô hiệu hóa chuyển hướng cho các yêu cầu AJAX:

    void WSFederationAuthenticationModule_AuthorizationFailed(object sender, AuthorizationFailedEventArgs e)
    {
        string requestedWithHeader = HttpContext.Current.Request.Headers["X-Requested-With"];

        if (!string.IsNullOrEmpty(requestedWithHeader) && requestedWithHeader.Equals("XMLHttpRequest", StringComparison.OrdinalIgnoreCase))
        {
            e.RedirectToIdentityProvider = false;
        }
    }

Điều này cho phép bạn trả về 401 phản hồi cho các yêu cầu AJAX, mà javascript của bạn sau đó có thể xử lý bằng cách tải lại trang. Tải lại trang sẽ ném một số 401 sẽ được xử lý bởi WIF (và WIF sẽ chuyển hướng người dùng đến trang đăng nhập).

Một ví dụ javascript để xử lý lỗi 401:

$(document).ajaxError(function (event, jqxhr, settings, exception) {

    if (jqxhr.status == 401) { //Forbidden, go to login
        //Use a reload, WIF will redirect to Login
        location.reload(true);
    }
});

Giải pháp tốt. Cảm ơn.
DanMan

19

Vấn đề này có thể xuất hiện sau đó sử dụng phương pháp ASP.NET MVC RedirectToAction. Để ngăn biểu mẫu hiển thị phản hồi trong div, bạn chỉ cần thực hiện một số loại bộ lọc phản hồi ajax cho các phản hồi không hợp lệ với $ .ajaxSetup . Nếu phản hồi chứa chuyển hướng MVC, bạn có thể đánh giá biểu thức này ở phía JS. Mã ví dụ cho JS bên dưới:

$.ajaxSetup({
    dataFilter: function (data, type) {
        if (data && typeof data == "string") {
            if (data.indexOf('window.location') > -1) {
                eval(data);
            }
        }
        return data;
    }
});

Nếu dữ liệu là: "window.location = '/ Acount / Đăng nhập'" bộ lọc ở trên sẽ nắm bắt điều đó và đánh giá để thực hiện chuyển hướng thay vì để dữ liệu được hiển thị.


datalà trong cơ thể phản ứng hoặc tiêu đề?
GMsoF

16

Kết hợp những gì Vladimir Prudnikov và Thomas Hansen nói:

  • Thay đổi mã phía máy chủ của bạn để phát hiện nếu đó là XHR. Nếu có, hãy đặt mã phản hồi của chuyển hướng thành 278. Trong django:
   if request.is_ajax():
      response.status_code = 278

Điều này làm cho trình duyệt coi phản hồi là một thành công và đưa nó vào Javascript của bạn.

  • Trong JS của bạn, hãy đảm bảo việc gửi biểu mẫu thông qua Ajax, kiểm tra mã phản hồi và chuyển hướng nếu cần:
$('#my-form').submit(function(event){ 

  event.preventDefault();   
  var options = {
    url: $(this).attr('action'),
    type: 'POST',
    complete: function(response, textStatus) {    
      if (response.status == 278) { 
        window.location = response.getResponseHeader('Location')
      }
      else { ... your code here ... } 
    },
    data: $(this).serialize(),   
  };   
  $.ajax(options); 
});

13
    <script>
    function showValues() {
        var str = $("form").serialize();
        $.post('loginUser.html', 
        str,
        function(responseText, responseStatus, responseXML){
            if(responseStatus=="success"){
                window.location= "adminIndex.html";
            }
        });     
    }
</script>

12

Thử

    $(document).ready(function () {
        if ($("#site").length > 0) {
            window.location = "<%= Url.Content("~") %>" + "Login/LogOn";
        }
    });

Đặt nó trên trang đăng nhập. Nếu nó được tải trong một div trên trang chính, nó sẽ chuyển hướng đến trang đăng nhập. "#site" là id của div nằm trên tất cả các trang trừ trang đăng nhập.


12

Mặc dù các câu trả lời dường như có hiệu quả với mọi người nếu bạn đang sử dụng Spring Security, tôi đã tìm thấy việc mở rộng Đăng nhậpUrlAuthenticationEntryPoint và thêm mã cụ thể để xử lý AJAX mạnh mẽ hơn. Hầu hết các ví dụ chặn tất cả các chuyển hướng không chỉ là lỗi xác thực. Điều này là không mong muốn cho dự án tôi làm việc. Bạn cũng có thể thấy cần phải mở rộng ExceptionTranslationFilter và ghi đè phương thức "sendStartAuthentication" để xóa bước bộ đệm nếu bạn không muốn yêu cầu AJAX bị lưu vào bộ đệm.

Ví dụ AjaxAwareAuthenticationEntryPoint:

public class AjaxAwareAuthenticationEntryPoint extends
    LoginUrlAuthenticationEntryPoint {

    public AjaxAwareAuthenticationEntryPoint(String loginUrl) {
        super(loginUrl);
    }

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        if (isAjax(request)) {
            response.sendError(HttpStatus.UNAUTHORIZED.value(), "Please re-authenticate yourself");
        } else {
        super.commence(request, response, authException);
        }
    }

    public static boolean isAjax(HttpServletRequest request) {
        return request != null && "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
    }
}

Nguồn: 1 , 2


3
Nó sẽ hữu ích (với tôi) nếu cử tri xuống sẽ giải thích lý do tại sao họ xuống bỏ phiếu. Nếu có điều gì xấu với giải pháp này, tôi muốn học hỏi từ những sai lầm của mình. Cảm ơn.
Giăng

Nếu sử dụng Spring và cũng sử dụng JSF, thì cũng kiểm tra xem: ("part / ajax"). EqualsIgnoreCase (request.getHeader ("face-request"));
J Slick

Người dùng có thể đã bỏ phiếu vì bạn không đề cập: (1) các mod phía máy khách cần thiết để phát hiện phản hồi lỗi của bạn; (2) các mod cần thiết cho cấu hình Spring để thêm Đăng nhập tùy chỉnhUrlAuthenticationEntryPoint vào chuỗi bộ lọc.
J Slick

Câu trả lời của bạn tương tự như câu trả lời này từ @Arpad. Nó làm việc cho tôi; sử dụng bảo mật mùa xuân 3.2.9. stackoverflow.com/a/8426947/4505142
Darren Parker

11

Tôi đã giải quyết điều này bằng cách đặt phần sau vào trang login.php của tôi.

<script type="text/javascript">
    if (top.location.href.indexOf('login.php') == -1) {
        top.location.href = '/login.php';
    }
</script>

10

Hãy để tôi chỉ trích dẫn lại vấn đề như được mô tả bởi @Steg

Tôi đã có một vấn đề tương tự như của bạn. Tôi thực hiện một yêu cầu ajax có 2 phản hồi có thể: một yêu cầu chuyển hướng trình duyệt sang một trang mới và một yêu cầu thay thế một biểu mẫu HTML hiện có trên trang hiện tại bằng một trang mới.

IMHO đây là một thách thức thực sự và sẽ phải chính thức được mở rộng theo các tiêu chuẩn HTTP hiện tại.

Tôi tin rằng Tiêu chuẩn http mới sẽ sử dụng mã trạng thái mới. nghĩa là: hiện đang báo 301/302cho trình duyệt đi và tìm nạp nội dung của yêu cầu này mới location.

Trong tiêu chuẩn mở rộng, nó sẽ nói rằng nếu phản hồi status: 308(chỉ là một ví dụ), thì trình duyệt sẽ chuyển hướng trang chính đến trang locationđược cung cấp.

Điều đó đang được nói; Tôi có xu hướng bắt chước hành vi trong tương lai này và do đó, khi cần một tài liệu.redirect, tôi có máy chủ trả lời là:

status: 204 No Content
x-status: 308 Document Redirect
x-location: /login.html

Khi JS nhận được " status: 204", nó sẽ kiểm tra sự tồn tại của x-status: 308tiêu đề và thực hiện document.redirect đến trang được cung cấp trong locationtiêu đề.

Chuyện này có ý nghĩa gì với bạn không?


7

Một số có thể tìm thấy dưới đây hữu ích:

Tôi muốn khách hàng được chuyển hướng đến trang đăng nhập cho bất kỳ hành động nghỉ ngơi nào được gửi mà không có mã thông báo ủy quyền. Vì tất cả các hành động nghỉ ngơi của tôi đều dựa trên Ajax, tôi cần một cách chung chung để chuyển hướng đến trang đăng nhập thay vì xử lý hàm thành công Ajax.

Đây là những gì tôi đã làm:

Trên bất kỳ yêu cầu Ajax nào, máy chủ của tôi sẽ trả về phản hồi Json 200 "CẦN TÁC GIẢ" (nếu khách hàng cần xác thực).

Ví dụ đơn giản trong Java (phía máy chủ):

@Secured
@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {

    private final Logger m_logger = LoggerFactory.getLogger(AuthenticationFilter.class);

    public static final String COOKIE_NAME = "token_cookie"; 

    @Override
    public void filter(ContainerRequestContext context) throws IOException {        
        // Check if it has a cookie.
        try {
            Map<String, Cookie> cookies = context.getCookies();

            if (!cookies.containsKey(COOKIE_NAME)) {
                m_logger.debug("No cookie set - redirect to login page");
                throw new AuthenticationException();
            }
        }
        catch (AuthenticationException e) {
            context.abortWith(Response.ok("\"NEED TO AUTHENTICATE\"").type("json/application").build());
        }
    }
}

Trong Javascript của tôi, tôi đã thêm đoạn mã sau:

$.ajaxPrefilter(function(options, originalOptions, jqXHR) {
    var originalSuccess = options.success;

    options.success = function(data) {
        if (data == "NEED TO AUTHENTICATE") {
            window.location.replace("/login.html");
        }
        else {
            originalSuccess(data);
        }
    };      
});

Và đó là về nó.


5

trong servlet, bạn nên đặt response.setStatus(response.SC_MOVED_PERMANENTLY); để gửi trạng thái '301' xmlHttp mà bạn cần để chuyển hướng ...

và trong hàm $ .ajax, bạn không nên sử dụng .toString()hàm ..., chỉ

if (xmlHttp.status == 301) { top.location.href = 'xxxx.jsp'; }

vấn đề là nó không linh hoạt lắm, bạn không thể quyết định nơi bạn muốn chuyển hướng ..

chuyển hướng qua các servlet nên là cách tốt nhất. nhưng tôi vẫn không thể tìm đúng cách để làm điều đó.


5

Tôi chỉ muốn chốt bất kỳ yêu cầu ajax nào cho toàn bộ trang. @SuperG đã giúp tôi bắt đầu. Đây là những gì tôi đã kết thúc với:

// redirect ajax requests that are redirected, not found (404), or forbidden (403.)
$('body').bind('ajaxComplete', function(event,request,settings){
        switch(request.status) {
            case 301: case 404: case 403:                    
                window.location.replace("http://mysite.tld/login");
                break;
        }
});

Tôi muốn kiểm tra cụ thể các mã trạng thái http nhất định để đưa ra quyết định của mình. Tuy nhiên, bạn chỉ có thể liên kết với ajaxError để nhận bất cứ điều gì ngoài thành công (có lẽ chỉ 200?) Tôi có thể vừa viết:

$('body').bind('ajaxError', function(event,request,settings){
    window.location.replace("http://mysite.tld/login");
}

1
cái sau sẽ ẩn bất kỳ lỗi nào khác khiến việc khắc phục sự cố trở nên rắc rối
Tim Abell

1
403 không có nghĩa là người dùng không được xác thực, điều đó có nghĩa là người dùng (có thể được xác thực) không có quyền xem tài nguyên được yêu cầu. Vì vậy, nó không nên chuyển hướng đến trang đăng nhập
Rob

5

Nếu bạn cũng muốn truyền các giá trị thì bạn cũng có thể đặt các biến phiên và truy cập Eg: Trong jsp của bạn, bạn có thể viết

<% HttpSession ses = request.getSession(true);
   String temp=request.getAttribute("what_you_defined"); %>

Và sau đó bạn có thể lưu trữ giá trị tạm thời này trong biến javascript của mình và chơi xung quanh


5

Tôi đã không có bất kỳ thành công nào với giải pháp tiêu đề - chúng không bao giờ được chọn trong phương thức ajaxSuccess / ajaxComplete của tôi. Tôi đã sử dụng câu trả lời của Steg với phản hồi tùy chỉnh, nhưng tôi đã sửa đổi một số bên JS. Tôi thiết lập một phương thức mà tôi gọi trong mỗi hàm để tôi có thể sử dụng các tiêu chuẩn $.get$.postphương thức.

function handleAjaxResponse(data, callback) {
    //Try to convert and parse object
    try {
        if (jQuery.type(data) === "string") {
            data = jQuery.parseJSON(data);
        }
        if (data.error) {
            if (data.error == 'login') {
                window.location.reload();
                return;
            }
            else if (data.error.length > 0) {
                alert(data.error);
                return;
            }
        }
    }
    catch(ex) { }

    if (callback) {
        callback(data);
    }
}

Ví dụ về nó được sử dụng ...

function submitAjaxForm(form, url, action) {
    //Lock form
    form.find('.ajax-submit').hide();
    form.find('.loader').show();

    $.post(url, form.serialize(), function (d) {
        //Unlock form
        form.find('.ajax-submit').show();
        form.find('.loader').hide();

        handleAjaxResponse(d, function (data) {
            // ... more code for if auth passes ...
        });
    });
    return false;
}

5

Cuối cùng, tôi giải quyết vấn đề bằng cách thêm một tùy chỉnh HTTP Header. Ngay trước khi phản hồi cho mọi yêu cầu ở phía máy chủ, tôi thêm url được yêu cầu hiện tại vào tiêu đề của phản hồi.

Loại ứng dụng của tôi trên máy chủ là Asp.Net MVC, và nó có một nơi tốt để làm điều đó. Trong Global.asaxtôi đã thực hiện Application_EndRequestsự kiện này:

    public class MvcApplication : System.Web.HttpApplication
    {

    //  ...
    //  ...

        protected void Application_EndRequest(object sender, EventArgs e)
        {
            var app = (HttpApplication)sender;
            app.Context.Response.Headers.Add("CurrentUrl",app.Context. Request.CurrentExecutionFilePath);
        }

    }

Nó hoạt động hoàn hảo cho tôi! Bây giờ trong mỗi phản ứng của JQuery $.posttôi có yêu cầu urltiêu đề phản ứng khác và cũng có thể đó được đưa ra khi kết quả của POSTphương pháp theo tình trạng 302, 303....

và điều quan trọng khác là không cần sửa đổi mã ở phía máy chủ cũng như phía máy khách.

và tiếp theo là khả năng có được quyền truy cập vào các thông tin khác của hành động đăng bài như lỗi, tin nhắn và ..., theo cách này.

Tôi đã đăng bài này, có thể giúp ai đó :)


4

Tôi đã gặp vấn đề này trên một ứng dụng django mà tôi đang mày mò (từ chối trách nhiệm: Tôi đang mày mò tìm hiểu và không phải là một chuyên gia). Những gì tôi muốn làm là sử dụng jQuery ajax để gửi yêu cầu XÓA đến tài nguyên, xóa nó ở phía máy chủ, sau đó gửi chuyển hướng trở lại (về cơ bản) trang chủ. Khi tôi gửi HttpResponseRedirect('/the-redirect/')từ tập lệnh python, phương thức ajax của jQuery đã nhận được 200 thay vì 302. Vì vậy, những gì tôi đã làm là gửi phản hồi 300 với:

response = HttpResponse(status='300')
response['Location'] = '/the-redirect/' 
return  response

Sau đó, tôi đã gửi / xử lý yêu cầu trên máy khách với jQuery.ajax như vậy:

<button onclick="*the-jquery*">Delete</button>

where *the-jquery* =
$.ajax({ 
  type: 'DELETE', 
  url: '/resource-url/', 
  complete: function(jqxhr){ 
    window.location = jqxhr.getResponseHeader('Location'); 
  } 
});

Có thể sử dụng 300 không "đúng", nhưng ít nhất nó hoạt động giống như tôi muốn.

PS: đây là một nỗi đau rất lớn để chỉnh sửa trên phiên bản di động của SO. ISP ngu ngốc đưa yêu cầu hủy dịch vụ của tôi qua ngay khi tôi hoàn thành câu trả lời của mình!


4

Bạn cũng có thể móc nguyên mẫu gửi XMLHttpRequest. Điều này sẽ làm việc cho tất cả các gửi (jQuery / dojo / etc) với một trình xử lý.

Tôi đã viết mã này để xử lý lỗi hết hạn 500 trang, nhưng nó cũng hoạt động tốt để bẫy chuyển hướng 200. Sẵn sàng mục nhập wikipedia trên XMLHttpRequest onreadystatechange về ý nghĩa của readyState.

// Hook XMLHttpRequest
var oldXMLHttpRequestSend = XMLHttpRequest.prototype.send;

XMLHttpRequest.prototype.send = function() {
  //console.dir( this );

  this.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 500 && this.responseText.indexOf("Expired") != -1) {
      try {
        document.documentElement.innerHTML = this.responseText;
      } catch(error) {
        // IE makes document.documentElement read only
        document.body.innerHTML = this.responseText;
      }
    }
  };

  oldXMLHttpRequestSend.apply(this, arguments);
}

2

Ngoài ra, bạn có thể sẽ muốn chuyển hướng người dùng đến URL tiêu đề đã cho. Vì vậy, cuối cùng nó sẽ trông như thế này:

$.ajax({
    //.... other definition
    complete:function(xmlHttp){
        if(xmlHttp.status.toString()[0]=='3'){
        top.location.href = xmlHttp.getResponseHeader('Location');
    }
});

CẬP NHẬT: Opps. Có cùng một nhiệm vụ, nhưng nó không hoạt động. Làm những thứ này. Tôi sẽ chỉ cho bạn giải pháp khi tôi tìm thấy nó.


4
Câu trả lời này nên được xóa bởi tác giả vì chính anh ta nói rằng điều này không hoạt động. Anh ta có thể đăng một giải pháp làm việc sau đó. -1
TheCrazyProgrammer

Ý tưởng là tốt, nhưng vấn đề là tiêu đề "Vị trí" không bao giờ được thông qua.
Martin Zvarík

Chỉnh sửa của tôi đã bị từ chối, vì vậy ... bạn phải sử dụng "var xmlHttp = $ .ajax ({...." biến không thể ở bên trong ... sau đó sử dụng: console.log (xmlHttp.get ALLResponseHeaders ()) ;
Martin Zvarík

2

Tôi đã có một giải pháp làm việc bằng cách sử dụng các câu trả lời từ liên kết @John và @Arpad và liên kết @RobWinch

Tôi sử dụng Spring Security 3.2.9 và jQuery 1.10.2.

Mở rộng lớp của Spring để chỉ phản hồi 4XX từ các yêu cầu AJAX:

public class CustomLoginUrlAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {

    public CustomLoginUrlAuthenticationEntryPoint(final String loginFormUrl) {
        super(loginFormUrl);
    }

    // For AJAX requests for user that isn't logged in, need to return 403 status.
    // For normal requests, Spring does a (302) redirect to login.jsp which the browser handles normally.
    @Override
    public void commence(final HttpServletRequest request,
                         final HttpServletResponse response,
                         final AuthenticationException authException)
            throws IOException, ServletException {
        if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
        } else {
            super.commence(request, response, authException);
        }
    }
}

applicationContext-security.xml

  <security:http auto-config="false" use-expressions="true" entry-point-ref="customAuthEntryPoint" >
    <security:form-login login-page='/login.jsp' default-target-url='/index.jsp'                             
                         authentication-failure-url="/login.jsp?error=true"
                         />    
    <security:access-denied-handler error-page="/errorPage.jsp"/> 
    <security:logout logout-success-url="/login.jsp?logout" />
...
    <bean id="customAuthEntryPoint" class="com.myapp.utils.CustomLoginUrlAuthenticationEntryPoint" scope="singleton">
        <constructor-arg value="/login.jsp" />
    </bean>
...
<bean id="requestCache" class="org.springframework.security.web.savedrequest.HttpSessionRequestCache">
    <property name="requestMatcher">
      <bean class="org.springframework.security.web.util.matcher.NegatedRequestMatcher">
        <constructor-arg>
          <bean class="org.springframework.security.web.util.matcher.MediaTypeRequestMatcher">
            <constructor-arg>
              <bean class="org.springframework.web.accept.HeaderContentNegotiationStrategy"/>
            </constructor-arg>
            <constructor-arg value="#{T(org.springframework.http.MediaType).APPLICATION_JSON}"/>
            <property name="useEquals" value="true"/>
          </bean>
        </constructor-arg>
      </bean>
    </property>
</bean>

Trong các tệp JSP của tôi, thêm trình xử lý lỗi AJAX toàn cầu như được hiển thị ở đây

  $( document ).ajaxError(function( event, jqxhr, settings, thrownError ) {
      if ( jqxhr.status === 403 ) {
          window.location = "login.jsp";
      } else {
          if(thrownError != null) {
              alert(thrownError);
          } else {
              alert("error");
          }
      }
  });

Ngoài ra, xóa các trình xử lý lỗi hiện có khỏi các cuộc gọi AJAX trong các trang JSP:

        var str = $("#viewForm").serialize();
        $.ajax({
            url: "get_mongoDB_doc_versions.do",
            type: "post",
            data: str,
            cache: false,
            async: false,
            dataType: "json",
            success: function(data) { ... },
//            error: function (jqXHR, textStatus, errorStr) {
//                 if(textStatus != null)
//                     alert(textStatus);
//                 else if(errorStr != null)
//                     alert(errorStr);
//                 else
//                     alert("error");
//            }
        });

Tôi hy vọng nó sẽ giúp người khác.

Update1 Tôi thấy rằng tôi cần thêm tùy chọn (luôn luôn sử dụng-default-target = "true") vào cấu hình đăng nhập mẫu. Điều này là cần thiết vì sau khi yêu cầu AJAX được chuyển hướng đến trang đăng nhập (do phiên hết hạn), Spring nhớ lại yêu cầu AJAX trước đó và tự động chuyển hướng đến nó sau khi đăng nhập. Điều này khiến JSON trả về được hiển thị trên trang trình duyệt. Tất nhiên, không phải những gì tôi muốn.

Update2 Thay vì sử dụng always-use-default-target="true", hãy sử dụng ví dụ @RobWinch về việc chặn các yêu cầu AJAX từ requstCache. Điều này cho phép các liên kết bình thường được chuyển hướng đến mục tiêu ban đầu của chúng sau khi đăng nhập, nhưng AJAX đi đến trang chủ sau khi đăng nhập.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.