阐述 `wp_nonce` 机制的源码,解释 `wp_create_nonce()` 如何通过 `user_id`, `action` 和 `timestamp` 生成一个随机字符串。

各位观众老爷,大家好!今天咱们来聊聊 WordPress 安全机制里一个非常重要的东西:wp_nonce。 这玩意儿听起来高大上,其实说白了就是 WordPress 为了防止 CSRF (Cross-Site Request Forgery,跨站请求伪造) 攻击而设计的一套验证机制。 咱们今天的目标是:扒开 wp_nonce 的源码,彻底搞清楚它到底是怎么工作的,尤其是 wp_create_nonce() 这个函数,它如何像变魔术一样,用 user_idactiontimestamp 变出一个随机字符串来。准备好了吗? Let’s dive in!

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

首先,咱们先明确一下 nonce 是个什么东西。 Nonce,全称是 "number used once",顾名思义,就是只能用一次的数字。在安全领域,它通常被用来防止重放攻击 (Replay Attack)。

在 WordPress 中,wp_nonce 实际上是一个加密过的字符串,它包含了用户 ID、一个特定的动作 (action) 和一个时间戳。 它的主要作用就是验证请求的合法性,确保请求真的是用户在知情的情况下发起的,而不是被恶意网站伪造的。

如果没有 wp_nonce,攻击者就可以伪造一个请求,比如修改你的文章、删除你的评论,甚至更改你的网站设置。 有了 wp_nonce,WordPress 就能识别出这些伪造的请求,并拒绝执行。

wp_create_nonce(): Nonce 的制造工厂

咱们今天的主角就是 wp_create_nonce() 函数。这个函数负责创建 Nonce,是整个 Nonce 机制的核心。 让我们先看看它的源码(为了便于理解,我对源码进行了一定的简化,并添加了注释):

function wp_create_nonce( $action = -1 ) {
    $user = wp_get_current_user();
    $uid = (int) $user->ID;
    if ( ! $uid ) {
        $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
    }

    $token = wp_get_session_token();
    $i = wp_nonce_tick();

    return substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
}

哇,代码量不多啊。咱们一行一行地分析:

  1. $action = -1: 这个参数 $action 是用来区分 Nonce 的用途的。 你可以把它想象成 Nonce 的 "名字"。 比如,如果你要创建一个用于删除文章的 Nonce,你可以把 $action 设置为 'delete_post'。 如果不传参数,默认是 -1。

  2. $user = wp_get_current_user();$uid = (int) $user->ID;: 获取当前用户的 ID。 如果用户未登录,$uid 就是 0。

  3. if ( ! $uid ) { $uid = apply_filters( 'nonce_user_logged_out', $uid, $action ); }: 如果用户未登录,则应用 nonce_user_logged_out 过滤器。允许插件修改未登录用户的UID,默认保持为0。

  4. $token = wp_get_session_token();: 获取用户的会话 Token。这个Token是用来增强Nonce的唯一性和安全性。

  5. $i = wp_nonce_tick();: 这个函数返回一个时间戳,但是它不是直接返回 time(),而是经过了一些处理,咱们稍后会详细讲解。

  6. return substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );: 这行代码是核心。 它把 $i (时间戳), $action (动作), $uid (用户 ID) 和 $token (会话Token) 用管道符 | 连接起来,然后用 wp_hash() 函数进行哈希,最后截取哈希值的一部分作为 Nonce。 注意到这里使用了 ‘nonce’ 作为 $algo 参数,wp_hash 会根据这个参数选择合适的哈希算法(通常是 hash_hmac( 'md5', $data, $key ))。

wp_nonce_tick(): 时间的魔法师

现在,咱们来详细看看 wp_nonce_tick() 函数。 它不是简单地返回 time(),而是对时间进行了一些处理。 代码如下:

function wp_nonce_tick() {
    /**
     * Filters the lifespan of nonces.
     *
     * @since 4.5.0
     *
     * @param int $lifespan Lifespan in seconds.
     */
    $nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS );

    return ceil( time() / ( $nonce_life / 2 ) );
}
  1. $nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS );: 这个函数获取 Nonce 的有效时间。默认情况下,DAY_IN_SECONDS 是 86400 秒 (也就是一天)。 你可以通过 nonce_life 过滤器来修改这个值。

  2. return ceil( time() / ( $nonce_life / 2 ) );: 这行代码是关键。 它把当前时间戳除以 Nonce 有效时间的一半,然后向上取整。 这意味着 Nonce 的有效期实际上是 Nonce lifespan 的一半,而每次 wp_nonce_tick() 函数返回的值会在 Nonce lifespan 的一半时间后才会变化一次。默认情况下Nonce的有效期是12小时。

为什么要这么做呢? 这是为了提高 Nonce 的安全性。 如果 Nonce 的有效期太短,用户在填写表单的时候,Nonce 可能就过期了。 如果 Nonce 的有效期太长,攻击者就有更多的时间来破解 Nonce。 所以,WordPress 选择了折中的方案,把 Nonce 的有效期设置为 Nonce lifespan 的一半。

wp_hash(): 哈希的守护者

咱们再来看看 wp_hash() 函数。这个函数负责对数据进行哈希,生成一个唯一的哈希值。 源码如下(简化版):

function wp_hash( $data, $algo = 'auth' ) {
    static $keys;

    if ( empty( $keys ) ) {
        $keys = wp_hash_salt();
    }

    $key = $keys[ $algo ];

    if ( empty( $key ) ) {
        $key = $keys['auth'];
    }

    $hmac = hash_hmac( 'md5', $data, $key );

    return $hmac;
}
  1. static $keys;: 使用静态变量缓存哈希盐值。

  2. if ( empty( $keys ) ) { $keys = wp_hash_salt(); }: 如果 $keys 为空,则调用 wp_hash_salt() 获取哈希盐值。

  3. $key = $keys[ $algo ];: 根据传入的 $algo 参数选择对应的哈希盐值。

  4. if ( empty( $key ) ) { $key = $keys['auth']; }: 如果 $algo 对应的哈希盐值不存在,则使用 auth 对应的盐值。

  5. $hmac = hash_hmac( 'md5', $data, $key );: 使用 hash_hmac() 函数进行哈希。hash_hmac() 函数使用 HMAC (Hash-based Message Authentication Code) 算法,它需要一个密钥 (这里是 $key) 来生成哈希值。 这使得哈希值更加安全,即使攻击者知道哈希算法,也无法轻易地破解哈希值。

  6. return $hmac;: 返回生成的哈希值。

重点是 wp_hash_salt() 函数,它返回一个包含多个哈希盐值的数组。 这些盐值是在 WordPress 安装时随机生成的,存储在 wp-config.php 文件中。 它们是保护 Nonce 安全的关键。

wp_verify_nonce(): Nonce 的验证官

有了 Nonce,当然也要有验证 Nonce 的方法。 wp_verify_nonce() 函数就是负责验证 Nonce 的。 它的源码如下:

function wp_verify_nonce( $nonce, $action = -1 ) {
    $nonce = (string) $nonce;
    $user = wp_get_current_user();
    $uid = (int) $user->ID;
    if ( ! $uid ) {
        $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
    }

    $token = wp_get_session_token();

    $i = wp_nonce_tick();

    // Nonce generated 0-12 hours ago
    $expected = substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
    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 );
    if ( hash_equals( $expected, $nonce ) ) {
        return 2;
    }

    /**
     * Fires when a nonce verification fails.
     *
     * @since 4.5.0
     *
     * @param string|int $nonce  The nonce that was attempted to be verified.
     * @param string|int $action The action that was used when generating the nonce.
     */
    do_action( 'wp_nonce_failed', $nonce, $action );

    return false;
}
  1. $nonce = (string) $nonce;: 将 Nonce 转换为字符串。

  2. $user = wp_get_current_user();$uid = (int) $user->ID;: 获取当前用户的 ID。

  3. if ( ! $uid ) { $uid = apply_filters( 'nonce_user_logged_out', $uid, $action ); }: 如果用户未登录,则应用 nonce_user_logged_out 过滤器。

  4. $token = wp_get_session_token();: 获取用户的会话 Token。

  5. $i = wp_nonce_tick();: 获取当前的时间戳。

  6. $expected = substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );: 使用当前时间戳生成一个 Nonce。

  7. if ( hash_equals( $expected, $nonce ) ) { return 1; }: 将生成的 Nonce 和传入的 Nonce 进行比较。 这里使用了 hash_equals() 函数,它可以防止时序攻击 (Timing Attack)。 如果两个 Nonce 相等,说明 Nonce 是有效的,函数返回 1。

  8. $expected = substr( wp_hash( ( $i - 1 ) . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );: 如果当前时间戳生成的 Nonce 无效,再尝试用前一个时间戳生成一个 Nonce。 这是因为 Nonce 的有效期是12小时,而 wp_nonce_tick() 函数返回的时间戳每 12 小时才会变化一次。

  9. if ( hash_equals( $expected, $nonce ) ) { return 2; }: 将用前一个时间戳生成的 Nonce 和传入的 Nonce 进行比较。 如果两个 Nonce 相等,说明 Nonce 是有效的,函数返回 2。

  10. do_action( 'wp_nonce_failed', $nonce, $action );: 如果两个 Nonce 都不相等,说明 Nonce 是无效的,触发 wp_nonce_failed action。

  11. return false;: 返回 false,表示 Nonce 验证失败。

总结: Nonce 的工作流程

现在,咱们来总结一下 Nonce 的工作流程:

  1. 创建 Nonce: 使用 wp_create_nonce() 函数创建一个 Nonce。 这个函数会把用户 ID、动作、时间和会话Token戳用管道符连接起来,然后用 wp_hash() 函数进行哈希,最后截取哈希值的一部分作为 Nonce。

  2. 在表单中加入 Nonce: 把 Nonce 作为一个隐藏字段添加到表单中。

  3. 提交表单: 用户提交表单,Nonce 也随之被提交到服务器。

  4. 验证 Nonce: 使用 wp_verify_nonce() 函数验证 Nonce。 这个函数会用当前时间和前一个时间戳生成两个 Nonce,然后和提交的 Nonce 进行比较。 如果有一个 Nonce 相等,说明 Nonce 是有效的。

代码示例

咱们来写一个简单的代码示例,演示如何使用 wp_nonce

<?php
// 生成 Nonce
$nonce = wp_create_nonce( 'my_action' );

// 显示表单
echo '<form method="post">';
echo '<input type="hidden" name="my_nonce" value="' . esc_attr( $nonce ) . '">';
echo '<input type="submit" value="Submit">';
echo '</form>';

// 处理表单提交
if ( $_SERVER['REQUEST_METHOD'] === 'POST' ) {
    // 验证 Nonce
    if ( wp_verify_nonce( $_POST['my_nonce'], 'my_action' ) ) {
        // Nonce 验证成功,处理表单数据
        echo 'Nonce is valid!';
    } else {
        // Nonce 验证失败
        echo 'Nonce is invalid!';
    }
}
?>

在这个例子中,咱们首先使用 wp_create_nonce() 函数创建了一个 Nonce,并把它添加到了表单中。 然后,在处理表单提交的时候,咱们使用 wp_verify_nonce() 函数验证 Nonce。 如果 Nonce 验证成功,就处理表单数据,否则就显示一个错误信息。

表格总结

为了方便大家理解,咱们用一个表格来总结一下 wp_create_nonce()wp_verify_nonce() 函数的参数和返回值:

函数 参数 返回值
wp_create_nonce() $action (可选): 一个字符串,用于区分 Nonce 的用途。 如果不传参数,默认是 -1。 一个加密过的字符串,包含了用户 ID、动作和时间戳。
wp_verify_nonce() $nonce (必选): 要验证的 Nonce。$action (可选): 一个字符串,用于区分 Nonce 的用途。 如果不传参数,默认是 -1。 1: Nonce 验证成功,且是用当前时间戳生成的。2: Nonce 验证成功,且是用前一个时间戳生成的。false: Nonce 验证失败。

安全性思考

wp_nonce 机制虽然可以有效地防止 CSRF 攻击,但它并不是万无一失的。 攻击者仍然可以通过一些手段来绕过 Nonce 验证,比如:

  • 窃取 Nonce: 如果攻击者能够窃取到用户的 Nonce,就可以伪造请求。 所以,要保护好 Nonce,不要把它泄露出去。

  • 重放 Nonce: 如果 Nonce 的有效期太长,攻击者就可以重放 Nonce。 所以,要设置一个合理的 Nonce 有效期。

  • 猜测 Nonce: 如果 Nonce 的生成算法不够安全,攻击者就可以猜测 Nonce。 所以,要使用安全的哈希算法来生成 Nonce。

因此,除了使用 wp_nonce 机制之外,还要采取其他的安全措施,比如:

  • 使用 HTTPS: 使用 HTTPS 可以防止攻击者窃取 Nonce。

  • 验证 Referer: 验证 Referer 可以防止攻击者伪造请求。

  • 限制请求频率: 限制请求频率可以防止攻击者进行暴力破解。

总结

今天咱们详细地讲解了 wp_nonce 机制的源码,特别是 wp_create_nonce() 函数是如何生成 Nonce 的。 希望通过今天的讲解,大家能够对 wp_nonce 有一个更深入的了解,并在自己的 WordPress 项目中正确地使用它,保护网站的安全。记住,安全无小事!下次见!

发表回复

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