💬

WordPressでログイン認証を利用したカスタムエンドポイントを作成する方法

2023/08/22に公開

はじめに

WordPressサイトの様々なデータをREST APIを通して取得する方法がある。
APIを作成する際に、認証に関わる部分で少し躓いたことがあったので、備忘録的に残しておく

まとめ

REST APIを独自に定義する方法

  • WordPressでは register_rest_route を利用して独自REST APIが作成できる

  • 作成するためのregister_rest_route周辺のコード例

    add_action('rest_api_init', function() {
      register_rest_route( 'myapi/v1', '/test1', [
          'methods' => 'GET',
          'callback' => 'func_test1',
          'permission_callback' => function() { return true; }
      ]);
    });
    function func_test1() {
      // ここに何らかの処理
      $return['title']='title';
      $return['body']='body';  
      return new WP_REST_Response( $return, 200 );
    }
    
  • この場合、例えば、ローカルにおいてポート18888で起動している場合、 http://localhost:18888/wp-json/myapi/v1/test1 をコールすると結果が返る

ログイン情報を利用する方法

  • WordPressでREST APIを利用するにあたり、Cookie認証の仕組みを利用することで、ログインユーザのみが利用できるようなケースを実装することができる。
    • テーマやプラグイン内ではなく、デスクトップアプリやWEBアプリなど、サイトの外からアクセスする場合にはOAuth認証・アプリケーションパスワード・ベーシック認証が適切
  • ただし、CSRFを避けるためnonceが必要
    • nonceはリプレイ攻撃などにより、認証情報を含めて丸っと同じリクエストを投げられた場合などにユーザが意図しないリクエストを発生させてしまうことを避けるたびに、リクエストのたびにユニークIDを発行し、同じエンドポイントかつ同じ値のリクエストはサーバ側で拒否するような形にすることで不正を防ぐことができる
    • ビルトインのAPI(wp.api.xxx)を利用もしくは継承する場合には、nonceの指定は不要となる。なぜなら処理が組み込まれているため。
    • ビルトインのAPIを利用もしくは継承のいずれもしない場合には、X-WP-Nonceヘッダなどを活用してnonceを入力する必要がある
  • このセクションでは、ミニマムなサンプルを紹介する
  • 前提
  • Localhost環境で動作
  • 実現したいこと
  • 前準備
  • wp-envでローカルのWordPress環境を作成

  • ポートは18888にセット

  • 記事を2件作成しておく

    • 公開状態の記事
    • 下書き状態の記事
  • パーマリンク設定でstructureをPost nameにする

  • rest apiから値が取得できることの確認

    $ curl --location 'http://localhost:18888/wp-json/wp/v2/posts'
    [{"id":5,"date":"2023-03-09T00:25:40",...]
    
  • header, footer, front-page.phpを作成

    • header

      <!DOCTYPE html>
      <html <?php language_attributes(); ?>>
      
      <head>
          <meta charset="<?php bloginfo('charset'); ?>">
          <meta name="description" content="<?php bloginfo('description'); ?>">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <meta http-equiv="X-UA-Compatible" content="ie=edge">
          <link rel="canonical" href="<?php bloginfo('url'); ?>">
          <?php wp_head(); ?>
      </head>
      
      <body <?php body_class(); ?>>
      
    • footer

      <?php wp_footer(); ?>
          </body>
      
          </html>
      
    • front-page.php

      <?php get_header(); ?>
      
      <div id="result"></div>
      
      <?php get_footer(); ?>
      
  • 実現方法
  • functions.php

    <?php
    
    //管理画面用のcssとjsを読み込む
    function load_assets($hook)
    {
        // nonceを作成
        $apiArgs = [
            'root' => esc_url_raw(rest_url()), //APIのルートURLが入ってます
            'nonce' => wp_create_nonce('wp_rest') //nonceの名前は「wp_rest」にします
        ];
    
        // jsのロード
        // wp_enqueue_script( 'master-js', plugins_url('master.js', __FILE__), ['jquery'], '1.0');
        wp_enqueue_script('asset', get_theme_file_uri('js/main.js'), array(), false, true);
    
        // php側で取得した値をjs側へ引き渡す
        wp_localize_script('asset', 'WP_API_Settings', $apiArgs);
    }
    add_action('wp_enqueue_scripts', 'load_assets');
    
    function getPostdata(WP_REST_Request $request)
    {
    
        $args;
        $nonceResult = wp_verify_nonce($_SERVER['HTTP_X_WP_NONCE'], 'wp_rest');
    
        if ($nonceResult && is_user_logged_in()) {
        if ( is_user_logged_in()) {
            $args = array(
                'post_type' => 'post',
                'post_status' => 'draft,publish',
            );
        } else {
            $args = array(
                'post_type' => 'post',
                'post_status' => 'publish',
            );
        }
    
        $the_query = get_posts($args);
    
        foreach ($the_query as $post) {
            $news_category = get_the_terms($post->ID, 'category name');
        }
    
        $the_query = new WP_Query($args);
        $data = array();
    
        while ($the_query->have_posts()) {
            $the_query->the_post();
    
            $data[] = array(
                'id' => get_the_ID(),
                'date' => get_the_date('Y/m/d', get_the_ID()),
                'title' => get_the_title(),
                'status' => get_post_status(),
                'categories' => $news_category,
                'tags' => get_the_tags(),
                'thumbnail' => get_the_post_thumbnail_url(get_the_ID(), 'full'),
                'content' => get_the_content(),
                'itemName' => get_post_meta(get_the_ID(), 'itemName', true),
                'price' => get_post_meta(get_the_ID(), 'price', true),
                'note' => get_post_meta(get_the_ID(), 'note', true),
            );
        }
    
        $response = new WP_REST_Response($data); // HTTPステータスやリクエストヘッダーを変更
        $response->set_status(200);
        return $response;
    }
    function add_custom_endpoint()
    {
        register_rest_route('custom/v1', '/getPostdata', array(
            'methods' => 'GET',
            'callback' => 'getPostdata',
        ));
    }
    add_action('rest_api_init', 'add_custom_endpoint');
    
  • main.js

const nonce = WP_API_Settings.nonce;
const origin = new URL(location.href).origin;
let path = '/wp-json/custom/v1/getPostdata/';
const url = origin + path;

const main = async () => {
  if (WP_API_Settings.nonce) {
    console.log('has nonce')
  } else {
    path = '/wp-json/wp/v2/posts';
    console.log('no nonce')
  }

  console.log(`WP_API_Settings.nonce: ${WP_API_Settings.nonce}`)

  const apiGet = await fetch(url, { 
    headers: {
      'X-WP-Nonce': nonce
    }
  })
  .then( res => res.json() )
  .then( json => json )

  document.querySelector('#result').innerHTML = JSON.stringify(apiGet);
}

main();

参考

Discussion