分析 WordPress `is_user_logged_in()` 函数的源码:如何通过 `Cookie` 和全局变量判断登录状态。

嘿!各位晚上好!今天咱们来聊聊 WordPress 身份验证的幕后英雄:is_user_logged_in() 函数。它虽然看起来简单,但实际上却融合了 Cookie 和全局变量两大武器,守护着 WordPress 的登录状态。准备好了吗?咱们这就开始深入剖析!

一、认识我们的主角:is_user_logged_in()

首先,让我们明确一下目标。is_user_logged_in() 函数的作用很简单,就是判断当前用户是否已经登录。如果登录了,返回 true;否则,返回 false。 听起来简单,但 WordPress 如何知道你是否登录了呢?这就涉及到 Cookie 和全局变量的巧妙配合了。

二、Cookie:记住你身份的小饼干

想象一下,你每次访问一个网站,都要重新输入用户名和密码,是不是很烦?Cookie 就是为了解决这个问题而生的。它就像一个身份识别卡,网站在你登录后会给你发一个 Cookie,以后你再访问,网站就通过这个 Cookie 认出你来了。

WordPress 也不例外。当你成功登录后,WordPress 会设置一些 Cookie 来记录你的登录信息。其中最重要的几个 Cookie 包括:

  • wordpress_[hash]: 存储你的用户名
  • wordpress_logged_in_[hash]: 存储登录状态以及其他验证信息
  • wordpress_sec_[hash]: (如果使用多站点) 存储安全信息,用于验证用户是否拥有访问特定站点的权限。

这里的 [hash] 是 WordPress 为每个站点生成的唯一哈希值,用于防止 Cookie 被其他站点盗用。

三、全局变量:WordPress 的内部记忆

除了 Cookie,WordPress 还使用全局变量来维护用户的登录状态。最关键的全局变量就是 $current_user

  • $current_user: 这是一个 WP_User 类的实例,包含了当前用户的各种信息,比如 ID、用户名、邮箱等等。

当用户成功登录后,WordPress 会用登录信息填充 $current_user 全局变量。以后,WordPress 就可以直接从 $current_user 中获取用户的登录信息,而不用每次都去读取 Cookie。

四、is_user_logged_in() 的源码解析

现在,让我们深入到 is_user_logged_in() 的源码中,看看它是如何利用 Cookie 和全局变量来判断登录状态的。

function is_user_logged_in() {
    /**
     * Fires before determining whether the current user is logged in.
     *
     * @since 2.5.0
     */
    do_action( 'init' ); //确保已经执行了初始化操作

    /**
     * Fires before determining whether the current user is logged in.
     *
     * @since 2.5.0
     *
     * @param bool $is_logged_in Whether the user is logged in. Default false.
     */
    $is_logged_in = apply_filters( 'determine_current_user', false ); //先通过插件过滤器尝试判断

    if ( $is_logged_in ) {
        return true;
    }

    if ( ! function_exists( 'wp_get_current_user' ) ) {
        include_once ABSPATH . WPINC . '/pluggable.php'; //加载可插拔函数
    }

    $user = wp_get_current_user(); //获取当前用户

    return ( $user->exists() ); //判断用户是否存在
}

让我们逐行分析这段代码:

  1. do_action( 'init' ): 这个 do_action 确保 WordPress 的初始化过程已经完成。在初始化过程中,WordPress 会设置 Cookie 和 $current_user 全局变量。如果没有完成初始化,就无法正确判断登录状态。

  2. $is_logged_in = apply_filters( 'determine_current_user', false ): 这是一个过滤器,允许插件通过自定义逻辑来判断登录状态。如果插件返回 true,则直接认为用户已登录,并跳过后续的判断。这提供了一种灵活的扩展机制。

  3. if ( ! function_exists( 'wp_get_current_user' ) ) { include_once ABSPATH . WPINC . '/pluggable.php'; }: 这一步检查 wp_get_current_user() 函数是否存在。如果不存在,就加载 pluggable.php 文件。pluggable.php 文件包含了一些可以被插件替换的核心函数,包括 wp_get_current_user()。 如果插件没有替换默认的wp_get_current_user()函数,则使用WordPress默认的。

  4. $user = wp_get_current_user(): 这是最关键的一步。wp_get_current_user() 函数负责从 Cookie 和 $current_user 全局变量中获取当前用户的登录信息。

  5. return ( $user->exists() ): 最后,is_user_logged_in() 函数判断 $user 对象是否存在。如果 $user 对象存在,说明用户已经登录,返回 true;否则,返回 false

五、wp_get_current_user() 源码解析

wp_get_current_user() 函数才是真正干活的。它负责读取 Cookie 和 $current_user 全局变量,并根据这些信息来判断用户是否登录。

function wp_get_current_user() {
    global $current_user, $wp_roles;

    if ( ! ( $current_user instanceof WP_User ) ) {
        if ( defined( 'XMLRPC_REQUEST' ) || defined( 'REST_REQUEST' ) || ( defined( 'WP_INSTALLING' ) && WP_INSTALLING ) || defined( 'WP_IMPORTING' ) ) {
            $current_user = new WP_User( 0 );
        } else {
            $current_user = wp_set_current_user();
        }
    }

    return $current_user;
}
  1. global $current_user, $wp_roles: 首先,声明 $current_user$wp_roles 为全局变量。$current_user 是我们前面提到的存储用户信息的对象,$wp_roles 则存储了 WordPress 的角色信息。

  2. if ( ! ( $current_user instanceof WP_User ) ): 这一步判断 $current_user 是否已经是 WP_User 类的实例。如果不是,说明还没有加载用户信息,需要从 Cookie 中读取。 如果是XMLRPC_REQUEST, REST_REQUEST,WP_INSTALLING或者WP_IMPORTING,则创建ID为0的用户,否则调用wp_set_current_user()。

  3. $current_user = wp_set_current_user(): 调用 wp_set_current_user() 函数来设置 $current_user 全局变量。这个函数负责读取 Cookie,验证登录信息,并填充 $current_user 对象。

  4. return $current_user: 最后,返回 $current_user 对象。

六、wp_set_current_user() 源码解析

wp_set_current_user() 函数是最核心的部分。它负责读取 Cookie,验证登录信息,并填充 $current_user 对象。

function wp_set_current_user( $id = 0, $name = '' ) {
    global $current_user, $wpdb;

    if ( ! ( $current_user instanceof WP_User ) ) {
        $current_user = new WP_User( 0 );
    }

    if ( 0 !== $id ) {
        $id = (int) $id;
        $userdata = get_userdata( $id );
        if ( $userdata ) {
            $current_user->init( $userdata );
        } else {
            $current_user->init( $id );
        }

        return $current_user;
    }

    if ( defined( 'XMLRPC_REQUEST' ) || defined( 'REST_REQUEST' ) ) {
        return $current_user;
    }

    if ( isset( $_GET['auth_redirect'] ) ) {
        return $current_user;
    }

    $user_id = apply_filters( 'determine_current_user', false );
    if ( $user_id ) {
        $current_user = get_userdata( $user_id );
        return $current_user;
    }

    $logged_in_cookie = wp_parse_auth_cookie( '', 'logged_in' );

    if ( ! empty( $logged_in_cookie['token'] ) ) {
        $token = $logged_in_cookie['token'];
        $hash  = hash( 'sha256', $token );

        $row = $wpdb->get_row( $wpdb->prepare( "SELECT user_id, expiration, user_password, token_password FROM {$wpdb->usermeta} WHERE meta_key = 'session_tokens' AND meta_value LIKE %s", '%' . $wpdb->esc_like( $hash ) . '%' ) );

        if ( $row ) {
            $session_tokens = json_decode( $row->meta_value, true );

            foreach ( $session_tokens as $session_hash => $session_data ) {
                if ( hash_equals( $session_hash, $hash ) ) {
                    if ( time() < $session_data['expiration'] ) {
                        $user_id = (int) $row->user_id;
                        if ( $user_id ) {
                            $userdata = get_userdata( $user_id );
                            if ( $userdata ) {
                                $current_user->init( $userdata );

                                /**
                                 * Fires immediately after the user is switched.
                                 *
                                 * @since 2.9.0
                                 *
                                 * @param int $id The user ID.
                                 * @param WP_User $old_current_user The old current user object.
                                 */
                                do_action( 'set_current_user', $user_id, $current_user );

                                wp_cache_close();

                                return $current_user;
                            }
                        }
                    }
                }
            }
        }
    }

    $current_user->init( 0 );
    return $current_user;
}

让我们分解一下:

  1. global $current_user, $wpdb: 声明 $current_user$wpdb 为全局变量。$wpdb 是 WordPress 数据库连接对象。

  2. if ( ! ( $current_user instanceof WP_User ) ) { $current_user = new WP_User( 0 ); }: 如果 $current_user 还不是 WP_User 类的实例,则创建一个新的 WP_User 对象。

  3. if ( 0 !== $id ): 如果传入了用户 ID,则根据 ID 获取用户信息,并填充 $current_user 对象。

  4. if ( defined( 'XMLRPC_REQUEST' ) || defined( 'REST_REQUEST' ) ): 如果是 XML-RPC 或 REST 请求,则直接返回 $current_user 对象。通常情况下,对于这些请求,需要使用其他的身份验证方式(比如 API 密钥)。

  5. $logged_in_cookie = wp_parse_auth_cookie( '', 'logged_in' ): 调用 wp_parse_auth_cookie() 函数来解析 wordpress_logged_in_[hash] Cookie。这个 Cookie 包含了用户的登录信息,比如用户名、密码哈希和登录时间。

  6. if ( ! empty( $logged_in_cookie['token'] ) ): 检查 Cookie 中是否存在 token,如果存在,则进行后续的验证。

  7. 查询数据库: 根据token,查询数据库中usermeta表,获取存储的session_token,判断session是否过期,token是否正确。

  8. $current_user->init( $userdata ): 如果验证成功,则根据用户信息填充 $current_user 对象。

  9. return $current_user: 最后,返回 $current_user 对象。

七、wp_parse_auth_cookie() 源码解析

wp_parse_auth_cookie() 函数负责解析 Cookie,提取其中的登录信息。

function wp_parse_auth_cookie( $cookie = '', $scheme = '' ) {
    if ( empty( $cookie ) ) {
        /**
         * Filters the authentication cookie.
         *
         * @since 2.5.0
         *
         * @param string $cookie The authentication cookie.
         * @param string $scheme The scheme to use. Default is empty string.
         */
        $cookie = $_COOKIE[ LOGGED_IN_COOKIE ] ?? '';
    }

    if ( empty( $cookie ) ) {
        return false;
    }

    $cookie_elements = explode( '|', $cookie );

    if ( count( $cookie_elements ) !== 4 ) {
        return false;
    }

    list( $username, $expiration, $token, $hmac ) = $cookie_elements;

    $username   = sanitize_user( $username, 'username' );
    $expiration = intval( $expiration );

    /**
     * Filters the authentication cookie elements.
     *
     * @since 2.8.0
     *
     * @param array $cookie_elements An array containing the authentication cookie elements.
     *                                Array keys include 'username', 'expiration', 'token', and 'hmac'.
     * @param string $cookie The authentication cookie.
     * @param string $scheme The scheme to use. Default is empty string.
     */
    $cookie_elements = apply_filters( 'auth_cookie_elements', compact( 'username', 'expiration', 'token', 'hmac' ), $cookie, $scheme );
    $username   = $cookie_elements['username'];
    $expiration = $cookie_elements['expiration'];
    $token      = $cookie_elements['token'];
    $hmac       = $cookie_elements['hmac'];

    // Allow zero-length username to be rejected.
    if ( '' === $username ) {
        return false;
    }

    $algo = 'sha256';
    $hash = hash_hmac( $algo, $username . '|' . $expiration . '|' . $token, wp_hash( 'logged_in' ) );

    if ( ! hash_equals( $hmac, $hash ) ) {
        /**
         * Fires after an authentication cookie fails hash comparison.
         *
         * @since 3.5.0
         *
         * @param array $cookie_elements An array containing the authentication cookie elements.
         *                                Array keys include 'username', 'expiration', 'token', and 'hmac'.
         * @param string $cookie The authentication cookie.
         * @param string $scheme The scheme to use. Default is empty string.
         */
        do_action( 'auth_cookie_malformed', $cookie_elements, $cookie, $scheme );
        return false;
    }

    return array(
        'username'   => $username,
        'expiration' => $expiration,
        'token'      => $token,
        'hmac'       => $hmac,
    );
}
  1. $cookie = $_COOKIE[ LOGGED_IN_COOKIE ] ?? '': 从 $_COOKIE 数组中获取 wordpress_logged_in_[hash] Cookie 的值。

  2. $cookie_elements = explode( '|', $cookie ): 将 Cookie 的值用 | 分隔成多个元素。

  3. list( $username, $expiration, $token, $hmac ) = $cookie_elements: 将分隔后的元素分别赋值给 $username$expiration$token$hmac 变量。

  4. $hash = hash_hmac( $algo, $username . '|' . $expiration . '|' . $token, wp_hash( 'logged_in' ) ): 使用 hash_hmac() 函数计算出一个哈希值。

  5. if ( ! hash_equals( $hmac, $hash ) ): 比较 Cookie 中的 $hmac 值和计算出的哈希值。如果两者不相等,说明 Cookie 被篡改,返回 false

  6. return array(...): 如果验证成功,则返回一个包含用户名、过期时间、token和哈希值的数组。

八、总结:Cookie + 全局变量,双剑合璧

通过上面的分析,我们可以看到,is_user_logged_in() 函数的实现依赖于 Cookie 和全局变量的协同工作。

  • Cookie 存储了用户的登录信息,就像一张身份识别卡,告诉 WordPress 你是谁。
  • 全局变量 $current_user 存储了当前用户的详细信息,方便 WordPress 在内部使用。

is_user_logged_in() 函数首先尝试从 Cookie 中读取登录信息,如果 Cookie 存在且有效,则将用户信息填充到 $current_user 全局变量中。以后,WordPress 就可以直接从 $current_user 中获取用户的登录信息,而不用每次都去读取 Cookie。

九、实际应用:如何使用 is_user_logged_in()

is_user_logged_in() 函数在 WordPress 开发中非常常用。你可以使用它来控制内容的显示,比如:

<?php if ( is_user_logged_in() ) : ?>
    <p>欢迎回来,<?php echo wp_get_current_user()->display_name; ?>!</p>
    <a href="<?php echo wp_logout_url( get_permalink() ); ?>">退出登录</a>
<?php else : ?>
    <p>请 <a href="<?php echo wp_login_url( get_permalink() ); ?>">登录</a> 后查看更多内容。</p>
<?php endif; ?>

这段代码会根据用户的登录状态显示不同的内容。如果用户已经登录,则显示欢迎信息和退出登录链接;否则,显示登录链接。

你还可以使用 is_user_logged_in() 函数来控制用户的权限,比如:

<?php if ( is_user_logged_in() && current_user_can( 'edit_posts' ) ) : ?>
    <a href="<?php echo admin_url( 'post-new.php' ); ?>">新建文章</a>
<?php endif; ?>

这段代码会判断用户是否登录,并且是否拥有编辑文章的权限。如果两者都满足,则显示新建文章的链接。

十、is_user_logged_in() 的优缺点

  • 优点:

    • 简单易用:只需要调用一个函数就可以判断登录状态。
    • 可扩展性强:可以通过过滤器自定义登录判断逻辑。
    • 安全性高:使用哈希算法来保护 Cookie 的安全。
  • 缺点:

    • 依赖于 Cookie:如果用户禁用了 Cookie,则无法正确判断登录状态。
    • 容易受到 CSRF 攻击:需要采取额外的措施来防止跨站请求伪造攻击。

十一、总结

is_user_logged_in() 函数是 WordPress 身份验证的核心组成部分。它通过 Cookie 和全局变量的协同工作,实现了简单、安全、可扩展的登录状态判断机制。理解 is_user_logged_in() 函数的实现原理,可以帮助你更好地理解 WordPress 的身份验证机制,并开发出更安全、更可靠的 WordPress 插件和主题。

希望今天的讲解对大家有所帮助!如果有什么问题,欢迎随时提问!

发表回复

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