深入理解 WordPress `wp_verify_nonce()` 函数的源码:如何通过时间戳和用户 ID 验证 `Nonce` 的有效性。

WordPress Nonce 机制深度解析:wp_verify_nonce() 函数源码大冒险

各位观众老爷们,大家好!欢迎来到今天的 WordPress 技术讲座。我是你们的老朋友,人称“代码界的段子手”——老码农。今天咱们要聊的,是 WordPress 安全机制中一个相当重要,但又常常被新手忽略的家伙:Nonce。更具体地说,我们要扒开 wp_verify_nonce() 函数的源码,看看它是如何像个老侦探一样,通过时间戳和用户 ID,来验证 Nonce 的有效性,揪出那些想浑水摸鱼的坏蛋。

准备好了吗?系好安全带,我们这就开始这场源码大冒险!

什么是 Nonce?为什么要用它?

在开始深入源码之前,我们先来回顾一下 Nonce 的基本概念。Nonce,是 "Number used ONCE" 的缩写,顾名思义,就是一个只能用一次的随机数。在 WordPress 中,Nonce 主要用于防止 CSRF (Cross-Site Request Forgery,跨站请求伪造) 攻击。

想象一下,如果没有 Nonce,一个坏蛋可以在你的网站上伪造一个请求,比如删除你的文章,然后通过各种手段诱骗你点击这个链接。一旦你点击了,你的 WordPress 就会执行这个恶意请求,因为你已经登录了,浏览器会自动带上你的身份信息。

Nonce 的作用,就像给每个敏感操作加上一个只有你知道的密码。坏蛋不知道这个密码,就无法伪造有效的请求。

wp_verify_nonce():Nonce 验证的核心

wp_verify_nonce() 函数是 WordPress 中验证 Nonce 是否有效的关键函数。它接受两个参数:

  1. $nonce: 要验证的 Nonce 值。
  2. $action: 生成 Nonce 时使用的 Action 值,用于区分不同的操作。

这个函数会返回以下值:

  • 1: Nonce 有效,且在有效期内。
  • 2: Nonce 有效,但已经过期(在有效期的一半之前)。
  • false: Nonce 无效。

现在,让我们打开 WordPress 的源码,找到 wp-includes/pluggable.php 文件,开始深入 wp_verify_nonce() 函数的内部。

wp_verify_nonce() 源码剖析

function wp_verify_nonce( $nonce, $action = -1 ) {
    $nonce = (string) $nonce;
    $action = (string) $action;

    $nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS );

    $i = wp_nonce_tick();

    // Nonce generated 0-12 hours ago
    $expected = substr( wp_hash( $i . $action . get_current_user_id(), 'nonce' ), -12, 10 );
    if ( hash_equals( $expected, $nonce ) ) {
        return 1;
    }

    // Nonce generated 12-24 hours ago
    $expected = substr( wp_hash( ( $i - 1 ) . $action . get_current_user_id(), 'nonce' ), -12, 10 );
    if ( hash_equals( $expected, $nonce ) ) {
        return 2;
    }

    /**
     * Fires when the nonce verification fails.
     *
     * @since 4.5.0
     *
     * @param string|int $nonce  The nonce that was rejected.
     * @param string|int $action The action hook the nonce was used for.
     */
    do_action( 'wp_nonce_failed', $nonce, $action );

    return false;
}

代码看起来不多,但信息量很大。让我们一行一行地解读:

  1. 参数类型转换:

    $nonce = (string) $nonce;
    $action = (string) $action;

    首先,将传入的 $nonce$action 强制转换为字符串类型,确保后续操作的类型一致性。这是一种良好的编程习惯,可以避免一些潜在的类型错误。

  2. 获取 Nonce 的有效期:

    $nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS );

    这里通过 apply_filters() 函数,允许开发者通过 nonce_life 过滤器来修改 Nonce 的有效期。默认情况下,Nonce 的有效期是 DAY_IN_SECONDS,也就是一天 (24 小时)。这给了开发者很大的灵活性,可以根据实际需求调整 Nonce 的有效期。

  3. 获取时间戳 "Tick":

    $i = wp_nonce_tick();

    wp_nonce_tick() 函数返回一个基于时间戳的整数,用于计算 Nonce 的哈希值。这个 "Tick" 的作用是,将 Nonce 的有效期分成多个时间段,使得 Nonce 在每个时间段内都是唯一的。

    我们稍后会详细分析 wp_nonce_tick() 函数。

  4. 计算预期的 Nonce 值 (当前时间段):

    $expected = substr( wp_hash( $i . $action . get_current_user_id(), 'nonce' ), -12, 10 );

    这行代码是关键。它做了以下几件事:

    • 将当前时间段的 "Tick" ($i)、Action ($action) 和当前用户 ID (get_current_user_id()) 拼接成一个字符串。
    • 使用 wp_hash() 函数对这个字符串进行哈希运算,生成一个哈希值。 wp_hash() 默认使用 md5 算法,但可以通过 wp_hash_algorithm filter 修改。
    • 截取哈希值的最后 10 位字符,作为预期的 Nonce 值。之所以截取 10 位,是为了缩短 Nonce 的长度,提高安全性,同时避免过长的 Nonce 影响性能。
    • 使用 hash_equals() 函数进行安全比较,防止时序攻击。
  5. 比较 Nonce 值:

    if ( hash_equals( $expected, $nonce ) ) {
        return 1;
    }

    使用 hash_equals() 函数,安全地比较计算出的预期 Nonce 值和传入的 $nonce 值。如果两者相等,说明 Nonce 有效,返回 1

  6. 计算预期的 Nonce 值 (上一个时间段):

    $expected = substr( wp_hash( ( $i - 1 ) . $action . get_current_user_id(), 'nonce' ), -12, 10 );
    if ( hash_equals( $expected, $nonce ) ) {
        return 2;
    }

    如果当前的 Nonce 值无效,则尝试使用上一个时间段的 "Tick" 值 ($i - 1) 重新计算预期的 Nonce 值,并进行比较。如果两者相等,说明 Nonce 虽然有效,但已经过期 (在有效期的一半之前),返回 2

    这样做是为了允许 Nonce 在一定时间内仍然有效,避免由于服务器时钟不同步或用户操作延迟导致 Nonce 验证失败。

  7. Nonce 验证失败钩子:

    do_action( 'wp_nonce_failed', $nonce, $action );

    如果 Nonce 验证失败,则触发 wp_nonce_failed 钩子,允许开发者执行一些自定义的操作,例如记录日志或显示错误信息。

  8. 返回 False:

    return false;

    如果 Nonce 验证失败,且不在过期时间内,则返回 false,表示 Nonce 无效。

wp_nonce_tick() 函数:时间戳的秘密

现在,让我们深入 wp_nonce_tick() 函数,看看它是如何计算时间戳 "Tick" 的。

function wp_nonce_tick() {
    /**
     * Filters the lifespan of the nonce.
     *
     * @since 4.5.0
     *
     * @param int $lifespan The amount of time to use as the lifespan of a nonce.
     *                        Default is DAY_IN_SECONDS.
     */
    $nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS );

    return ceil( time() / ( $nonce_life / 2 ) );
}

这段代码也很简单:

  1. 获取 Nonce 的有效期:

    $nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS );

    同样,通过 apply_filters() 函数获取 Nonce 的有效期。

  2. 计算时间戳 "Tick":

    return ceil( time() / ( $nonce_life / 2 ) );

    这行代码的核心在于计算时间戳 "Tick" 的逻辑。它做了以下几件事:

    • 将当前时间戳 (time()) 除以 Nonce 有效期的一半 ($nonce_life / 2)。
    • 使用 ceil() 函数向上取整,得到一个整数值,作为时间戳 "Tick"。

    这样做的目的是,将 Nonce 的有效期分成两个时间段。例如,如果 Nonce 的有效期是 24 小时,那么每个时间段就是 12 小时。在每个时间段内,wp_nonce_tick() 函数返回的值都是相同的,从而保证 Nonce 在每个时间段内都是唯一的。

为什么 Nonce 要基于用户 ID?

你可能注意到,在计算 Nonce 的哈希值时,wp_verify_nonce() 函数使用了 get_current_user_id() 函数来获取当前用户 ID。为什么要这样做呢?

原因很简单:为了防止用户之间的 Nonce 冲突。

如果没有用户 ID,那么所有用户的 Nonce 值都会相同,这会导致一个用户生成的 Nonce 可以被其他用户使用,从而破坏了 Nonce 的唯一性。

通过将用户 ID 包含在 Nonce 的哈希值中,可以保证每个用户的 Nonce 值都是唯一的,从而提高安全性。

总结:wp_verify_nonce() 的工作原理

现在,让我们总结一下 wp_verify_nonce() 函数的工作原理:

  1. 获取 Nonce 的有效期和时间戳 "Tick"。
  2. 将时间戳 "Tick"、Action 和用户 ID 拼接成一个字符串。
  3. 对这个字符串进行哈希运算,并截取哈希值的最后 10 位字符,作为预期的 Nonce 值。
  4. 比较预期的 Nonce 值和传入的 Nonce 值。
  5. 如果两者相等,则返回 1,表示 Nonce 有效。
  6. 如果 Nonce 值不在当前时间段,则尝试使用上一个时间段的时间戳 "Tick" 重新计算预期的 Nonce 值,并进行比较。
  7. 如果两者相等,则返回 2,表示 Nonce 有效,但已经过期。
  8. 如果 Nonce 验证失败,则触发 wp_nonce_failed 钩子,并返回 false,表示 Nonce 无效。

用一张表格来总结一下:

步骤 描述
1 获取 Nonce 有效期 (nonce_life),默认 24 小时 (DAY_IN_SECONDS)。
2 获取时间戳 "Tick" ($i),通过 wp_nonce_tick() 计算,将有效期分成两个时间段。
3 拼接字符串:$i . $action . get_current_user_id()
4 使用 wp_hash() 对字符串进行哈希运算。
5 截取哈希值的最后 10 位字符,作为预期 Nonce 值。
6 使用 hash_equals() 安全地比较预期 Nonce 值和传入的 Nonce 值。
7 如果相等,返回 1(有效)。
8 尝试使用上一个时间段的 Tick ($i - 1) 重新计算,如果相等,返回 2(过期)。
9 如果验证失败,触发 wp_nonce_failed 钩子,返回 false(无效)。

如何正确使用 wp_verify_nonce()

现在我们已经深入了解了 wp_verify_nonce() 函数的源码,那么如何正确地使用它呢?

以下是一些建议:

  1. 在生成 Nonce 时,使用唯一的 Action 值。 Action 值用于区分不同的操作,可以防止 Nonce 被用于错误的操作。
  2. 在验证 Nonce 时,使用与生成 Nonce 时相同的 Action 值。 否则,Nonce 验证将会失败。
  3. 不要将 Nonce 暴露给客户端。 Nonce 应该保存在服务器端,例如 Session 或数据库中。
  4. 定期更新 Nonce。 Nonce 的有效期是有限的,过期后就不能再使用。
  5. 使用 wp_nonce_field() 函数生成隐藏的 Nonce 字段。 这个函数会自动生成包含 Nonce 值的隐藏表单字段,方便在表单提交时传递 Nonce 值。

总结

通过这次源码大冒险,我们深入了解了 WordPress wp_verify_nonce() 函数的工作原理,以及 Nonce 机制在 WordPress 安全中的重要作用。希望这次讲座能够帮助你更好地理解 WordPress 安全机制,写出更安全、更可靠的 WordPress 代码。

记住,代码世界充满了乐趣,只要你敢于探索,就能发现更多的秘密!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注