>  기사  >  웹 프론트엔드  >  Ajax 동기화의 쿠키 문제에 대하여

Ajax 동기화의 쿠키 문제에 대하여

一个新手
一个新手원래의
2017-10-24 09:41:372055검색

머리말

이런 문제가 발생하면 정말 속수무책입니다. 프론트엔드 브라우저 호환성은 언제나 골치 아픈 문제였습니다.

이 글은 그토록 당황스럽고 무기력했던 날만을 기록합니다. 모두의 심심함을 달래는데 활용해보세요 T_T

현장이 재현되어있습니다

동료: 어서! 어서 해봐요! 온라인에서 문제가 발생했습니다. !
나: 대체 뭐야?! 뭐야?!
동료: 이번 출시로 인한 건가요?
나: 롤백하세요! 롤백! (먹으려고 할 때 왜 체인을 떨어뜨리는가! 배를 돌볼 수 없잖아! 빨리 확인)
...

혼란스러운 대화 끝에 진정할 수 있는 건 '지뢰 제거' 뿐이다. ".

롤백, 프록시, 패킷 캡처, 비교, 단일 요소 문제 해결. . .

콤보 펀치 세트를 마친 후 마침내 결함을 찾는 데 향 한 개 정도가 걸렸습니다. Ajax 동기화 콜백에 문제가 있는 것으로 밝혀졌습니다! 무모한! 그러면 안 됩니다! 그런 수술이 있나요? !

문제 재발

한 문장으로 문제 요약

ajax를 사용하여 "동기화" 요청을 하면 성공에서 이 대상 쿠키를 반환합니다. > 콜백이 실패했습니다! document.cookie는 ajax 실행이 완료될 때까지 업데이트되지 않습니다success回调中读取此目标cookie 失败!ajax执行结束后 document.cookie 才会被更新

影响范围

PC 端和 Android 端影响范围小,属于偶现。

IOS 端是重灾区,出来 Chrome 和 Safari 浏览器外的绝大多说浏览器都会出现此问题,并且 App 内置的 Webview 环境同样不能幸免。

在本同步请求回调内预读取本请求返回的 cookie 会产生问题。

半壁江山都沦陷了,我要这铁棒有何用!

追因溯果

小范围的兼容问题我姑且可以饶你,奈何你如此猖狂,怎能任你瞒天过海!

纵向对比

排除一些干扰项,还原其本质,我们分别用框架nej,jQueryjs写几个相同功能的“同步” demo,走着瞧着。。

【nej.html】使用 NEJ 库

<!DOCTYPE html>
<html>
<head>
    <title>nej</title>
    <meta charset="utf-8" />
</head>
<body>
    test
    <script src="http://nej.netease.com/nej/src/define.js?pro=./"></script>
    <script>
        define([
            &#39;{lib}util/ajax/xdr.js&#39;
        ], function () {
            var _j = NEJ.P(&#39;nej.j&#39;);
            _j._$request(&#39;/api&#39;, {
                sync: true,
                method: &#39;POST&#39;,
                onload: function (_data) {
                    alert("cookie:\n" + document.cookie)
                }
            });
        });
    </script>
</body>
</html>

【jquery.html】使用 jQuery 库

<!DOCTYPE html>
<html>
<head>
    <title>jquery</title>
    <meta charset="utf-8" />
</head>
<body>
    jquery
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script>
        $.ajax({
            url: &#39;/api&#39;,
            async: false,
            method: &#39;POST&#39;,
            success: function (result) {
                alert("cookie:\n" + document.cookie)
            }
        });
    </script>
</body>
</html>

【js.html】自己实现的 ajax 请求函数

<!DOCTYPE html>
<html>
<head>
    <title>JS</title>
    <meta charset="utf-8" />
</head>
<body>
    js
    <script>
        var _$ajax = (function () {
            /**
            * 生产XHR兼容IE6
            */
            var createXHR = function () {
                if (typeof XMLHttpRequest != "undefined") { // 非IE6浏览器
                    return new XMLHttpRequest();
                } else if (typeof ActiveXObject != "undefined") {   // IE6浏览器
                    var version = [
                        "MSXML2.XMLHttp.6.0",
                        "MSXML2.XMLHttp.3.0",
                        "MSXML2.XMLHttp",
                    ];
                    for (var i = 0; i < version.length; i++) {
                        try {
                            return new ActiveXObject(version[i]);
                        } catch (e) {
                            return null
                        }
                    }
                } else {
                    throw new Error("您的系统或浏览器不支持XHR对象!");
                }
            };
            /**
            * 将JSON格式转化为字符串
            */
            var formatParams = function (data) {
                var arr = [];
                for (var name in data) {
                    arr.push(name + "=" + data[name]);
                }
                arr.push("nocache=" + new Date().getTime());
                return arr.join("&");
            };
            /**
            * 字符串转换为JSON对象,兼容IE6
            */
            var _getJson = (function () {
                var e = function (e) {
                    try {
                        return new Function("return " + e)()
                    } catch (n) {
                        return null
                    }
                };
                return function (n) {
                    if ("string" != typeof n) return n;
                    try {
                        if (window.JSON && JSON.parse) return JSON.parse(n)
                    } catch (t) {
                    }
                    return e(n)
                };
            })();

            /**
            * 回调函数
            */
            var callBack = function (xhr, options) {
                if (xhr.readyState == 4 && !options.requestDone) {
                    var status = xhr.status;
                    if (status >= 200 && status < 300) {
                        options.success && options.success(_getJson(xhr.responseText));
                    } else {
                        options.error && options.error();
                    }
                    //清空状态
                    this.xhr = null;
                    clearTimeout(options.reqTimeout);
                } else if (!options.requestDone) {
                    //设置超时
                    if (!options.reqTimeout) {
                        options.reqTimeout = setTimeout(function () {
                            options.requestDone = true;
                            !!this.xhr && this.xhr.abort();
                            clearTimeout(options.reqTimeout);
                        }, !options.timeout ? 5000 : options.timeout);
                    }
                }
            };
            return function (options) {
                options = options || {};
                options.requestDone = false;
                options.type = (options.type || "GET").toUpperCase();
                options.dataType = options.dataType || "json";
                options.contentType = options.contentType || "application/x-www-form-urlencoded";
                options.async = options.async;
                var params = options.data;
                //创建 - 第一步
                var xhr = createXHR();
                //接收 - 第三步
                xhr.onreadystatechange = function () {
                    callBack(xhr, options);
                };
                //连接 和 发送 - 第二步
                if (options.type == "GET") {
                    params = formatParams(params);
                    xhr.open("GET", options.url + "?" + params, options.async);
                    xhr.send(null);
                } else if (options.type == "POST") {
                    xhr.open("POST", options.url, options.async);
                    //设置表单提交时的内容类型
                    xhr.setRequestHeader("Content-Type", options.contentType);
                    xhr.send(params);
                }
            }
        })();
        _$ajax({
            url: &#39;/api&#39;,
            async: false,
            type: &#39;POST&#39;,
            success: function (result) {
                alert("cookie:\n" + document.cookie)
            }
        });
    </script>
</body>
</html>

三个文件都是一样的,在html 加载完之后发起一个同步请求,该请求会返回一个 cookie,在回调中将document.cookie打印出来,检测是否已经在回调时写入的了 cookie。

下面使用 node 实现这个可写 cookie 的服务。
【serve.js】

var express = require("express");
var http = require("http");
var fs = require("fs");
var app = express();

var router = express.Router();
router.post(&#39;/api&#39;, function (req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
    res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");
    res.header("Set-Cookie", ["target=ccccccc|" + new Date()]);
    res.end(&#39;ok&#39;);
});

router.get(&#39;/test1&#39;, function (req, res, next) {
    fs.readFile("./nej.html", function (err, data) {
        res.end(data);
    });
});

router.get(&#39;/test2&#39;, function (req, res, next) {
    fs.readFile("./jquery.html", function (err, data) {
        res.end(data);
    });
});

router.get(&#39;/test3&#39;, function (req, res, next) {
    fs.readFile("./js.html", function (err, data) {
        res.end(data);
    });
});

app.use(&#39;/&#39;, router);
http.createServer(app).listen(3000);

好了,万事大吉,run 一把

$ node serve.js

操作

我们依次执行如下操作,

  1. 使用 ios 端 QQ 浏览器,清空所有缓存

  2. 加载其中一个页面,观察是否有目标 cookie 输出

  3. 执行刷新操作,观察是否有目标 cookie 输出,比较 cookie 输出的时间戳,确认是否为上次 cookie 的同步结果而非本次请求获取的 cookie,

  4. 清空所有缓存,切换目标 html 文件,循环执行2,3,4步骤

结果

【nej.html】

  • 纯净环境加载,未读取到目标 cookie

  • 刷新加载,读取到上一次请求返回的 cookie

【jquery.html】

  • 纯净环境加载,未读取到目标 cookie

  • 刷新加载,未读取到目标 cookie

【js.html】

  • 纯净环境加载,未读取到目标 cookie

  • 刷新加载,未读取到目标 cookie

咦?结果不一样!使用 nej 的第二次加载读取到了第一次 cookie。其他的两次均为获取到。

原因

nej 依赖框架的加载是异步的,当同步请求发起时,dom 已经加载完毕,回调相应时,document.cookie已经呈“ready”状态,可读可写。但请求依然获取不到自身返回携带的 cookie。

Ajax 동기화의 쿠키 문제에 대하여

而其他两种加载的机制阻塞了 dom 的加载,导致同步请求发起时,dom 尚未加载完成,回调相应时,document.cookie

영향 범위

PC와 Android에 미치는 영향 범위는 작으며 간헐적으로 발생합니다.

IOS가 가장 큰 타격을 받는 분야입니다. Chrome과 Safari를 제외한 대부분의 브라우저에서 이 문제가 발생하며, 앱에 내장된 Webview 환경도 면역되지 않습니다.

이 동기 요청 콜백 내에서 이 요청에 의해 반환된 쿠키를 미리 읽으면 문제가 발생할 수 있습니다.

국토의 절반이 망했는데 이 쇠막대가 무슨 소용이 있겠는가!

    원인 추적
  • 미세한 호환성 문제는 용서할 수 있는데 너무 오만해서 어떻게 숨길 수 있겠습니까?
  • 수직 비교

  • 일부 간섭 항목을 제거하고 본질을 복원하기 위해 nej, jQueryjs 프레임워크를 사용하여 여러 항목을 작성합니다. 동일 "동기화" 기능의 데모입니다. 기다려 보겠습니다. .

【nej.html】NEJ 라이브러리 사용

$(&#39;document&#39;).click(function () {
    // TODO 发起同步请求
});
    【jquery.html】jQuery 라이브러리 사용
  • _$ajax({
        url: &#39;/api&#39;,
        async: false,
        type: &#39;POST&#39;,
        success: function (result) {
            setTimeout(function(){
                // do something 在此处获取 cookie 操作是安全的
            },0)
        }
    });

    【js.html】직접 구현한 ajax 요청 기능

    rrreee
  • 세 파일은 동일하며 html로 로딩 후 그런 다음 콜백에 document.cookie를 인쇄하여 쿠키를 반환하는 동기 요청을 시작하여 콜백 중에 쿠키가 기록되었는지 확인합니다.
  • 다음은 노드를 사용하여 쓰기 가능한 쿠키 서비스를 구현합니다.

    【serve.js】
  • rrreee
괜찮습니다. 실행하세요

rrreee

Operation
  • 다음 작업을 순서대로 수행합니다.

      🎜ios에서 QQ 브라우저 사용 , 모든 캐시 지우기🎜🎜🎜🎜페이지 중 하나를 로드하고 대상 쿠키 출력이 있는지 관찰🎜🎜🎜🎜새로 고침 작업을 수행하고, 대상 쿠키 출력이 있는지 관찰하고, 쿠키 출력의 타임스탬프를 비교하고 확인합니다. 마지막 쿠키의 동기화 결과인지 여부 이 요청으로 얻은 쿠키 대신 🎜🎜🎜🎜모든 캐시를 지우고 대상 html 파일을 전환하고 2, 3, 4단계를 반복🎜🎜
    🎜 결과🎜🎜[nej.html]🎜🎜🎜🎜순수 환경 로딩, 대상 쿠키가 읽히지 않음🎜🎜🎜🎜새로고침 로딩, 이전 요청에서 반환된 쿠키가 읽음🎜🎜🎜🎜【jquery.html】🎜🎜 🎜🎜순수 환경 로딩, 대상 쿠키를 읽지 않음🎜🎜 🎜🎜새로 고침 및 로드, 대상 쿠키를 읽지 않음🎜🎜🎜🎜[js.html]🎜🎜🎜🎜순수 환경 로딩, 대상 쿠키를 읽지 않음🎜 🎜🎜🎜새로고침하고 로드하면 대상 쿠키가 읽혀지지 않습니다🎜🎜🎜 🎜응? 결과는 다릅니다! nej를 사용한 두 번째 로드는 첫 번째 쿠키를 읽습니다. 나머지 두 번은 얻었습니다. 🎜🎜Reason🎜🎜nej 종속 프레임워크의 로딩은 비동기식입니다. 콜백이 응답되면 document.cookie가 이미 "준비" 상태입니다. 상태를 읽고 쓸 수 있습니다. 그러나 요청은 여전히 ​​자체적으로 반환된 쿠키를 얻을 수 없습니다. 🎜🎜Ajax 동기화의 쿠키 문제에 대하여🎜 🎜다른 두 가지 로드 메커니즘은 DOM 로드를 차단하여 동기 요청이 시작될 때 DOM이 로드되지 않게 만듭니다. 콜백이 응답하면 document.cookie는 여전히 쓸 수 없습니다. 🎜🎜Single Factor Control🎜🎜위 HTML 파일의 로직을 수정하겠습니다. 🎜문서를 클릭하고 트리거될 때까지 동기화 요청을 지연하세요. 🎜다음🎜rrreee🎜은 여전히 ​​위의 실행 단계입니다. 결과를 살펴보겠습니다🎜🎜Results🎜🎜[nej.html]🎜🎜🎜🎜순수 환경 로딩, 대상 쿠키를 읽지 못했습니다🎜🎜🎜🎜새로 고침 로딩 , 이전 요청에서 반환된 쿠키 읽기🎜🎜🎜🎜[jquery.html]🎜🎜🎜🎜순수한 환경 로딩, 대상 쿠키가 읽히지 않음🎜🎜🎜🎜로드를 새로 고치고 이전 요청에서 반환된 쿠키 읽기🎜🎜 🎜🎜【js.html】🎜🎜🎜🎜순수 환경 로딩, 대상 쿠키를 읽지 않음🎜
  • 刷新加载,读取到上一次请求返回的 cookie

结果和预期一样,本次请求无法获取本期返回的目标 cookie,请求回调执行后,目标cookie才会更新到document.cookie上。

特例

在执行以上操作是,发现,【jquery.html】的执行结果时不时会有两种结果

  • 纯净环境加载,未读取到目标 cookie

  • 刷新加载,读取到上一次请求返回的 cookie
    另外一种几率较小,但也会出现

  • 纯净环境加载,读取到目标 cookie

  • 刷新加载,读取到目标 cookie

产生原因

一言不合看源码

我们在 jquery 的源码中看到,jquery 的success回调绑定在了 onload 事件上

https://code.jquery.com/jquery-3.2.1.js :9533行

Ajax 동기화의 쿠키 문제에 대하여

而我自己实现的和 nej 的实现均是将success回调绑定在了 onreadystatechange 事件上,唯一的区别就在于此

一个正向的 ajax 请求,会先触发两次onreadystatechange,在触发onload,或许原因在于document.cookie的同步有几率在onload事件触发前完成??I'm not sure.

问题结论

  1. 在 PC 端,Android 端,IOS 端Chrome、Safari 浏览器环境下,ajax 的同步请求的回调方法中,取到本请求返回的 cookie 失败几率低

  2. IOS 端,QQ 浏览器、App 内置Webview浏览器环境下,失败率极高。

解决方案

只有问题没有方案的都是在耍流氓!

方案1 - 明修栈道暗度陈仓

将回调方法中的 cookie 获取方法转化为异步操作。

_$ajax({
    url: &#39;/api&#39;,
    async: false,
    type: &#39;POST&#39;,
    success: function (result) {
        setTimeout(function(){
            // do something 在此处获取 cookie 操作是安全的
        },0)
    }
});

方案2 - 不抵抗政策

没有把握的方案,我们是要斟酌着实施的。

如果你不能100%却被操作的安全性,那并不建议你强行使用 ajax 的同步操作,很多机制并不会像我们自以为是的那样理所应当。



위 내용은 Ajax 동기화의 쿠키 문제에 대하여의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.