从REST API检索到的随机数无效,并且不同于wp_LOCALIZE_SCRIPT中生成的随机数

时间:2018-02-28 作者:Christian

For those that arrive from Google: You probably shouldn\'t get the nonces from the REST API, unless you really know what you\'re doing. Cookie-based authentication with the REST API is only meant for plugins and themes. For a single page application, you should probably use OAuth.

这个问题的存在是因为文档不清楚在构建单页应用程序时应该如何进行身份验证,JWT并不真正适合web应用程序,OAuth比基于cookie的身份验证更难实现。

<小时>The handbook 有一个关于主干JavaScript客户端如何处理nonce的示例,如果我遵循该示例,我会得到一个内置端点(如/wp/v2/posts)接受的nonce。

\\wp_localize_script("client-js", "theme", [
  \'nonce\' => wp_create_nonce(\'wp_rest\'),
  \'user\' => get_current_user_id(),

]);
然而,使用主干网是不可能的,主题也是不可能的,因此我编写了以下插件:

<?php
/*
Plugin Name: Nonce Endpoint
*/

add_action(\'rest_api_init\', function () {
  $user = get_current_user_id();
  register_rest_route(\'nonce/v1\', \'get\', [
    \'methods\' => \'GET\',
    \'callback\' => function () use ($user) {
      return [
        \'nonce\' => wp_create_nonce(\'wp_rest\'),
        \'user\' => $user,
      ];
    },
  ]);

  register_rest_route(\'nonce/v1\', \'verify\', [
    \'methods\' => \'GET\',
    \'callback\' => function () use ($user) {
      $nonce = !empty($_GET[\'nonce\']) ? $_GET[\'nonce\'] : false;
      return [
        \'valid\' => (bool) wp_verify_nonce($nonce, \'wp_rest\'),
        \'user\' => $user,
      ];
    },
  ]);
});
我对JavaScript控制台进行了一些修改,并编写了以下内容:

var main = async () => { // var because it can be redefined
  const nonceReq = await fetch(\'/wp-json/nonce/v1/get\', { credentials: \'include\' })
  const nonceResp = await nonceReq.json()
  const nonceValidReq = await fetch(`/wp-json/nonce/v1/verify?nonce=${nonceResp.nonce}`, { credentials: \'include\' })
  const nonceValidResp = await nonceValidReq.json()
  const addPost = (nonce) => fetch(\'/wp-json/wp/v2/posts\', {
    method: \'POST\',
    credentials: \'include\',
    body: JSON.stringify({
      title: `Test ${Date.now()}`,
      content: \'Test\',
    }),
    headers: {
      \'X-WP-Nonce\': nonce,
      \'content-type\': \'application/json\'
    },
  }).then(r => r.json()).then(console.log)

  console.log(nonceResp.nonce, nonceResp.user, nonceValidResp)
  console.log(theme.nonce, theme.user)
  addPost(nonceResp.nonce)
  addPost(theme.nonce)
}

main()
预期的结果是两个新职位,但我得到Cookie nonce is invalid 从第一个开始,第二个成功创建post。这可能是因为nonces不同,但为什么呢?我在两个请求中都以同一用户身份登录。

enter image description here

如果我的方法是错误的,我应该如何获得nonce?

Edit:

我试着在没有太多运气的情况下与环球足球队混战。利用wp\\u加载的操作更幸运:

<?php
/*
Plugin Name: Nonce Endpoint
*/

$nonce = \'invalid\';
add_action(\'wp_loaded\', function () {
  global $nonce;
  $nonce = wp_create_nonce(\'wp_rest\');
});

add_action(\'rest_api_init\', function () {
  $user = get_current_user_id();
  register_rest_route(\'nonce/v1\', \'get\', [
    \'methods\' => \'GET\',
    \'callback\' => function () use ($user) {
      return [
        \'nonce\' => $GLOBALS[\'nonce\'],
        \'user\' => $user,
      ];
    },
  ]);

  register_rest_route(\'nonce/v1\', \'verify\', [
    \'methods\' => \'GET\',
    \'callback\' => function () use ($user) {
      $nonce = !empty($_GET[\'nonce\']) ? $_GET[\'nonce\'] : false;
      error_log("verify $nonce $user");
      return [
        \'valid\' => (bool) wp_verify_nonce($nonce, \'wp_rest\'),
        \'user\' => $user,
      ];
    },
  ]);
});
现在,当我运行上面的JavaScript时,创建了两篇文章,但验证端点失败了!

enter image description here

我立即去调试wp\\u verify\\u:

function wp_verify_nonce( $nonce, $action = -1 ) {
  $nonce = (string) $nonce;
  $user = wp_get_current_user();
  $uid = (int) $user->ID; // This is 0, even though the verify endpoint says I\'m logged in as user 2!
我添加了一些日志记录

// Nonce generated 0-12 hours ago
$expected = substr( wp_hash( $i . \'|\' . $action . \'|\' . $uid . \'|\' . $token, \'nonce\'), -12, 10 );
error_log("expected 1 $expected received $nonce uid $uid action $action");
if ( hash_equals( $expected, $nonce ) ) {
  return 1;
}

// Nonce generated 12-24 hours ago
$expected = substr( wp_hash( ( $i - 1 ) . \'|\' . $action . \'|\' . $uid . \'|\' . $token, \'nonce\' ), -12, 10 );
error_log("expected 2 $expected received $nonce uid $uid action $action");
if ( hash_equals( $expected, $nonce ) ) {
  return 2;
}
JavaScript代码现在会生成以下条目。如您所见,调用verify端点时,uid为0。

[01-Mar-2018 11:41:57 UTC] verify 716087f772 2
[01-Mar-2018 11:41:57 UTC] expected 1 b35fa18521 received 716087f772 uid 0 action wp_rest
[01-Mar-2018 11:41:57 UTC] expected 2 dd35d95cbd received 716087f772 uid 0 action wp_rest
[01-Mar-2018 11:41:58 UTC] expected 1 716087f772 received 716087f772 uid 2 action wp_rest
[01-Mar-2018 11:41:58 UTC] expected 1 716087f772 received 716087f772 uid 2 action wp_rest

3 个回复
最合适的回答,由SO网友:Otto 整理而成

仔细查看function rest_cookie_check_errors().

当你通过/wp-json/nonce/v1/get, 你一开始并没有发送一个nonce。因此,此函数使用以下代码使身份验证无效:

if ( null === $nonce ) {
    // No nonce at all, so act as if it\'s an unauthenticated request.
    wp_set_current_user( 0 );
    return true;
}
这就是为什么您从REST调用中获得的临时值与从主题中获得的临时值不同。REST调用故意不识别您的登录凭据(在本例中是通过cookie验证),因为您没有在get请求中发送有效的nonce。

现在,wp\\u加载的代码之所以能够工作,是因为在rest代码使您的登录无效之前,您获得了nonce并将其保存到全局。验证失败,因为rest代码在进行验证之前使您的登录无效。

SO网友:Christian

虽然此解决方案可行,it isn\'t recommended. OAuth是首选。

我想我明白了。

我认为wp\\u verify\\u nonce被破坏了,因为wp\\u get\\u current\\u user无法获得正确的用户对象

正如奥托所说,事实并非如此。

幸运的是,它有一个过滤器:$uid = apply_filters( \'nonce_user_logged_out\', $uid, $action );

使用此过滤器,我能够编写以下内容,JavaScript代码按其应该的方式执行:

enter image description here

<?php
/*
Plugin Name: Nonce Endpoint
*/

$nonce = \'invalid\';
add_action(\'wp_loaded\', function () {
  global $nonce;
  $nonce = wp_create_nonce(\'wp_rest\');
});

add_action(\'rest_api_init\', function () {
  $user = get_current_user_id();
  register_rest_route(\'nonce/v1\', \'get\', [
    \'methods\' => \'GET\',
    \'callback\' => function () use ($user) {
      return [
        \'nonce\' => $GLOBALS[\'nonce\'],
        \'user\' => $user,
      ];
    },
  ]);

  register_rest_route(\'nonce/v1\', \'verify\', [
    \'methods\' => \'GET\',
    \'callback\' => function () use ($user) {
      $nonce = !empty($_GET[\'nonce\']) ? $_GET[\'nonce\'] : false;
      add_filter("nonce_user_logged_out", function ($uid, $action) use ($user) {
        if ($uid === 0 && $action === \'wp_rest\') {
          return $user;
        }

        return $uid;
      }, 10, 2);

      return [
        \'status\' => wp_verify_nonce($nonce, \'wp_rest\'),
        \'user\' => $user,
      ];
    },
  ]);
});
如果你发现这个补丁有安全问题,请告诉我,现在我看不出它有什么问题,除了globals。

SO网友:Mark Kaplun

看看所有这些代码,您的问题似乎是闭包的使用。在init 阶段您应该只设置挂钩,而不评估数据,因为并非所有的核心都已完成加载和初始化。

在里面

add_action(\'rest_api_init\', function () {
  $user = get_current_user_id();
  register_rest_route(\'nonce/v1\', \'get\', [
    \'methods\' => \'GET\',
    \'callback\' => function () use ($user) {
      return [
        \'nonce\' => $GLOBALS[\'nonce\'],
        \'user\' => $user,
      ];
    },
  ]);
the$user 很早就注定要在闭包中使用,但没有人向您保证已经处理了cookie,并且基于cookie对用户进行了身份验证。更好的代码是

add_action(\'rest_api_init\', function () {
  register_rest_route(\'nonce/v1\', \'get\', [
    \'methods\' => \'GET\',
    \'callback\' => function () {
    $user = get_current_user_id();
      return [
        \'nonce\' => $GLOBALS[\'nonce\'],
        \'user\' => $user,
      ];
    },
  ]);
与wordpress中的任何钩子一样,尽可能使用最新的钩子,不要试图预先计算任何不需要的东西。

结束