WordPress REST API认证流程中wp_rest_auth_cookie验证机制的源码分析

WordPress REST API 认证流程中 wp_rest_auth_cookie 验证机制的源码分析

大家好,今天我们来深入探讨 WordPress REST API 认证流程中一个非常重要的环节:wp_rest_auth_cookie 验证机制。这是一个基于 Cookie 的身份验证方式,常用于 WordPress 站点内部的 REST API 调用,例如 Gutenberg 编辑器、后台管理界面等等。我们将从源码层面剖析它的工作原理,理解其安全性以及适用场景。

1. Cookie 认证的基本概念

在深入源码之前,我们先简单回顾一下 Cookie 认证的基础概念。

  • Cookie: 储存在用户浏览器上的小型文本文件,可以包含用户身份信息、会话信息等。
  • Session: 服务器端维护的用户会话状态,通常与 Cookie 中的 Session ID 关联。

Cookie 认证的基本流程是:用户登录后,服务器生成一个 Session ID,并将它保存在 Cookie 中发送给浏览器。后续用户访问时,浏览器会自动携带这个 Cookie,服务器通过 Cookie 中的 Session ID 找到对应的 Session 信息,从而验证用户身份。

2. WordPress Cookie 认证的特殊性

WordPress 的 Cookie 认证机制与传统的 Session 认证有一些区别:

  • 没有显式的 Session: WordPress 并没有像 PHP 的 $_SESSION 那样的全局 Session 对象。它的 Cookie 认证依赖于 WordPress 的用户数据和密钥。
  • 密钥加盐: WordPress 使用 AUTH_KEYSECURE_AUTH_KEY 等常量作为密钥,并对密钥进行加盐处理,增强安全性。
  • 插件扩展性: WordPress 提供了丰富的钩子(hooks)机制,允许插件扩展或修改 Cookie 认证流程。

3. wp_rest_auth_cookie 验证机制的入口:rest_cookie_check_errors()

rest_cookie_check_errors() 函数是 REST API 请求进行 Cookie 认证的入口点。它位于 wp-includes/rest-api/class-wp-rest-server.php 文件中。

/**
 * Check if the user is logged in based on cookies.
 *
 * @since 4.7.0
 *
 * @param WP_Error|null $result Authentication result, null if not yet authenticated.
 *
 * @return WP_Error|true Authentication result.
 */
function rest_cookie_check_errors( $result ) {
    if ( ! empty( $result ) ) {
        return $result;
    }

    if ( ! is_user_logged_in() ) {
        return new WP_Error(
            'rest_not_logged_in',
            __( 'You are not currently logged in.' ),
            array( 'status' => 401 )
        );
    }

    // Is the user logged in, but lacking the nonce?
    if ( empty( $_COOKIE[ LOGGED_IN_COOKIE ] ) ) {
        return new WP_Error(
            'rest_cookie_invalid_nonce',
            __( 'Cookie nonce is invalid' ),
            array( 'status' => 403 )
        );
    }

    return true;
}

这个函数主要做了以下几件事:

  1. 检查是否已经认证: 如果 $result 不为空,说明已经经过其他认证方式(例如 OAuth),直接返回 $result
  2. 检查用户是否登录: 使用 is_user_logged_in() 函数检查用户是否已经登录。如果未登录,返回一个 WP_Error 对象,表示认证失败。
  3. 检查 Cookie 是否存在: 检查 LOGGED_IN_COOKIE 常量对应的 Cookie 是否存在。如果不存在,返回一个 WP_Error 对象,表示 Cookie 无效。

注意: LOGGED_IN_COOKIE 常量定义了 WordPress 用于存储登录信息的 Cookie 名称。它通常包含用户名、过期时间和一个哈希值。

4. is_user_logged_in() 函数的运作方式

is_user_logged_in() 函数是 WordPress 验证用户是否登录的关键函数。它位于 wp-includes/pluggable.php 文件中。

/**
 * Determines whether the current visitor is a logged in user.
 *
 * @since 2.0.0
 *
 * @return bool True if the user is logged in, false if not.
 */
function is_user_logged_in() {
    $user = wp_get_current_user();

    return ( $user->exists() );
}

is_user_logged_in() 函数内部调用了 wp_get_current_user() 函数来获取当前用户对象。如果用户对象存在($user->exists() 返回 true),则认为用户已登录。

5. wp_get_current_user() 函数的深入分析

wp_get_current_user() 函数负责从 Cookie 中提取用户信息并验证其有效性。它位于 wp-includes/pluggable.php 文件中。

/**
 * Retrieve the current user object.
 *
 * @since 2.0.0
 * @since 3.5.0 Returns a WP_User object, or a WP_User object with ID 0 if no user is logged in.
 *
 * @return WP_User WP_User object
 */
function wp_get_current_user() {
    static $current_user = null;

    if ( ! isset( $current_user ) ) {
        $current_user = wp_get_current_user_old();
    }

    return $current_user;
}

这个函数使用了静态变量 $current_user 来缓存用户对象,避免重复查询。它实际上调用了 wp_get_current_user_old() 函数来完成用户信息的提取和验证。

6. wp_get_current_user_old() 函数的详细过程

wp_get_current_user_old() 函数是 Cookie 认证的核心所在。它位于 wp-includes/pluggable.php 文件中。

function wp_get_current_user_old() {
    global $wp_current_user;

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

    if ( defined( 'XMLRPC_REQUEST' ) || defined( 'REST_REQUEST' ) || ( defined( 'WP_INSTALLING' ) && WP_INSTALLING ) || is_feed() || defined( 'DOING_AJAX' ) ) {
        return $wp_current_user;
    }

    if ( isset( $_COOKIE[ LOGGED_IN_COOKIE ] ) ) {
        $logged_in_cookie = $_COOKIE[ LOGGED_IN_COOKIE ];
        $cookie_elements = explode( '|', $logged_in_cookie );

        if ( count( $cookie_elements ) === 4 ) {
            list( $username, $expiration, $token, $hmac ) = $cookie_elements;

            if ( $expiration > time() ) {
                /**
                 * Fires before the authentication cookie is validated.
                 *
                 * @since 2.8.0
                 *
                 * @param string $username The username from the authentication cookie.
                 * @param string $token The token from the authentication cookie.
                 */
                do_action( 'auth_cookie_validation', $username, $token );

                $user = get_user_by( 'login', $username );

                if ( $user ) {
                    $pass_frag = substr( $user->user_pass, 8, 4 );

                    $key = wp_hash( $username . '|' . $pass_frag . '|' . $expiration . '|' . $token, 'logged_in' );
                    $key = hash_hmac( 'md5', $username . '|' . $pass_frag . '|' . $expiration . '|' . $token, $key );

                    if ( hash_equals( $hmac, $key ) ) {
                        $wp_current_user = $user;
                        wp_set_auth_cookie( $user->ID, true, is_ssl() );
                        do_action( 'set_current_user' );
                    }
                }
            }
        }
    }

    return $wp_current_user;
}

这个函数的过程比较复杂,我们逐步分析:

  1. 初始化用户对象: 如果 $wp_current_user 不是 WP_User 类的实例,则创建一个新的 WP_User 对象。
  2. 跳过 Cookie 认证: 在某些情况下(例如 XML-RPC 请求、REST 请求、安装过程、Feed 以及 AJAX 请求),会直接返回初始化的用户对象,跳过 Cookie 认证。注意:这里 REST_REQUEST 的定义会跳过cookie认证,但是实际上,rest_cookie_check_errors 会先于此函数执行,所以会进行cookie检查。
  3. 检查 Cookie 是否存在: 检查 LOGGED_IN_COOKIE 常量对应的 Cookie 是否存在。如果不存在,直接返回初始化的用户对象。
  4. 解析 Cookie 值: 将 Cookie 值按照 | 分隔符拆分成多个元素。这些元素包括:用户名、过期时间、token 和 HMAC 值。
  5. 验证 Cookie 过期时间: 检查过期时间是否大于当前时间。如果已过期,直接返回初始化的用户对象。
  6. 执行 auth_cookie_validation 钩子: 触发 auth_cookie_validation 钩子,允许插件在 Cookie 验证之前执行自定义逻辑。
  7. 根据用户名获取用户对象: 使用 get_user_by( 'login', $username ) 函数根据用户名获取用户对象。
  8. 计算 HMAC 值: 使用密钥和加盐算法计算 HMAC 值,并与 Cookie 中的 HMAC 值进行比较。
    • 获取密码片段: 从用户密码中截取一个片段(第 8 到 11 位)。
    • 生成密钥: 使用用户名、密码片段、过期时间和 token,以及 ‘logged_in’ 字符串,通过 wp_hash() 函数生成密钥。
    • 计算 HMAC: 使用 HMAC-MD5 算法对用户名、密码片段、过期时间和 token 进行哈希计算,密钥为上一步生成的密钥。
  9. 验证 HMAC 值: 使用 hash_equals() 函数比较计算出的 HMAC 值和 Cookie 中的 HMAC 值。hash_equals() 函数可以防止时序攻击。
  10. 设置当前用户: 如果 HMAC 值验证成功,则将获取到的用户对象设置为当前用户,并调用 wp_set_auth_cookie() 函数重新设置 Cookie。
  11. 执行 set_current_user 钩子: 触发 set_current_user 钩子,允许插件在设置当前用户后执行自定义逻辑。

7. wp_set_auth_cookie() 函数的作用

wp_set_auth_cookie() 函数用于设置登录 Cookie。它位于 wp-includes/pluggable.php 文件中。

/**
 * Sets the authentication cookies.
 *
 * @since 2.5.0
 *
 * @param int    $user_id User ID.
 * @param bool   $remember Whether to remember the user. Default false.
 * @param bool   $secure Whether the admin cookies should only be used over HTTPS.
 */
function wp_set_auth_cookie( $user_id, $remember = false, $secure = '' ) {
    if ( '' === $secure ) {
        $secure = is_ssl();
    }

    $expiration = time() + DAY_IN_SECONDS;
    $rememberme_expiration = time() + ( $remember ? ( 14 * DAY_IN_SECONDS ) : DAY_IN_SECONDS );

    if ( $remember ) {
        $expiration = $rememberme_expiration;
    }

    $user = get_userdata( $user_id );
    $username = $user->user_login;

    /**
     * Filters the authentication cookie expiration period.
     *
     * @since 2.8.0
     *
     * @param int    $expiration  Authentication cookie expiration period.
     * @param int    $user_id     User ID.
     * @param bool   $remember    Whether to remember the user. Default false.
     * @param string $username    User login.
     */
    $expiration = apply_filters( 'auth_cookie_expiration', $expiration, $user_id, $remember, $username );

    $token = wp_generate_password( 43, false, false );
    update_user_meta( $user_id, 'session_tokens', wp_get_session_token( $user_id, $token ) );

    $pass_frag = substr( $user->user_pass, 8, 4 );

    $key = wp_hash( $username . '|' . $pass_frag . '|' . $expiration . '|' . $token, 'logged_in' );
    $hash = hash_hmac( 'md5', $username . '|' . $pass_frag . '|' . $expiration . '|' . $token, $key );

    $auth_cookie = $username . '|' . $expiration . '|' . $token . '|' . $hash;

    setcookie( LOGGED_IN_COOKIE, $auth_cookie, $expiration, COOKIEPATH, COOKIE_DOMAIN, $secure, true );

    if ( COOKIEPATH != SITECOOKIEPATH ) {
        setcookie( LOGGED_IN_COOKIE, $auth_cookie, $expiration, SITECOOKIEPATH, COOKIE_DOMAIN, $secure, true );
    }

    /**
     * Fires after the authentication cookies are set.
     *
     * @since 2.5.0
     *
     * @param string $auth_cookie The authentication cookie.
     * @param int    $user_id     User ID.
     * @param bool   $remember    Whether to remember the user. Default false.
     * @param string $username    User login.
     */
    do_action( 'set_auth_cookie', $auth_cookie, $user_id, $remember, $username );
}

这个函数主要做了以下几件事:

  1. 确定 Cookie 的安全标志: 根据是否使用 HTTPS 协议,确定 Cookie 的安全标志。
  2. 计算 Cookie 的过期时间: 根据是否选择“记住我”选项,计算 Cookie 的过期时间。
  3. 获取用户名: 根据用户 ID 获取用户名。
  4. 生成 token: 生成一个随机token, 并使用update_user_meta保存到数据库. wp_get_session_token 函数用于管理session token, 包括生成,存储,删除等操作。
  5. 计算 HMAC 值: 使用密钥和加盐算法计算 HMAC 值。
  6. 构建 Cookie 值: 将用户名、过期时间、token 和 HMAC 值组合成 Cookie 值。
  7. 设置 Cookie: 使用 setcookie() 函数设置 Cookie。
  8. 触发 set_auth_cookie 钩子: 触发 set_auth_cookie 钩子,允许插件在设置 Cookie 后执行自定义逻辑。

8. wp_hash() 函数和密钥加盐

wp_hash() 函数是 WordPress 用于生成密钥的函数。它位于 wp-includes/pluggable.php 文件中。

/**
 * Generates a hash (keyed HMAC) of a given string.
 *
 * @since 2.5.0
 * @access private
 *
 * @param string $data  The string to be hashed.
 * @param string $scheme The cryptographic scheme to use.
 *                       See hash_hmac() for list of supported schemes.
 * @return string The hash.
 */
function wp_hash( $data, $scheme = 'auth' ) {
    $salt = wp_salt( $scheme );
    return hash_hmac( 'md5', $data, $salt );
}

wp_hash() 函数内部调用了 wp_salt() 函数来获取盐值,然后使用 HMAC-MD5 算法对数据进行哈希计算。

wp_salt() 函数负责生成盐值。它位于 wp-includes/pluggable.php 文件中。

/**
 * Salts a string using the AUTH_KEY constant.
 *
 * @since 2.5.0
 * @access private
 *
 * @param string $scheme Salt scheme.
 * @return string
 */
function wp_salt( $scheme = 'auth' ) {
    if ( defined( 'AUTH_KEY' ) ) {
        switch ( $scheme ) {
            case 'auth':
                $salt = AUTH_KEY;
                break;
            case 'secure_auth':
                $salt = SECURE_AUTH_KEY;
                break;
            case 'logged_in':
                $salt = LOGGED_IN_KEY;
                break;
            case 'nonce':
                $salt = NONCE_KEY;
                break;
            default:
                $salt = AUTH_KEY;
                break;
        }
    } else {
        $salt = '';
    }

    return $salt;
}

wp_salt() 函数根据不同的方案(例如 ‘auth’、’secure_auth’、’logged_in’、’nonce’)返回不同的密钥。这些密钥定义在 wp-config.php 文件中,例如 AUTH_KEYSECURE_AUTH_KEYLOGGED_IN_KEYNONCE_KEY 和相应的 AUTH_SALTSECURE_AUTH_SALTLOGGED_IN_SALTNONCE_SALT

9. 安全性分析

wp_rest_auth_cookie 验证机制的安全性依赖于以下几个因素:

  • 密钥的保密性: AUTH_KEYSECURE_AUTH_KEY 等密钥必须保密,不能泄露给他人。
  • HMAC 算法的强度: HMAC-MD5 算法虽然不是最强的哈希算法,但在密钥足够安全的前提下,可以提供一定的安全性。
  • hash_equals() 函数的防时序攻击能力: hash_equals() 函数可以防止时序攻击,避免攻击者通过测量哈希比较的时间来猜测密钥。
  • HTTPS 协议的使用: 使用 HTTPS 协议可以防止 Cookie 被中间人窃取。

10. 适用场景

wp_rest_auth_cookie 验证机制适用于以下场景:

  • WordPress 站点内部的 REST API 调用: 例如 Gutenberg 编辑器、后台管理界面等等。
  • 需要用户登录才能访问的 REST API 接口。
  • 不需要高安全性的 REST API 接口。

对于需要高安全性的 REST API 接口,建议使用 OAuth 2.0 或 JWT 等更安全的认证方式。

11. 代码示例:如何使用 wp_rest_auth_cookie 进行认证

在 JavaScript 中,可以使用以下代码发送带有 Cookie 的 REST API 请求:

fetch( '/wp-json/my-plugin/v1/my-endpoint', {
  credentials: 'include' // 关键:包含 Cookie
})
.then( response => response.json() )
.then( data => {
  console.log( data );
});

关键在于 credentials: 'include' 选项。这个选项告诉浏览器在发送请求时包含 Cookie。

在 PHP 中,可以使用以下代码检查用户是否通过 Cookie 认证:

add_action( 'rest_api_init', function () {
  register_rest_route( 'my-plugin/v1', '/my-endpoint', array(
    'methods'  => 'GET',
    'callback' => 'my_endpoint_callback',
    'permission_callback' => function () {
      return is_user_logged_in(); // 关键:检查用户是否登录
    }
  ) );
});

function my_endpoint_callback( WP_REST_Request $request ) {
  // 处理请求
  return array( 'message' => 'Hello, ' . wp_get_current_user()->user_login );
}

关键在于 permission_callback 函数。这个函数使用 is_user_logged_in() 函数检查用户是否登录。

12. 表格:wp_rest_auth_cookie 验证机制的关键函数和常量

函数/常量 作用 文件
rest_cookie_check_errors() REST API 请求进行 Cookie 认证的入口点 wp-includes/rest-api/class-wp-rest-server.php
is_user_logged_in() 检查用户是否已经登录 wp-includes/pluggable.php
wp_get_current_user() 获取当前用户对象 wp-includes/pluggable.php
wp_get_current_user_old() 从 Cookie 中提取用户信息并验证其有效性 wp-includes/pluggable.php
wp_set_auth_cookie() 设置登录 Cookie wp-includes/pluggable.php
wp_hash() 生成密钥 wp-includes/pluggable.php
wp_salt() 生成盐值 wp-includes/pluggable.php
LOGGED_IN_COOKIE 存储登录信息的 Cookie 名称 wp-config.php
AUTH_KEY 用于生成密钥的常量 wp-config.php
SECURE_AUTH_KEY 用于生成密钥的常量(用于 HTTPS 请求) wp-config.php

安全性与适用性: 选择合适的认证方式

我们深入剖析了 wp_rest_auth_cookie 验证机制的原理和实现,了解了它的优缺点以及适用场景。选择合适的认证方式,才能保证 WordPress REST API 的安全性和可用性。

发表回复

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