Как получить уникальный одноразовый код (nonce) для каждого Ajax-запроса?

29 янв. 2013 г., 04:59:40
Просмотры: 17.6K
Голосов: 12

Я видел несколько обсуждений о том, как заставить WordPress генерировать уникальный одноразовый код (nonce) для последующих Ajax-запросов, но никак не могу добиться этого — каждый раз, когда я запрашиваю, казалось бы, новый nonce, WordPress возвращает мне тот же самый код. Я понимаю концепцию nonce_life в WordPress и даже пробовал изменять это значение, но это не помогло.

Я не генерирую nonce в JS-объекте в заголовке через локализацию — я делаю это на своей странице отображения. Мне удалось заставить страницу обрабатывать Ajax-запрос, но когда я запрашиваю новый nonce из WordPress в callback-функции, я получаю тот же самый код, и я не понимаю, что делаю не так... В конечном итоге я хочу расширить эту функциональность, чтобы на странице могло быть несколько элементов, каждый с возможностью добавления/удаления — поэтому мне нужно решение, которое позволит выполнять несколько последовательных Ajax-запросов с одной страницы.

(И я должен сказать, что всю эту функциональность я поместил в плагин, поэтому frontend "страница отображения" на самом деле является функцией, включенной в плагин...)

functions.php: локализация, но я не создаю nonce здесь

wp_localize_script('myjs', 'ajaxVars', array('ajaxurl' => 'admin-ajax.php')));

Вызывающий JS:

$("#myelement").click(function(e) {
    e.preventDefault();
    post_id = $(this).data("data-post-id");
    user_id = $(this).data("data-user-id");
    nonce = $(this).data("data-nonce");
    $.ajax({
      type: "POST",
      dataType: "json",
      url: ajaxVars.ajaxurl,
      data: {
         action: "myfaves",
         post_id: post_id,
         user_id: user_id,
         nonce: nonce
      },
      success: function(response) {
         if(response.type == "success") {
            nonce = response.newNonce;
            ... other stuff
         }
      }
  });
});

Принимающий PHP:

function myFaves() {
   $ajaxNonce = 'myplugin_myaction_nonce_' . $postID;
   if (!wp_verify_nonce($_POST['nonce'], $ajaxNonce))
      exit('Извините!');

   // Получаем различные POST-переменные и делаем другие действия...

   // Подготавливаем JSON-ответ и генерируем новый уникальный nonce
   $newNonce = wp_create_nonce('myplugin_myaction_nonce_' . $postID . '_' 
       . str_replace('.', '', gettimeofday(true)));
   $response['newNonce'] = $newNonce;

   // Также позволяем странице обработать себя, если нет JS/Ajax-возможностей
   } else {
      header("Location: " . $_SERVER["HTTP_REFERER"];
   }
   die();
}

Frontend PHP функция отображения, среди которой есть:

$nonce = wp_create_nonce('myplugin_myaction_nonce_' . $post->ID);
$link = admin_url('admin-ajax.php?action=myfaves&post_id=' . $post->ID
   . '&user_id=' . $user_ID
   . '&nonce=' . $nonce);

echo '<a id="myelement" data-post-id="' . $post->ID
   . '" data-user-id="' . $user_ID
   . '" data-nonce="' . $nonce
   . '" href="' . $link . '">Моя ссылка</a>';

На данном этапе я был бы очень благодарен за любые подсказки или указания по получению уникального nonce для каждого нового Ajax-запроса в WordPress...


ОБНОВЛЕНИЕ: Я решил свою проблему. Приведенные выше фрагменты кода верны, однако я изменил создание $newNonce в PHP callback-функции, добавив строку с микросекундами, чтобы гарантировать уникальность для последующих Ajax-запросов.

5
Комментарии

Если очень кратко: вы создаёте nonce после его получения (при отображении)? Почему бы не создать его во время вызова localize?

kaiser kaiser
29 янв. 2013 г. 07:36:36

jQuery использует начальный nonce из атрибута "data-nonce" в ссылке a#myelement, и идея в том, что страница может обрабатываться как через Ajax, так и самостоятельно. Мне казалось, что создание nonce один раз через вызов localize исключит его из обработки без JS, но возможно я ошибаюсь. В любом случае Wordpress возвращает мне тот же самый nonce...

Tim Tim
29 янв. 2013 г. 17:17:11

И ещё: Размещение nonce в вызове localize не помешает ли иметь несколько элементов на странице, где каждый элемент может иметь уникальный nonce для Ajax-запроса?

Tim Tim
29 янв. 2013 г. 18:11:09

Создание nonce внутри локализации сделает его доступным только для этого одного скрипта. Но вы также можете добавить неограниченное количество других значений локализации (с именованными ключами) с отдельными nonce.

kaiser kaiser
29 янв. 2013 г. 21:32:50

Если вы решили проблему, рекомендуется опубликовать свой ответ и пометить его как "принятый". Это поможет поддерживать порядок на сайте. Я просто экспериментировал с вашим кодом, и некоторые вещи у меня не работают, поэтому особенно прошу вас опубликовать ваше решение.

s_ha_dum s_ha_dum
1 февр. 2013 г. 17:55:30
Все ответы на вопрос 2
0
Вот перевод на русский язык:

Это очень подробный ответ на мой собственный вопрос, который выходит за рамки простого решения проблемы генерации уникальных одноразовых номеров (nonces) для последующих Ajax-запросов. Речь идет о функции "добавить в избранное", которая была сделана универсальной для целей ответа (моя функция позволяет пользователям добавлять ID вложений фотографий в список избранного, но это можно применить и к другим функциям, использующим Ajax). Я реализовал это как отдельный плагин, и в нем отсутствуют некоторые детали — но здесь достаточно информации, чтобы понять суть, если вы хотите повторить эту функциональность. Она будет работать как на отдельных записях/страницах, так и в списках записей (например, вы можете добавлять/удалять элементы из избранного прямо на месте через Ajax, и каждая запись будет иметь свой уникальный одноразовый номер для каждого Ajax-запроса). Учтите, что, вероятно, существует более эффективный и/или элегантный способ сделать это, и пока что это работает только для Ajax — я еще не реализовал обработку данных $_POST без использования Ajax.

scripts.php

/**
* Подключение jQuery для фронтенда
*/
function enqueueFavoritesJS()
{
    // Показывать Ajax JS для избранного только для авторизованных пользователей
    if (is_user_logged_in()) {
        wp_enqueue_script('favorites-js', MYPLUGIN_BASE_URL . 'js/favorites.js', array('jquery'));
        wp_localize_script('favorites-js', 'ajaxVars', array('ajaxurl' => admin_url('admin-ajax.php')));
    }
}
add_action('wp_enqueue_scripts', 'enqueueFavoritesJS');

favorites.js (Много отладочного кода, который можно удалить)

$(document).ready(function()
{
    // Переключение элемента в избранном
    $(".faves-link").click(function(e) {
        // Предотвращаем стандартное поведение и используем Ajax
        e.preventDefault();
        var $this = $(this);
        console.log("Начало события клика...");

        // Получаем переменные со страницы
        post_id = $this.attr("data-post-id");
        user_id = $this.attr("data-user-id");
        the_toggle = $this.attr("data-toggle");
        ajax_nonce = $this.attr("data-nonce");

        console.log("data-post-id: " + post_id);
        console.log("data-user-id: " + user_id);
        console.log("data-toggle: " + the_toggle);
        console.log("data-nonce: " + ajax_nonce);
        console.log("Начало Ajax...");

        $.ajax({
            type: "POST",
            dataType: "json",
            url: ajaxVars.ajaxurl,
            data: {
                // Отправляем JSON обратно в PHP для обработки
                action : "myFavorites",
                post_id: post_id,
                user_id: user_id,
                _ajax_nonce: ajax_nonce,
                the_toggle: the_toggle
            },
            beforeSend: function() {
                if (the_toggle == "y") {
                    $this.text("Удаление из избранного...");
                    console.log("Удаление...");
                } else {
                    $this.text("Добавление в избранное...");
                    console.log("Добавление...");
                }
            },
            success: function(response) {
                // Обрабатываем JSON, полученный от PHP
                if(response.type == "success") {
                    console.log("Успех!");
                    console.log("Новый nonce: " + response.newNonce);
                    console.log("Новое состояние: " + response.theToggle);
                    console.log("Сообщение от PHP: " + response.message);
                    $this.text(response.message);
                    $this.attr("data-toggle", response.theToggle);
                    // Устанавливаем новый nonce
                    _ajax_nonce = response.newNonce;
                    console.log("_ajax_nonce теперь: " + _ajax_nonce);
                } else {
                    console.log("Ошибка!");
                    console.log("Новый nonce: " + response.newNonce);
                    console.log("Сообщение от PHP: " + response.message);
                    $this.parent().html("<p>" + response.message + "</p>");
                    _ajax_nonce = response.newNonce;
                    console.log("_ajax_nonce теперь: " + _ajax_nonce);
                }
            },
            error: function(e, x, settings, exception) {
                // Общая отладка
                var errorMessage;
                var statusErrorMap = {
                    '400' : "Сервер понял запрос, но его содержимое было неверным.",
                    '401' : "Несанкционированный доступ.",
                    '403' : "Запрещенный ресурс.",
                    '500' : "Внутренняя ошибка сервера",
                    '503' : "Сервис недоступен"
                };
                if (x.status) {
                    errorMessage = statusErrorMap[x.status];
                    if (!errorMessage) {
                        errorMessage = "Неизвестная ошибка.";
                    } else if (exception == 'parsererror') {
                        errorMessage = "Ошибка. Не удалось разобрать JSON-запрос.";
                    } else if (exception == 'timeout') {
                        errorMessage = "Превышено время ожидания запроса.";
                    } else if (exception == 'abort') {
                        errorMessage = "Запрос был прерван сервером.";
                    } else {
                        errorMessage = "Неизвестная ошибка.";
                    }
                    $this.parent().html(errorMessage);
                    console.log("Сообщение об ошибке: " + errorMessage);
                } else {
                    console.log("ОШИБКА!!");
                    console.log(e);
                }
            }
        }); // Закрытие $.ajax
    }); // Конец события клика
});

Функции (отображение на фронтенде и Ajax-действие)

Для вывода ссылки "Добавить/Удалить из избранного" просто вызовите ее на своей странице/записи:

if (function_exists('myFavoritesLink') {
    myFavoritesLink($user_ID, $post->ID);
}

Функция отображения на фронтенде:

function myFavoritesLink($user_ID, $postID)
{
    global $user_ID;
    if (is_user_logged_in()) {
        // Устанавливаем начальное состояние элемента и текст ссылки - обновляется в ответе
        $myUserMeta = get_user_meta($user_ID, 'myMetadata', true);
        if (is_array($myUserMeta['metadata']) && in_array($postID, $myUserMeta['metadata'])) {
            $toggle = "y";
            $linkText = "Удалить из избранного";
        } else {
            $toggle = "n";
            $linkText = "Добавить в избранное";
        }

        // Создаем одноразовый номер для Ajax-запроса (только для начального запроса)
        // Новый nonce возвращается в ответе
        $ajaxNonce = wp_create_nonce('myplugin_myaction_' . $postID);
        echo '<p class="faves-action"><a class="faves-link"' 
            . ' data-post-id="' . $postID 
            . '" data-user-id="' . $user_ID  
            . '" data-toggle="' . $toggle 
            . '" data-nonce="' . $ajaxNonce 
            . '" href="#">' . $linkText . '</a></p>' . "\n";

    } else {
        // Пользователь не авторизован
        echo '<p>Войдите, чтобы использовать функцию избранного.</p>' . "\n";
    }

}

Функция Ajax-действия:

/**
* Переключение добавления/удаления из избранного
*/
function toggleFavorites()
{
    if (is_user_logged_in()) {
        // Проверяем nonce
        $ajaxNonce = 'myplugin_myaction' . $_POST['post_id'];
        if (! wp_verify_nonce($_POST['_ajax_nonce'], $ajaxNonce)) {
            exit('Извините!');
        }
        // Обрабатываем переменные POST
        if (isset($_POST['post_id']) && is_numeric($_POST['post_id'])) {
            $postID = $_POST['post_id'];
        } else {
            return;
        }
        if (isset($_POST['user_id']) && is_numeric($_POST['user_id'])) {
            $userID = $_POST['user_id'];
        } else {
            return;
        }
        if (isset($_POST['the_toggle']) && ($_POST['the_toggle'] === "y" || $_POST['the_toggle'] === "n")) {
            $toggle = $_POST['the_toggle'];
        } else {
            return;
        }

        $myUserMeta = get_user_meta($userID, 'myMetadata', true);

        // Инициализируем массив myUserMeta, если он не существует
        if ($myUserMeta['myMetadata'] === '' || ! is_array($myUserMeta['myMetadata'])) {
            $myUserMeta['myMetadata'] = array();
        }

        // Переключаем элемент в списке избранного
        if ($toggle === "y" && in_array($postID, $myUserMeta['myMetadata'])) {
            // Удаляем элемент из списка избранного
            $myUserMeta['myMetadata'] = array_flip($myUserMeta['myMetadata']);
            unset($myUserMeta['myMetadata'][$postID]);
            $myUserMeta['myMetadata'] = array_flip($myUserMeta['myMetadata']);
            $myUserMeta['myMetadata'] = array_values($myUserMeta['myMetadata']);
            $newToggle = "n";
            $message = "Добавить в избранное";
        } else {
            // Добавляем элемент в список избранного
            $myUserMeta['myMetadata'][] = $postID;
            $newToggle = "y";
            $message = "Удалить из избранного";
        }

        // Подготовка ответа
        // Новый nonce для следующего запроса - уникальный с добавлением микросекунд
        $newNonce = wp_create_nonce('myplugin_myaction_' . $postID . '_' 
            . str_replace('.', '', gettimeofday(true)));
        $updateUserMeta = update_user_meta($userID, 'myMetadata', $myUserMeta);

        // Ответ для jQuery
        if($updateUserMeta === false) {
            $response['type'] = "error";
            $response['theToggle'] = $toggle;
            $response['message'] = "Не удалось обновить список избранного.";
            $response['newNonce'] = $newNonce;
        } else {
            $response['type'] = "success";
            $response['theToggle'] = $newToggle;
            $response['message'] = $message;
            $response['newNonce'] = $newNonce;
        }

        // Обработка через Ajax, иначе стандартная обработка
        if (! empty($_SERVER['HTTP_X_REQUESTED_WITH']) && 
            strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
                $response = json_encode($response);
                echo $response;
        } else {
            header("Location: " . $_SERVER["HTTP_REFERER"]);
        }
        exit();
    } // Конец is_user_logged_in()
}
add_action('wp_ajax_myFavorites', 'toggleFavorites');
1 февр. 2013 г. 19:47:34
1

Мне действительно приходится задуматься о логике получения нового одноразового кода (nonce) для каждого AJAX-запроса. Исходный nonce действительно истекает, но его можно использовать несколько раз до этого момента. Получение nonce через AJAX в JavaScript сводит на нет его предназначение, особенно если он предоставляется в случае ошибки. (Основная цель nonce — обеспечение базовой безопасности, связывая действие с пользователем в рамках определенного временного промежутка.)

Мне не следует упоминать другие ответы, но я новичок и не могу комментировать выше, поэтому относительно предложенного «решения»: вы получаете новый nonce каждый раз, но не используете его в запросе. Было бы крайне сложно добиться совпадения микросекунд каждый раз, чтобы новый nonce соответствовал созданному таким образом. PHP-код проверяет исходный nonce, а JavaScript передает именно его... поэтому это работает (потому что срок его действия еще не истек).

28 нояб. 2013 г. 06:15:41
Комментарии

Проблема в том, что nonce истекает после использования и будет возвращать -1 в ajax-функции после каждого раза. Это становится проблемой, если вы проверяете части формы в PHP и возвращаете ошибки для вывода. Nonce формы был использован, но ошибка фактически произошла при валидации полей в PHP, и когда форма отправляется снова, на этот раз она не может быть проверена, и check_ajax_referer возвращает -1, что не то, что нам нужно!

Solomon Closson Solomon Closson
26 окт. 2016 г. 22:22:04