На своем сайте я использую Codecha — программистскую капчу. Это уникальная капча, для решения которой требуется написать тело функции, решающей поставленную задачу, на одном из выбранных языков программирования.

КДПВ —  виджет этой самой капчи

Она не обеспечивает столь же надежной защиты от ботов, как, например, Google reCAPTCHA (набор заданий ограничен и их довольно быстро можно прорешать, а затем отдавать готовые ответы), но зато помогает против не-программистов (для определенной категории форумов толпы студентов, просящих выполнить их лабораторную работу — настоящая проблема, хуже спам-ботов). Но заметка не об этом.

Проблема, с которой я столкнулся в ходе использования Codecha, — совершенно неудобный API как самого виджета капчи, так и сервера.

Для начала предлагаю вкратце посмотреть на способ подключения виджета. Копировать документацию не буду, только самое главное.

Итак, регистрируемся, где-то в форме добавляем следующий код:

<script type="text/javascript" src="//codecha.org/api/challenge?k=YOUR_PUBLIC_KEY"> </script>

С виджетом все. Теперь при отправке формы нужно получить поля codecha_challenge_field и codecha_response_field, а затем отправить их на сервер для проверки. Здесь и далее серверный код будет на Node.js.

Парсим форму, вызываем функцию проверки (используем промисы и q-io):

var HTTP = require("q-io/http");

var checkCaptcha = function(req, fields) {
    var challenge = fields.codecha_challenge_field;
    var response = fields.codecha_response_field;
    if (!challenge)
        return Promise.reject("Captcha challenge is empty");
    if (!response)
        return Promise.reject("Captcha is empty", "error");
    var body = `challenge=${challenge}&response=${response}&remoteip=${req.ip}&privatekey=PRIVTE_KEY`;
    var url = "http://codecha.org/api/verify";
    return HTTP.request({
        url: url,
        method: "POST",
        body: [body],
        headers: {
            "Content-Type": "application/x-www-form-urlencoded",
            "Content-Length": Buffer.byteLength(body)
        },
        timeout: (15 * 1000) //15 секунд
    }).then(function(response) {
        if (response.status != 200)
            return Promise.reject("Failed to check captcha");
        return response.body.read("utf8");
    }).then(function(data) {
        var result = data.toString();
        if (result.replace("true") == result)
            return Promise.reject("Invalid captcha");
        return Promise.resolve();
    });
};

При успешной проверке будет возвращена строка «true», иначе — строка «false».

Казалось бы, все просто, какие вообще могут быть вопросы? Но нет. Веселье начинается, когда пользователь отправил одно сообщение при помощи AJAX и хочет отправить следующее без обновления страницы. Для этого reCAPTCHA, например, предоставляет метод grecaptcha.reset, а вот у Codecha такого метода не имеется. При решении капчи весь ее HTML удаляется, остается лишь текст, сообщающий об успешном решении задания. Приходится обновлять страницу.

То же самое происходит, если, к примеру, во время проверки капчи на сервере произошла ошибка. В таком случае пара задание-решение на сервере Codecha инвалидируется, и повторная проверка той же пары выдаст «false». Пользователю опять придется обновлять страницу (в этом случае он еще и потеряет все введенные в форму данные).

Нехорошо получается. Может, есть какой-то скрытый API виджета? Должен быть. Но нет.

Смотрим код того самого скрипта, который вставляли в форму
var codecha = {
    language: "PHP",
    publicKey: "e37ea65c651a4eada4d5d4e97ae92d90",
    
    fieldPrefix: "codecha_",
    base_url: "//codecha.org",
    spinner_path: "/static/ajax-loader.gif",
    css_path: "/static/widget.css",
    survey: true,
    callbacks: {},
};

codecha.callbacks.hideErrorOverlay = function() {
    codecha.errorOverlayDiv.hidden = true;
    return false;
}

codecha.callbacks.codeSubmit = function() {
    var xhr = codecha.CORCSRequest(codecha.base_url + "/api/code");

    codecha.disable();

    var params = {
        'challenge': codecha.challenge,
        'code': codecha.codeArea.value
    };

    xhr.send(codecha.serialize(params));

    return false;
};

codecha.callbacks.choseLanguage = function() {
    codecha.languageSelector.hidden = false;
    codecha.languageSelector.style.display = '';
    codecha.changeChallenge.value = "\u2713";
    codecha.button.disabled = true;

    codecha.changeChallenge.onclick = codecha.callbacks.requestNewChallenge;
    return false;
};

codecha.callbacks.requestNewChallenge = function() {
    codecha.disable();
    codecha.setStatus("waiting");
    codecha.languageSelector.hidden = true;
    codecha.languageSelector.style.display = 'none';
    codecha.changeChallenge.value = "change lang";

    var lang = codecha.languageSelector[codecha.languageSelector.selectedIndex].value;

    var xhr = codecha.CORCSRequest(codecha.base_url + "/api/change");

    var params = {
        'challenge': codecha.challenge,
        'k': codecha.publicKey,
        'lang': lang
    };

    xhr.send(codecha.serialize(params));
    
    codecha.changeChallenge.onclick = codecha.callbacks.choseLanguage;
    return false;
};

codecha.callbacks.textAreaKeyPress = function(ev) {
    object = codecha.codeArea;
    if (ev.keyCode == 9)
    {
        start = object.selectionStart;
        end = object.selectionEnd;


        object.value = object.value.substring(0, start) + "\t" + object.value.substr(end);
        object.setSelectionRange(start + 1, start + 1);

        object.selectionStart = object.selectionEnd = start + 1;

        return false;
    }

    return true;
};

codecha.callbacks.updateState = function() {
    var xhr = codecha.CORCSRequest(codecha.base_url + "/api/state");


    xhr.send(codecha.serialize({ 'challenge': codecha.challenge }));

    return false;
};

codecha.callbacks.sendSurvey = function() {
    var xhr = codecha.CORCSRequest(codecha.base_url + "/api/survey");

    var mark = codecha.surveyMark[codecha.surveyMark.selectedIndex].value;

    var params = {
        'challenge': codecha.challenge,
        'response': codecha.response,
        'mark': mark,
        'opinion' : codecha.surveyOpinion.value
    };

    xhr.send(codecha.serialize(params));

    return false;
};

codecha.callbacks.switchToRecaptcha = function() {
    var challengeField = document.getElementById(codecha.fieldPrefix + "challenge_field");
    var responseField = document.getElementById(codecha.fieldPrefix + "response_field");
    
    codecha.removeElement(codecha.mainDiv);
    codecha.removeElement(codecha.recaptchaSwitch);
    codecha.removeElement(challengeField);
    codecha.removeElement(responseField);

    codecha.recaptchaDiv.hidden = false;

    var xhr = codecha.CORCSRequest(codecha.base_url + "/api/recaptchify");
    var params = { 'challenge': codecha.challenge };

    xhr.send(codecha.serialize(params));

    return false;
};

codecha.removeElement = function(element) {
    element.parentElement.removeChild(element);
};

codecha.removeRecatpcha = function() {
    this.removeElement(this.recaptchaDiv);
};

codecha.escape = function(str) {
    var div = document.createElement('div');
    div.appendChild(document.createTextNode(str));
    return div.innerHTML;
};

codecha.serialize = function(obj) {
    array = [];

    for (key in obj) {
        array[array.length] = encodeURIComponent(key) + "=" + encodeURIComponent(obj[key]);
    }

    var result = array.join("&"); 
    result = result.replace(/%20/g, "+");
    return result;
};

codecha.enable = function() {
    this.button.disabled = false;
    this.changeChallenge.disabled = false;
    this.codeArea.disabled = false;
    this.spinner.hidden = true;
};

codecha.disable = function() {
    this.button.disabled = true;
    this.changeChallenge.disabled = true;
    this.codeArea.disabled = true;
    this.spinner.hidden = false;
};

codecha.inject_css = function() {
    var css_link=document.createElement("link");
    css_link.setAttribute("rel", "stylesheet");
    css_link.setAttribute("type", "text/css");
    css_link.setAttribute("href", codecha.base_url + codecha.css_path);
    document.getElementsByTagName("head")[0].appendChild(css_link);
};

codecha.setStatus = function(state) {
    this.statusSpan.innerHTML = state;
};

codecha.setResponseFields = function() {
    var challengeField = document.getElementById(this.fieldPrefix + "challenge_field");
    var responseField = document.getElementById(this.fieldPrefix + "response_field");

    challengeField.value = this.challenge;
    responseField.value = this.response;
    
};

codecha.setChallenge = function(uuid, language_name, wording, top, sampleCode, bottom) {
    this.challenge = uuid;
    this.wordingDiv.innerHTML = "<strong>" + language_name + ":</strong> " + wording;
    this.codeAreaTop.innerHTML = "<pre>\n"+this.escape(top)+"</pre>";
    this.codeArea.value = sampleCode;
    this.codeAreaBottom.innerHTML = this.escape(bottom);

    if (top.length > 0) {
        this.codeAreaTop.hidden = false;
    } else {
        this.codeAreaTop.hidden = true;
    }

    if (bottom.length > 0) {
        this.codeAreaBottom.hidden = false;
    } else {
        this.codeAreaBottom.hidden = true;
    }

};

codecha.showErrorMessage = function(message) {
    this.errorMessageDiv.innerHTML = message;
    this.errorOverlayDiv.hidden = false;
};

codecha.showSurvey = function() {
    codecha.mainDiv.innerHTML = "        <strong>Challenge completed! You may proceed.</strong>

         If you have some spare time you may help us improve our widget by answearing any question below.

         Challenge was: 
        <select id=\"codecha_survey_mark_selector\">             <option value=\"5\">a way too hard</option>             <option value=\"4\">a bit too hard</option>             <option value=\"3\" selected>perfect</option>             <option value=\"2\">a bit too easy</option>             <option value=\"1\">a way too easy</option>         </select>
         How do you like our widget?
         <textarea id=\"codcha_survey_opinion_area\" name=\"codcha_survey_opinion_area\">I like/dislike it because...</textarea>         <input type=\"submit\" class=\"codecha_button\" name=\"codecha_survey_submit\" id=\"codecha_survey_submit\" value=\"SUBMIT\"/>        ";

    codecha.surveySubmit = document.getElementById("codecha_survey_submit");
    codecha.surveyMark = document.getElementById("codecha_survey_mark_selector");
    codecha.surveyOpinion = document.getElementById("codcha_survey_opinion_area");
    codecha.surveySubmit.onclick = codecha.callbacks.sendSurvey;
};

codecha.CORCSRequest = function (url) {
    var xhr = new XMLHttpRequest();

    if ("withCredentials" in xhr) {
        xhr.open("POST", url, true);
    } else if (typeof XDomainRequest != "undefined") {
        xhr = new XDomainRequest();
        xhr.open("POST", url);
    } else {
        xhr = null;
    }

    xhr.onload = function() { eval(this.responseText); };

    xhr.onerror = function() {
        alert("Error!");
        codecha.enable();
    };

    xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

    return xhr;
};

codecha.init = function() {
    document.write(
            "<input type=\"hidden\" id=\"" + this.fieldPrefix + "challenge_field\" /name=\"" + this.fieldPrefix + "challenge_field\" />",
            "<input type=\"hidden\" id=\"" + this.fieldPrefix + "response_field\" name=\"" + this.fieldPrefix + "response_field\" />",
            "<div id=\"codecha_widget\">",
                "<div id=\"codecha_error_overlay\">",
                    "<a href=\"#\" id=\"codecha_error_overlay_hide\">hide</a>",
                    "<div id=\"codecha_error_message\"></div>",
                "</div>",
                "<div id=\"codecha_wording\"></div>",
                "<div id=\"codecha_code_area_top\"></div>",
                "<textarea name=\"codecha_code_area\" id=\"codecha_code_area\">",
                "</textarea>",
                "<div id=\"codecha_code_area_bottom\"></div>",
                "<div id=\"codecha_bottom_container\">",
                    
                        "<a title=\"click to learn more\" href=\"" + codecha.base_url + "/about\" target=\"_blank\" id=\"codecha_about\">Codecha</a>",
                    
                    "<div id=\"codecha_bottom\">",
                        "<span id=\"codecha_spinner\">",
                            "<span id=\"codecha_status\">waiting</span>",
                            "<img alt=\"spinner\" src=\"" + codecha.base_url + codecha.spinner_path + "\" />",
                        "</span>",
                        "<select id=\"codecha_language_selector\">",
                        
                            "<option value=\"c\" >C/C++</option>",
                        
                            "<option value=\"java\" >Java</option>",
                        
                            "<option value=\"python\" >Python</option>",
                        
                            "<option value=\"ruby\" >Ruby</option>",
                        
                            "<option value=\"php\"  selected >PHP</option>",
                        
                            "<option value=\"haskell\" >Haskell</option>",
                        
                        "</select>",
                        "<input type=\"submit\" class=\"codecha_button\" name=\"codecha_change_challenge\" id=\"codecha_change_challenge\" title=\"request new challenge\" value=\"change lang\"/>",
                        "<input type=\"submit\" class=\"codecha_button\" name=\"codecha_code_submit_button\" id=\"codecha_code_submit_button\" value=\"VERIFY\"/>",
                    "</div>",
                "</div>",
            "</div>",
            
            "<div id=\"codecha_recaptcha\">",
            
            "</div>"
        );

    this.mainDiv = document.getElementById("codecha_widget");
    this.codeArea = document.getElementById("codecha_code_area");
    this.codeAreaTop = document.getElementById("codecha_code_area_top");
    this.codeAreaBottom = document.getElementById("codecha_code_area_bottom");
    this.wordingDiv = document.getElementById("codecha_wording");
    this.errorOverlayDiv = document.getElementById("codecha_error_overlay");
    this.errorMessageDiv = document.getElementById("codecha_error_message");
    this.errorHide = document.getElementById("codecha_error_overlay_hide");
    this.button = document.getElementById("codecha_code_submit_button");
    this.spinner = document.getElementById("codecha_spinner");
    this.statusSpan = document.getElementById("codecha_status");
    this.languageSelector = document.getElementById("codecha_language_selector");
    this.recaptchaDiv = document.getElementById("codecha_recaptcha");

    

    this.changeChallenge = document.getElementById("codecha_change_challenge");

    this.changeChallenge.onclick = codecha.callbacks.choseLanguage;
    this.button.onclick = codecha.callbacks.codeSubmit;
    this.errorHide.onclick = codecha.callbacks.hideErrorOverlay;
    this.codeArea.onkeydown = codecha.callbacks.textAreaKeyPress;

    

    this.errorOverlayDiv.hidden = true;
    this.spinner.hidden = true;
    this.languageSelector.hidden = true;
    this.languageSelector.style.display = 'none';

    
    
    
        this.inject_css();
    

    this.enable();

    codecha.setChallenge("d847842d3225459582722c8695ef8523", "PHP", "For given numbers \u0022a\u0022 and \u0022b\u0022 write a function named \u0022lessab\u0022 that returns the value \u00221\u0022 if \u0022a\u0022 is less than \u0022b\u0022, and returns the value \u00220\u0022 otherwise.\u000A",
            "function lessab($a,$b) {\u000A",
            "# put your code here\u000A",
            "\u000A}\u000A");
};

codecha.init();


Откройте спойлер, нажмите Ctrl+F и введите «codecha.init». Да-да, это правда. Можете сами попробовать. Это именно HTML-код в виде строки, добавляемый на страницу при помощи document.write. Даже не спрашивайте меня, что в этом плохого.

Но и это еще не все! (с) реклама чудо-приборов

Как же происходит, например, смена языка? Отправляем запрос, а потом… барабанная дробь…

xhr.onload = function() { eval(this.responseText); };

О, да! Тут бы можно было и закончить, но я все же решил доесть кактус до конца. Для этого я написал свою обертку над Codecha. Не буду слишком вдаваться в разглагольствования, приведу сразу код с пояснениями.

Итак, HTML виджета:

<div id="captcha" class="codechaContainer">
    <input type="hidden" id="codecha_public_key" value="PUBLIC_KEY" />
    <input type="hidden" id="codecha_challenge_field" name="codecha_challenge_field" value="CHALLENGE" />
    <input type="hidden" id="codecha_response_field" name="codecha_response_field" />
    <div id="codecha_ready_widget" style="display: none;"><strong>Challenge completed! You may proceed.</strong></div>
    <div id="codecha_widget">
        <div id="codecha_error_overlay" hidden="true">
            <a id="codecha_error_overlay_hide" href="javascript:void(0);" onclick="codecha.hideErrorOverlay();">hide</a>
            <div id="codecha_error_message"></div>
        </div>
        <div id="codecha_wording"></div>
        <div id="codecha_code_area_top"></div>
        <textarea id="codecha_code_area" name="codecha_code_area"></textarea>
        <div id="codecha_code_area_bottom"></div>
        <div id="codecha_bottom_container">
            <a title="click to learn more" href="//codecha.org/about" target="_blank" id="codecha_about">Codecha</a>
            <div id="codecha_bottom">
                <span id="codecha_spinner" hidden="true">
                    <span id="codecha_status">waiting</span><img src="//codecha.org/static/ajax-loader.gif" />
                </span>
                <select id="codecha_language_selector" hidden="true" style="display: none;">
                    <option value="c" selected="true">C/C++</option>
                    <option value="java">Java</option>
                    <option value="python">Python</option>
                    <option value="ruby">Ruby</option>
                    <option value="php">PHP</option>
                    <option value="haskell">Haskell</option>
                </select>
                <input type="submit" class="codecha_button" name="codecha_change_challenge"
                       id="codecha_change_challenge" title="request new challenge" value="change lang"
                       onclick="codecha.chooseLanguage(); return false;" />
                <input type="submit" class="codecha_button" name="codecha_code_submit_button"
                       id="codecha_code_submit_button" value="VERIFY"
                       onclick="codecha.codeSubmit(); return false;" />
            </div>
        </div>
    </div>
</div>

Клиентский скрипт:

var codecha = {};

//Вспомогательные функции для лучшей читаемости

function id(_id) {
    return document.getElementById(_id);
}

function node(type, text) {
    if (typeof type != "string")
        return null;
    type = type.toUpperCase();
    return ("TEXT" == type) ? document.createTextNode(text ? text : "") : document.createElement(type);
};

function post(action, data) {
    return $.ajax(action, {
        type: "POST",
        data: data,
        dataType: "text"
    });
};

codecha.mustRequestNewChallenge = false;

codecha.serialize = function(obj) {
    var array = [];
    for (key in obj)
        array[array.length] = encodeURIComponent(key) + "=" + encodeURIComponent(obj[key]);
    var result = array.join("&");
    result = result.replace(/%20/g, "+");
    return result;
};

codecha.escape = function(str) {
    var div = node("div");
    div.appendChild(node("text", str));
    return div.innerHTML;
};

codecha.enable = function() {
    id("codecha_code_submit_button").disabled = false;
    id("codecha_change_challenge").disabled = false;
    id("codecha_code_area").disabled = false;
    id("codecha_spinner").hidden = true;
};

codecha.disable = function() {
    id("codecha_code_submit_button").disabled = true;
    id("codecha_change_challenge").disabled = true;
    id("codecha_code_area").disabled = true;
    id("codecha_spinner").hidden = false;
};

codecha.hideErrorOverlay = function() {
    id("codecha_error_overlay").hidden = true;
}

codecha.setStatus = function(state) {
    id("codecha_status").innerHTML = state;
};

codecha.updateState = function() {
    post("//codecha.org/api/state",
        codecha.serialize({ 'challenge': id("codecha_challenge_field").value })).then(function(response) {
        var match = /codecha\.response\s*\=\s"([^"]+)"/gi.exec(response);
        if (match) {
            codecha.mustRequestNewChallenge = true;
            id("codecha_response_field").value = match[1];
            id("codecha_widget").style.display = "none";
            id("codecha_ready_widget").style.display = "";
        } else {
            eval(response.replace(".callbacks", ""));
        }
    }).catch(function(err) {
        console.log(err);
    });
};

codecha.showErrorMessage = function(message) {
    id("codecha_error_message").innerHTML = message;
    id("codecha_error_overlay").hidden = false;
};

codecha.setChallenge = function(uuid, langName, wording, top, sampleCode, bottom) {
    id("codecha_challenge_field").value = uuid;
    id("codecha_wording").innerHTML = "<strong>" + langName + ":</strong> " + wording;
    id("codecha_code_area_top").innerHTML = "<pre>\n"+this.escape(top)+"</pre>";
    id("codecha_code_area").value = sampleCode;
    id("codecha_code_area_bottom").innerHTML = codecha.escape(bottom);
    id("codecha_code_area_top").hidden = (top.length <= 0);
    id("codecha_code_area_bottom").hidden = (bottom.length <= 0);
};

codecha.choseLanguage = function() {
    id("codecha_language_selector").hidden = false;
    id("codecha_language_selector").style.display = "";
    id("codecha_change_challenge").value = "\u2713";
    id("codecha_code_submit_button").disabled = true;
    id("codecha_change_challenge").onclick = codecha.requestNewChallenge;
    return false;
};

codecha.requestNewChallenge = function() {
    codecha.disable();
    codecha.setStatus("waiting");
    var select = id("codecha_language_selector");
    select.hidden = true;
    select.style.display = "none";
    id("codecha_change_challenge").value = "change lang";
    id("codecha_change_challenge").onclick = codecha.choseLanguage;
    id("codecha_response_field").value = "";
    var p;
    if (!codecha.mustRequestNewChallenge) {
        p = Promise.resolve();
    } else {
        p = Promise.resolve().then(function() {
            return $.getJSON("/api/codechaChallenge.json");
        }).then(function(model) {
            codecha.mustRequestNewChallenge = false;
            id("codecha_challenge_field").value = model;
            return Promise.resolve();
        });
    }
    p.then(function() {
        var params = {
            "challenge": id("codecha_challenge_field").value,
            "k": id("codecha_public_key").value,
            "lang": select.options[select.selectedIndex].value
        };
        return post("//codecha.org/api/change", codecha.serialize(params));
    }).then(function(response) {
        eval(response);
    }).catch(function(err) {
        console.log(err);
    });
    return false;
};

codecha.codeSubmit = function() {
    codecha.disable();
    var params = {
        "challenge": id("codecha_challenge_field").value,
        "code": id("codecha_code_area").value
    };
    post("//codecha.org/api/code", codecha.serialize(params)).then(function(response) {
        codecha.setStatus("sending");
        setTimeout(codecha.updateState, 1000);
    }).catch(function(err) {
        console.log(err);
    });
};

(function() {
    var link = node("link");
    link.setAttribute("rel", "stylesheet");
    link.setAttribute("type", "text/css");
    link.setAttribute("href", "//codecha.org/static/widget.css");
    document.querySelector("head").appendChild(link);

})();

window.addEventListener("load", function load() {
    window.removeEventListener("load", load, false);
    codecha.disable();
    codecha.requestNewChallenge();
}, false);

var reloadCaptcha = function() {
    codecha.requestNewChallenge();
    id("codecha_widget").style.display = "";
    id("codecha_ready_widget").style.display = "none";
};

Серверный код:

var requestChallenge = function(req) {
    var url = `http://codecha.org/api/challenge?k=PUBLIC_KEY`;
    return HTTP.request({
        url: url,
        timeout: (15 * 1000)
    }).then(function(response) {
        if (response.status != 200)
            return Promise.reject("Failed to get challenge");
        return response.body.read("utf8");
    }).then(function(data) {
        var match = /codecha.setChallenge\("([^"]+)"/gi.exec(data.toString());
        if (!match)
            return Promise.reject("Captcha server error");
        return Promise.resolve(match[1]);
    });
};

Теперь разберемся с тем, что тут происходит. Сначала мы обращаемся по тому самому адресу скрипта, который вставляется в форму, и при помощи регулярного выражения вычленяем идентификатор задания. Затем подставляем идентификатор в HTML-шаблон и, наконец, отдаем страницу пользователю.

Клиентский скрипт, в свою очередь, отправляет запрос на смену задания. Важно заметить, что при запросе нового задания требуется передать идентификатор предыдущего. Получаем новое задание, подставляем в виджет. Но самое интересное дальше.

После того как пользователь введет задание и сервер Codecha проверит его (тут все почти идентично исходному виджету), мы НЕ удаляем элементы интерфейса, а просто прячем их. После отправки AJAX-запросом сообщения на сервер мы можем вызвать функцию reloadCaptcha, которая запросит идентификатор задания с нашего сервера, а затем, используя его идентификатор, получит новое задание с сервера Codecha и заполнит виджет заново. Пользователь при этом чувствует себя сухо и комфортно, поскольку обновлять страницу не требуется.

В конце хотелось бы дать некоторые рекомендации:

  1. Никогда, НИКОГДА не используйте eval для интерпретации ответа от сервера. Это только добавит головной боли разработчикам, но никак не поможет защитить API от их вмешательства.
  2. Думайте о том, как будет использоваться ваш продукт. Рассматривайте все варианты, даже самые неправдоподобные (хотя описанный в заметке случай вполне себе обыденный). Старайтесь делать API пригодным для использования во всех ситуациях.
  3. Не завязывайте получение новой, никак не связанной с предыдущей, порции данных на информацию о предыдущей порции данных. Это, опять же, только добавит головной боли, но ни от чего не защитит.
  4. Если уж решили защитить API от вмешательства, то хотя бы используйте обфускацию. Без нее это бессмысленная трата своего и чужого времени.

Комментарии (0)