WordPress源码深度解析之:`WordPress`的`REST API`:`wp-includes/rest-api.php`中的路由注册与权限验证。

各位听众,大家好!今天咱们来聊聊WordPress的REST API,特别是wp-includes/rest-api.php这个核心文件中的路由注册和权限验证。这玩意儿就像WordPress的大门,你得知道怎么开门进屋,才能跟它好好玩耍。

开场白:REST API是啥?跟WordPress有啥关系?

简单来说,REST API就是一套规则,让不同的程序(比如你的手机APP、前端框架、或者其他网站)能够通过网络来访问和操作WordPress里的数据。想象一下,你不用登录WordPress后台,就能用代码发篇文章、改个标题,是不是很酷?

WordPress REST API让WordPress不仅仅是个博客系统,而是一个可以被各种应用利用的数据平台。

主角登场:wp-includes/rest-api.php

这个文件是WordPress REST API的“启动器”。它负责初始化REST API,注册默认的路由,以及加载其他的REST API控制器。你可以把它想象成一个总指挥,负责安排各个“演员”(控制器)出场。

第一幕:路由注册(Routing)—— 指挥交通的关键

路由,说白了,就是URL和处理函数之间的对应关系。当用户访问某个特定的URL时,WordPress就知道该调用哪个函数来处理请求。

wp-includes/rest-api.php中,rest_api_init钩子是路由注册的核心。WordPress会在init钩子之后,调用rest_api_init,让你有机会注册自己的路由。

add_action( 'rest_api_init', 'my_register_custom_routes' );

function my_register_custom_routes() {
    // 注册一个路由,用于获取特定作者的文章
    register_rest_route(
        'my-plugin/v1', // 命名空间,类似于模块的名字
        '/authors/(?P<id>d+)/posts', // 路由,支持正则表达式
        array(
            'methods'  => 'GET', // 请求方法,可以是GET, POST, PUT, DELETE等
            'callback' => 'my_get_author_posts', // 回调函数,用于处理请求
            'args'     => array(
                'id' => array(
                    'validate_callback' => 'is_numeric', // 参数验证,确保id是数字
                    'sanitize_callback' => 'absint', // 参数清理,转换为绝对整数
                ),
            ),
            'permission_callback' => 'my_check_permission' //权限校验
        )
    );
}

function my_get_author_posts( $request ) {
    $author_id = $request['id'];

    $args = array(
        'author' => $author_id,
    );

    $posts = get_posts( $args );

    if ( empty( $posts ) ) {
        return new WP_Error( 'no_posts', 'No posts found for this author.', array( 'status' => 404 ) );
    }

    $data = array();
    foreach ($posts as $post) {
        $data[] = array(
            'id' => $post->ID,
            'title' => $post->post_title,
            'content' => $post->post_content,
        );
    }

    return rest_ensure_response( $data );
}

function my_check_permission( $request ) {
    // 只有登录用户才能访问
    if ( ! is_user_logged_in() ) {
        return new WP_Error( 'rest_forbidden', 'You must be logged in to access this resource.', array( 'status' => 401 ) );
    }

    return true;
}

代码解读:

  • register_rest_route(): 这个函数是注册路由的关键。

    • 'my-plugin/v1': 命名空间,避免不同插件的路由冲突。建议使用插件名/版本号的格式。
    • '/authors/(?P<id>d+)/posts': 路由的URL。(?P<id>d+)是一个正则表达式,用于匹配作者ID,并将其作为参数传递给回调函数。
    • array(...): 包含路由的详细信息,如请求方法、回调函数、参数验证等。
    • 'methods' => 'GET': 指定允许的HTTP方法。
    • 'callback' => 'my_get_author_posts': 指定处理请求的回调函数。
    • 'args': 定义路由参数,并进行验证和清理。
      • 'validate_callback': 验证参数是否符合预期。
      • 'sanitize_callback': 清理参数,防止安全问题。
    • 'permission_callback' => 'my_check_permission': 权限校验回调函数。
  • my_get_author_posts( $request ): 回调函数,负责处理请求,并返回数据。

    • $request: 包含了请求的所有信息,包括参数、请求头等。
    • rest_ensure_response(): 确保返回的数据是一个WP_REST_Response对象,这是REST API的标准响应格式。
    • WP_Error: 用于返回错误信息。
  • my_check_permission( $request ): 权限校验回调函数。

命名空间(Namespace):规划你的地盘

命名空间就像是你在WordPress REST API里的地盘。使用正确的命名空间,可以避免不同插件之间的路由冲突。通常,命名空间的格式是插件名/版本号,例如my-plugin/v1

正则表达式:路由的精确匹配

正则表达式让你可以定义更复杂的路由规则。比如,你可以用正则表达式匹配不同格式的日期、ID等。

HTTP方法:GET, POST, PUT, DELETE

REST API支持多种HTTP方法,每种方法都有不同的含义:

HTTP方法 含义 例子
GET 获取资源。通常用于读取数据,不应该修改服务器上的数据。 获取文章列表、获取特定文章的内容。
POST 创建资源。通常用于提交表单、创建新的文章等。 创建一篇新的文章。
PUT 更新资源。通常用于完全替换一个已存在的资源。 完整替换一篇已存在的文章的内容。
DELETE 删除资源。通常用于删除文章、评论等。 删除一篇文章。
PATCH 部分更新资源。通常用于只修改资源的部分字段,而不是完全替换。与PUT的区别是PATCH只需要传递需要修改的字段,而PUT需要传递所有字段。虽然REST规范建议使用PATCH,但WordPress REST API核心并没有完全实现PATCH方法,通常还是使用PUT。 只修改文章的标题,而不修改内容。

参数验证和清理:安全第一

在处理用户传递的参数之前,务必进行验证和清理。这可以防止SQL注入、XSS攻击等安全问题。

  • validate_callback: 验证参数是否符合预期。例如,你可以用is_numeric()函数验证参数是否是数字。
  • sanitize_callback: 清理参数,去除不安全的字符。例如,你可以用absint()函数将参数转换为绝对整数。sanitize_text_field 函数可以用来清理文本,移除HTML标签和编码特殊字符。

第二幕:权限验证(Authentication & Authorization)—— 守门神的职责

权限验证是REST API安全的关键。你需要确保只有经过授权的用户才能访问和操作数据。

WordPress REST API提供了多种权限验证方式:

  • Cookies: 如果你是从WordPress网站的前端发送请求,通常可以使用Cookies进行权限验证。用户登录后,WordPress会在Cookie中存储用户的登录信息。
  • Nonce: Nonce是一种一次性使用的令牌,可以防止CSRF攻击。
  • OAuth: OAuth是一种更安全的权限验证方式,允许第三方应用在用户授权的情况下访问WordPress API。
  • JWT (JSON Web Tokens): JWT 是一种基于标准的、自包含的、安全的传输JSON对象的开放标准。可以用于授权和信息交换。

permission_callback:权限验证的核心

在注册路由时,你可以指定一个permission_callback函数,用于进行权限验证。这个函数接收一个WP_REST_Request对象作为参数,并返回true表示允许访问,返回WP_Error对象或false表示拒绝访问。

function my_check_permission( $request ) {
    // 只有管理员才能访问
    if ( ! current_user_can( 'manage_options' ) ) {
        return new WP_Error( 'rest_forbidden', 'You do not have permission to access this resource.', array( 'status' => 401 ) );
    }

    return true;
}

代码解读:

  • current_user_can( 'manage_options' ): 这个函数用于检查当前用户是否具有manage_options权限,这是管理员的权限。
  • WP_Error: 用于返回错误信息。status字段指定了HTTP状态码,401表示未授权。

常用的权限验证函数:

函数 含义
is_user_logged_in() 检查用户是否已登录。
current_user_can( $capability ) 检查当前用户是否具有指定的权限。
wp_verify_nonce( $nonce, $action ) 验证Nonce是否有效,用于防止CSRF攻击。
apply_filters( 'rest_authentication_errors', $result ) 允许插件修改权限验证结果。

第三幕:REST API Controller—— 控制器的职责

虽然我们可以直接在rest_api_init钩子中注册路由和编写回调函数,但更好的做法是使用REST API Controller。Controller可以将路由、权限验证、数据处理等逻辑封装在一起,使代码更易于维护和扩展。

WordPress 核心已经提供了很多Controller,比如:WP_REST_Posts_ControllerWP_REST_Users_ControllerWP_REST_Terms_Controller

自定义Controller的步骤:

  1. 创建一个类,继承WP_REST_Controller
  2. 定义register_routes()方法,用于注册路由。
  3. 定义处理请求的方法,如get_item()create_item()update_item()delete_item()
  4. 注册Controller。
<?php
/**
 * My Custom Post Type Controller
 */
class My_REST_CPT_Controller extends WP_REST_Controller {

    /**
     * 命名空间和版本
     *
     * @var string
     */
    protected $namespace = 'my-plugin/v1';

    /**
     * 路由名称
     *
     * @var string
     */
    protected $rest_base = 'my-cpts';

    /**
     * 注册路由
     *
     * @return void
     */
    public function register_routes() {

        register_rest_route( $this->namespace, '/' . $this->rest_base, array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array( $this, 'get_items' ),
                'permission_callback' => array( $this, 'get_items_permissions_check' ),
                'args'                => array(),
            ),
            array(
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => array( $this, 'create_item' ),
                'permission_callback' => array( $this, 'create_item_permissions_check' ),
                'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
            ),
        ) );

        register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[d]+)', array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array( $this, 'get_item' ),
                'permission_callback' => array( $this, 'get_item_permissions_check' ),
                'args'                => array(
                    'context' => array(
                        'default' => 'view',
                    ),
                ),
            ),
            array(
                'methods'             => WP_REST_Server::EDITABLE,
                'callback'            => array( $this, 'update_item' ),
                'permission_callback' => array( $this, 'update_item_permissions_check' ),
                'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
            ),
            array(
                'methods'             => WP_REST_Server::DELETABLE,
                'callback'            => array( $this, 'delete_item' ),
                'permission_callback' => array( $this, 'delete_item_permissions_check' ),
                'args'                => array(
                    'force' => array(
                        'default' => false,
                    ),
                ),
            ),
        ) );

        register_rest_route( $this->namespace, '/' . $this->rest_base . '/schema', array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array( $this, 'get_public_item_schema' ),
            ),
        ) );
    }

    /**
     * 获取项目集合
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error
     */
    public function get_items( $request ) {
        $data = array();
        // 这里替换成你自己的数据获取逻辑
        for ( $i = 1; $i < 2; $i++ ) {
            $itemdata = $this->prepare_item_for_response( (object)array('id' => $i, 'title' => "Item {$i}"), $request );
            $data[] = $this->prepare_response_for_collection( $itemdata );
        }

        // return a collection of response objects
        return new WP_REST_Response( $data, 200 );
    }

    /**
     * 获取单个项目
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error
     */
    public function get_item( $request ) {
        $item = array();
        $params = $request->get_params();
        $id = (int) $params['id'];
        $item['id'] = $id;
        $item['title'] = "Item {$id}";
        // 这里替换成你自己的数据获取逻辑
        $data = $this->prepare_item_for_response( (object)$item, $request );

        // return a response or error based on some conditional
        if ( 1 == 1 ) {
            return new WP_REST_Response( $data, 200 );
        } else {
            return new WP_Error( 'code', __( 'message', 'text-domain' ) );
        }
    }

    /**
     * 创建项目
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error
     */
    public function create_item( $request ) {

        $item = $this->prepare_item_for_database( $request );

        if ( is_wp_error( $item ) ) {
            return $item;
        }
        // 这里替换成你自己的数据处理逻辑
        $item['id'] = rand(10,100);
        $item['title'] = "Item {$item['id']}";

        $data = $this->prepare_item_for_response( (object)$item, $request );

        return new WP_REST_Response( $data, 200 );
    }

    /**
     * 更新项目
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error
     */
    public function update_item( $request ) {
        $item = array();
        $params = $request->get_params();
        $id = (int) $params['id'];
        $item['id'] = $id;
        $item['title'] = "Item {$id} - Updated";
        // 这里替换成你自己的数据处理逻辑

        $data = $this->prepare_item_for_response( (object)$item, $request );

        return new WP_REST_Response( $data, 200 );
    }

    /**
     * 删除项目
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error
     */
    public function delete_item( $request ) {
        $item = array();
        $params = $request->get_params();
        $id = (int) $params['id'];
        $item['id'] = $id;
        $item['title'] = "Item {$id} - Deleted";
        // 这里替换成你自己的数据处理逻辑

        $data = $this->prepare_item_for_response( (object)$item, $request );

        return new WP_REST_Response( $data, 200 );
    }

    /**
     * 检查是否有获取多个项目的权限
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
     */
    public function get_items_permissions_check( $request ) {
         if ( ! current_user_can( 'read' ) ) {
            return new WP_Error( 'rest_forbidden', __( 'You cannot view the resource.', 'my-text-domain' ), array( 'status' => $this->authorization_status_code() ) );
         }
        return true;
    }

    /**
     * 检查是否有获取单个项目的权限
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
     */
    public function get_item_permissions_check( $request ) {
        return $this->get_items_permissions_check( $request );
    }

    /**
     * 检查是否有创建项目的权限
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
     */
    public function create_item_permissions_check( $request ) {
        if ( ! current_user_can( 'edit_posts' ) ) {
            return new WP_Error( 'rest_forbidden', __( 'You cannot create the resource.', 'my-text-domain' ), array( 'status' => $this->authorization_status_code() ) );
        }
        return true;
    }

    /**
     * 检查是否有更新项目的权限
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
     */
    public function update_item_permissions_check( $request ) {
        if ( ! current_user_can( 'edit_posts' ) ) {
            return new WP_Error( 'rest_forbidden', __( 'You cannot update the resource.', 'my-text-domain' ), array( 'status' => $this->authorization_status_code() ) );
        }
        return true;
    }

        /**
     * 检查是否有删除项目的权限
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
     */
    public function delete_item_permissions_check( $request ) {
        if ( ! current_user_can( 'delete_posts' ) ) {
            return new WP_Error( 'rest_forbidden', __( 'You cannot delete the resource.', 'my-text-domain' ), array( 'status' => $this->authorization_status_code() ) );
        }
        return true;
    }

    /**
     * 准备数据库项目
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return array|WP_Error
     */
    protected function prepare_item_for_database( $request ) {

        $params = $request->get_params();

        $prepared_item = array();

        if ( isset( $params['title'] ) ) {
            if ( is_string( $params['title'] ) ) {
                $prepared_item['title'] = $params['title'];
            } else {
                return new WP_Error( 'rest_invalid_type', __( 'Title must be a string.', 'my-text-domain' ), array( 'status' => 400 ) );
            }
        }
        return $prepared_item;
    }

    /**
     * 准备项目以进行响应
     *
     * @param mixed $item WordPress representation of the item.
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response Returns a JSON-compatible representation of the item.
     */
    public function prepare_item_for_response( $item, $request ) {

        $data = array(
            'id'          => absint( $item->id ),
            'title'       => $item->title,
        );

        $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
        $data = $this->add_additional_fields_to_object( $data, $request );
        $data = $this->filter_response_by_context( $data, $context );

        // Wrap the data in a response object.
        return rest_ensure_response( $data );
    }

    /**
     * 获取项目模式,用于参数验证
     *
     * @return array
     */
    public function get_item_schema() {
        if ( $this->schema ) {
            return $this->add_additional_fields_schema( $this->schema );
        }

        $schema = array(
            // This tells the spec of JSON Schema we are using.
            '$schema'              => 'http://json-schema.org/draft-04/schema#',
            // The title property defines a human-readable title for the schema.
            'title'                => 'my-cpt',
            // The type property defines the data type.
            'type'                 => 'object',
            // In addition to the standard JSON Schema defined properties, the schema may use semantic properties from the WordPress API.
            'context'              => array(
                'description' => __( 'Scope under which the request is made; determines fields present in response.', 'my-text-domain' ),
                'type'        => 'string',
                'enum'        => array( 'view', 'edit', 'embed' ),
                'arg_options' => array(
                    'sanitize_callback' => 'sanitize_key',
                    'validate_callback' => 'rest_validate_request_arg',
                ),
            ),
            // Properties available in the schema.
            'properties'           => array(
                'id'          => array(
                    'description' => __( 'Unique identifier for the object.', 'my-text-domain' ),
                    'type'        => 'integer',
                    'context'     => array( 'view', 'edit', 'embed' ),
                    'readonly'    => true,
                ),
                'title'       => array(
                    'description' => __( 'The title for the object.', 'my-text-domain' ),
                    'type'        => 'string',
                    'context'     => array( 'view', 'edit', 'embed' ),
                ),
            ),
        );
        $this->schema = $schema;
        return $this->add_additional_fields_schema( $this->schema );
    }

    /**
     * 获取授权状态码
     *
     * @return int
     */
    public function authorization_status_code() {

        if ( is_user_logged_in() ) {
            return 403;
        }

        return 401;
    }

}
add_action( 'rest_api_init', function () {
  $controller = new My_REST_CPT_Controller();
  $controller->register_routes();
} );

代码解读:

  • My_REST_CPT_Controller: 自定义的Controller类,继承自WP_REST_Controller
  • $namespace$rest_base: 定义了命名空间和路由的基础URL。
  • register_routes(): 注册路由的方法。
  • get_items()get_item()create_item()update_item()delete_item(): 处理不同HTTP请求的方法。
  • prepare_item_for_response(): 格式化返回的数据。
  • get_item_schema(): 返回用于参数验证的JSON Schema。
  • 权限校验函数: get_items_permissions_checkget_item_permissions_checkcreate_item_permissions_checkupdate_item_permissions_checkdelete_item_permissions_check

JSON Schema:数据校验的利器

JSON Schema是一种描述JSON数据结构的规范。你可以用JSON Schema定义数据的类型、格式、范围等,用于验证用户提交的数据是否有效。

总结:

今天我们一起深入了解了WordPress REST API中的路由注册和权限验证。掌握了这些知识,你就可以构建自己的REST API,让WordPress与各种应用无缝连接。记住,安全第一,一定要做好参数验证和权限验证。

希望今天的讲解对大家有所帮助!下次再见!

发表回复

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