WordPress Logo WordPress ユーザーの一覧表示対策

更新日:2025年04月24日

作成日:2019年01月18日

ユーザー情報が漏れるリスク

WordPressでは、例えば「サイトのURL/?author=1」などのURLにアクセスすることで、投稿者のユーザー名(ログインID)が閲覧者に知られてしまう可能性があります。

表示されるのは厳密には「user_nicename」という項目で、「user_login」(ログイン時に使用するID)とは異なる項目ですが、初期設定のままだと「user_login」がそのまま「user_nicename」として使用されているため、実質的にログインIDが外部に判明してしまうケースが少なくありません。

また、WP REST API がデフォルトで有効化されているため、認証なしでもユーザー情報などのデータが外部から取得できる状態になっています。

ユーザー名が知られるだけで即座に大きな被害が出るわけではありませんが、ブルートフォース攻撃や辞書攻撃の成功率を高める要因になります。

そのため、複雑なパスワードの設定や、ログイン試行制限の導入(例:Limit Login Attempts Reloaded プラグイン)など、一覧表示対策以外の基本的なセキュリティ対策と組み合わせることが重要です。

author アーカイブによるリスク

WordPress では、投稿者ごとに自動的に「著者(投稿者)アーカイブ(author archive)」が生成されます。そのため、「サイトのURL/?author=1」のような URL にアクセスすると、自動的に「サイトのURL/author/ユーザー名/」といった著者アーカイブページにリダイレクトされる仕様になっています。

ここで表示される「ユーザー名」は、正確には WordPress のユーザー情報の「user_nicename」という項目です。これは著者アーカイブのスラッグとして使用され、「user_login」(ログイン時に使う ID)とは異なるものです。但し、ユーザー作成時の初期設定では「user_nicename」に「user_login」と同じ値が自動的に設定されるため、変更を行っていない場合は、実質的にログイン ID がそのまま外部に公開されてしまうケースが多くなっています。

なお、「?author=1」によって表示されるのは、通常、WordPressインストール時に作成されたID=1の管理者ユーザーのアーカイブページであるため、特に注意が必要です。

また、プロフィール設定で「ニックネーム」や「ブログ上の表示名」を変更しても、「user_nicename」には影響しない(変更されない)ため、著者アーカイブの URL からログイン ID を推測されるリスクは残ったままとなります。

※ 基本的なことですが、プロフィール設定では「ニックネーム」を設定し、「ブログ上の表示名」をニックネームなどログインID 以外に変更します。

対策方法

以下のいずれかの(または両方の)方法を導入することで、author アーカイブによるリスクの基本的な対策が可能です。WP REST API に対しては必要に応じて REST API を無効にするなど別途対応します。

  • 著者アーカイブページへのアクセスを制御する
  • user_nicename をログイン ID(user_login)とは異なるものに変更する

著者アーカイブページへのアクセスを制御する場合、以下の2つのケースが考えられます。

著者アーカイブページが必要ない
「?author=1」も著者アーカイブページ(/author/username/)も両方ブロック(完全に非公開にする)
著者アーカイブページを公開する
「?author=1」のようなアクセスはブロックし、著者アーカイブページ(/author/username/)は公開

著者アーカイブページが必要ない場合

著者アーカイブページを公開する必要がない場合は、template_redirect フックで条件分岐タグ is_author() を使って著者アーカイブページへのアクセスがあるとホームや 404ページなどにリダイレクトさせます。

以下を functions.php に記述します。

404ページにリダイレクトさせるには、home_url() の部分を home_url('/404') とします。

function disable_author_archive() {
  if (is_author()) {
    // ホームにリダイレクト(必要に応じて home_url('/404') で404 ページにリダイレクト)
    wp_safe_redirect(home_url(), 301);
    // または wp_redirect(home_url());
    exit;
  }
}
add_action('template_redirect', 'disable_author_archive');

特徴:

  • 著者アーカイブページかどうかを WordPress の条件分岐タグ is_author() で判定
  • クエリ形式・パーマリンク形式のどちらにも対応
  • 柔軟でメンテナンスしやすい
  • template_redirect フックなので、比較的遅いタイミング(クエリ処理後)で実行される

この例の場合、リダイレクト先は home_url() を使っているので(自サイト内なので)、 wp_redirect() を使うこともできますが、wp_safe_redirect() を使ったほうがより安心です。

ステータスコード 301 を指定する場合の注意点

wp_safe_redirect() や wp_redirect() の第2引数にはステータスコードを指定することができ、この例では 301 (Moved Permanently) を指定しています。省略時は 302 (Found 一時的な移動)になります。

但し、301リダイレクトは「恒久的」な移動を意味するので、ブラウザは次回以降はサーバーに確認せずキャッシュされたリダイレクト結果を使うことがあります

そのため、テスト中は 302(Found) を使い(ブラウザキャッシュされにくい)、テストが終わって仕様確定したら、最後に 301 に切り替えるのが無難です。

結果が期待通りでない場合は、キャッシュ削除(履歴削除)する必要があるかも知れません。

ログインユーザーにはアクセスを許可

ログインユーザーは許可し、未ログイン者だけ制限する場合は、以下のようになります。

function disable_author_archive() {
  if (is_author() && !is_user_logged_in()) {
    wp_safe_redirect(home_url(), 301);
    exit;
  }
}
add_action('template_redirect', 'disable_author_archive');
特定の著者ページのみ公開

以下は特定の著者ページのみ公開する(アクセスを許可する)場合の例です。

以下は /author/username/ にアクセスがあると、もしその著者のIDが $allowed_author_id でなければホームにリダイレクトし、許可した著者だけ、そのままページが表示されます。

但し、「user_nicename」がログイン ID(user_login)と同じ場合(初期状態のデフォルトの場合)、ログイン ID が公開されることになるので注意が必要です

必要に応じて、user_nicename をログイン ID とは異なるものに変更します。

function disable_author_archive() {
  if (is_author()) {
    $allowed_author_id = 2; // ここに許可したい著者のユーザーIDを指定

    $author_id = get_queried_object_id(); // 現在表示中の著者のIDを取得

    if ($author_id != $allowed_author_id) {
      wp_safe_redirect(home_url(), 301);
      exit;
    }
  }
}
add_action('template_redirect', 'disable_author_archive');

ユーザーIDではなく、ユーザー名を指定する場合は以下のようにします。

function disable_author_archive() {
  if (is_author()) {
    $allowed_user_name = 'foo';  // 許可したいユーザー名(例: foo)を設定
    $author_name = get_queried_object()->user_nicename; // 現在のユーザー名を取得

    if ($author_name != $allowed_user_name) {
      wp_safe_redirect(home_url(), 301);
      exit;
    }
  }
}
add_action('template_redirect', 'disable_author_archive');

複数の著者ページを許可

複数の著者ページを許可したい場合(例:IDが 2 と 5 )、は以下のように記述できます。

function disable_author_archive() {
  if (is_author()) {
    $allowed_author_ids = [2, 5]; // 許可したいユーザーIDを配列で指定

    $author_id = get_queried_object_id();

    if (!in_array($author_id, $allowed_author_ids, true)) {
      wp_safe_redirect(home_url(), 301);
      exit;
    }
  }
}
add_action('template_redirect', 'disable_author_archive');

著者アーカイブページを公開する場合

著者アーカイブページを公開する場合は、以下のような方法があります。

ID列挙によるユーザー名露出は防ぎたいが、著者アーカイブページ(/author/username/)自体は公開したい場合に適しています。

  • クエリパラメータ(author=xxx)を init アクションで判定する
  • redirect_canonical フィルターを使用する
  • .htaccess を利用する

著者アーカイブページを公開する場合、/author/username/が公開される以上、URL からユーザー名(user_nicename)が推測できるリスクはゼロにはできませんが、少なくとも、?author=1 のID列挙攻撃は防げます。また、この場合、user_nicename をログイン ID とは異なるものに変更ことでリスクを小さくすることができます。

クエリパラメータ(author=xxx)を init アクションで判定する方法

init アクションフックで $_SERVER を使って初期リクエストチェックする例です。

以下の場合、?author=1 や ?author=foo のようなアクセスをリダイレクトします。数字の場合だけリダイレクトさせるには、author=([0-9]*) を author=([0-9]+) のように * を + に変更します。

function disable_author_archive_query() {
  if( preg_match('/author=([0-9]*)/i', $_SERVER['QUERY_STRING']) ){
    wp_safe_redirect( home_url(), 301 );
    exit;
  }
}
add_action('init', 'disable_author_archive_query');

特徴:

  • ?author=1 や ?author=foo のようなアクセスを init 時点で遮断(リダイレクト)
  • クエリ段階で早期に処理できるため、サーバー負荷軽減(軽量・パフォーマンス志向)
  • 著者アーカイブページ /author/スラッグ/ にはアクセス可能
redirect_canonical フィルターを使う方法

redirect_canonical フィルターと die() を使ってアクセスを遮断する例です。

if (!is_admin()) {
  // クエリ形式の遮断
  if (preg_match('/author=([0-9]*)/i', $_SERVER['QUERY_STRING'])) die();

  // パーマリンク形式(パーマリンク構造が「基本」の場合)の遮断
  add_filter('redirect_canonical', 'disable_author_permalink_access', 10, 2);
}

/**
 * パーマリンク形式 (?author=数字 もしくは ?author=数字/) のリクエストを遮断
 * @param string|false $redirect リダイレクト先のURL
 * @param string $request 元のリクエストURI
 * @return string|false リダイレクト先のURL、または false(リダイレクトしない)
 */
function disable_author_permalink_access($redirect, $request) {
  // リクエストに "?author=数字や文字列"(スラッシュ付きでもOK)が含まれていれば即終了
  if (preg_match('/\?author=([0-9]*)(\/*)/i', $request)) die();
  else return $redirect;
}

特徴:

  • ?author=1 や ?author=foo のようなアクセスを遮断
  • パーマリンク設定が「基本」に設定されている場合の /index.php?author=1 のようなアクセスも遮断
  • die() によって即時終了するため、情報露出を確実に防げる
  • ユーザー体験としてはやや粗い(何も表示されない→変更可能)
  • 著者アーカイブページ /author/スラッグ/ にはアクセス可能

上記の場合、author=1 や ?author=foo のようなアクセスがあると、die() によって即時終了するため 真っ白なページ(ブラウザの空白ページ)が表示されます。

以下は die() の代わりに wp_die() を使ってメッセージを表示する例です。

if (!is_admin()) {
  // クエリ形式の遮断
  if (preg_match('/author=([0-9]*)/i', $_SERVER['QUERY_STRING'])) {
    wp_die('アクセスはブロックされました。');
  }

  // パーマリンク形式の遮断
  add_filter('redirect_canonical', 'disable_author_permalink_access', 10, 2);
}

function disable_author_permalink_access($redirect, $request) {
  if (preg_match('/\?author=([0-9]*)(\/*)/i', $request)) {
    wp_die('アクセスはブロックされました。');
  } else {
    return $redirect;
  }
}

die() による遮断はセキュリティ重視のサイトや API 経由の処理には向いていますが、一般ユーザー向けサイトではリダイレクトの方が親切です。

以下は、 die() の代わりに 404ページにリダイレクトさせる例です。

if (!is_admin()) {
  // クエリ形式の遮断
  if (preg_match('/author=([0-9]*)/i', $_SERVER['QUERY_STRING'])) {
    wp_safe_redirect( home_url('/404') );
    exit;
  }

  // パーマリンク形式の遮断
  add_filter('redirect_canonical', 'disable_author_permalink_access', 10, 2);
}

function disable_author_permalink_access($redirect, $request) {
  if (preg_match('/\?author=([0-9]*)(\/*)/i', $request)) {
    wp_safe_redirect( home_url('/404') );
    exit;
  } else {
    return $redirect;
  }
}

注意点

完全に無言で止めたい場合(攻撃者にもヒントを与えたくない場合:例 セキュリティ重視のサイトや API 経由の処理)は、無言で die() の方がいいと考える場合もあります。

.htaccess を利用

Apache レベルでアクセスブロック・リダイレクトを行う方法です。WordPress に到達する前に制御できるため、より強力ですが、サーバーの設定(たとえば AllowOverride が有効かどうか)や、.htaccess の配置場所により、利用できない場合もあります。

以下を、WordPress のインストールディレクトリにある .htaccess ファイルに記述します。

この設定により、author=1 や author=foo のようなリクエストがあった場合、トップページ(または指定先)へリダイレクトされます。

https://example.com/ の部分は、自身のサイトのURLに置き換えてください。また、記述位置は「# BEGIN WordPress」より前にする必要があります。

なお、正規表現部分を author=([0-9]*) から author=([0-9]+) に変更することで、「数字が1文字以上存在する場合のみ」マッチし、より精密な条件でリダイレクトを行えます。

<IfModule mod_rewrite.c>
  RewriteCond %{QUERY_STRING} ^author=([0-9]*)
  RewriteRule .* https://example.com/? [L,R=301]
</IfModule> 

RewriteRule の https://example.com/? の最後の ? は元のリクエリパラメータ(?author=1 など)をリダイレクト先に引き継がず、消すための指定です。

相対パス(ホスト名に依存しない)で指定する場合は、RewriteRule .* /? [R=301,L] のようにします。

user_nicename をログイン ID とは異なるものに変更する

ユーザー作成時の初期設定では user_nicename に user_login(ログイン ID)と同じ値が自動的に設定されているため、user_nicename をログイン ID(user_login)とは異なるものにすることで、投稿者アーカイブの URL にログイン ID が表示されるリスクを回避できます。

但し、この操作は、ユーザー作成直後に変更するのが最も安全です。運用後に変更する場合は、既存のリンクは 404 になるので、リダイレクト対応なども検討する必要があります(SEO や既存リンク対策)。

user_nicename をログイン ID(user_login)と異なるものにするには以下のような方法があります。

  • データベースを直接編集
  • プラグインを利用
  • functions.php で変更
  • ユーザー編集画面で変更(functions.php に記述)
データベースを直接編集

phpMyAdmin などを使用してデータベースを直接編集して user_nicename を変更することができます。

wp_users テーブルにある user_nicename フィールドを編集します。

該当ユーザーの行を探し、user_nicename の値を任意の文字列(ログイン ID と異なる値)に変更します。

注意: URL 構造(example.com/author/スラッグ)にも影響するため、スラッグとして適切な文字列を設定する必要があります(半角英数字、ハイフン推奨)。

プラグインを利用

専用のユーザー編集プラグイン(例:Edit Author Slug)を使えば、user_nicename に相当する「著者スラッグ」を GUI から安全に変更できます。

この方法はデータベースを直接編集するよりも安全かつ便利です。

functions.php で変更

wp_update_user() 関数などを使って変更することも可能です。

以下は functions.php で wp_update_user() を使って、ユーザーID が 1 のユーザーの user_nicename を変更する例です。以下のコードは一度アクセスするだけで実行される(ページを開くたびに毎回実行される)ので、実行後は必ず add_action() をコメントアウトします。

user_nicename はスラッグ(URLに使う文字列)なので、記号などを含めるとトラブルの元になるため、必ず sanitize_title_with_dashes() sanitize_title() を通して英数字・ハイフンだけに整えます。

function safe_update_user_nicename_on_init() {
  $user_id = 1; // 対象ユーザーID
  $new_nicename = 'foo-public'; // 新しい user_nicename

  $slug = sanitize_title_with_dashes($new_nicename);

  // すでに同じスラッグが使われていないか確認
  $existing_user = get_user_by('slug', $slug);
  if ($existing_user && $existing_user->ID != $user_id) {
    error_log('すでに使われているスラッグのため、更新を中止しました。');
    return;
  }

  // 更新
  $result = wp_update_user([
    'ID' => $user_id,
    'user_nicename' => $slug
  ]);

  if (is_wp_error($result)) {
    error_log('ユーザー更新に失敗しました: ' . $result->get_error_message());
  } else {
    error_log('ユーザーID ' . $user_id . ' の user_nicename を ' . $slug . ' に更新しました。');
  }
}
// 実行が完了したら以下の add_action() をコメントアウト
add_action('init', 'safe_update_user_nicename_on_init');

確認

確認方法としては、phpMyAdmin などでデータベースを開いて、wp_users テーブルの user_nicename カラムの値を直接確認するのが確実です。

/author/{新しいuser_nicename} にアクセスして確認することもできます。新しい user_nicename で著者アーカイブが表示されれば成功です(旧スラッグでアクセスすると 404 にります)。

但し、サーバーやブラウザキャッシュにより、すぐに新しい user_nicename が反映されないこともあるため確認するときは、ブラウザキャッシュをクリアするか、シークレットモードでチェックすると確実です。

また、サーバーの error_log を確認することもできます(上記のコードでは成功でも失敗でもログを出力)。

error_log() により出力されるサーバーの error_log ファイルの場所は、環境によって異なります。例えば、MAMP/XAMPP(ローカル開発環境)の場合、以下にあります。

  • MAMP: /Applications/MAMP/logs/php_error.log
  • XAMPP: xampp/php/logs/php_error_log

また、開発環境の場合、WordPress 自体のデバッグ設定(wp-config.php)で以下が設定されていれば、/wp-content/debug.log に出力されます(※ 本番環境ではこれらは false にします)。

但し、WP_DEBUG_LOG が true になっている場合、WordPress が PHP エラーや error_log() の出力をキャッチし、wp-content/debug.log に記録するため、通常のサーバーの error_log(MAMP の場合は php_error.log など)には出力されません。

define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );

複数ユーザーの user_nicename 一括更新

以下は複数ユーザーの user_nicename 一括更新する例です。$users_to_update に、更新対象のユーザーIDと新しい user_nicename をセットします。

この場合も、前述のコード同様、実行後は必ず add_action() をコメントアウトします。

function bulk_update_user_nicenames() {
  // 更新したいユーザーのリストを用意(ユーザーID と新しい user_nicename を指定)
  $users_to_update = [
    // user_id => 新しい user_nicename
    2 => 'alice-public',
    3 => 'bob-public',
    4 => 'charlie-public',
    // 必要に応じて追加
  ];

  foreach ($users_to_update as $user_id => $new_nicename) {
    $slug = sanitize_title_with_dashes($new_nicename);

    // すでに同じスラッグが存在するか確認
    $existing_user = get_user_by('slug', $slug);

    if ($existing_user && $existing_user->ID != $user_id) {
      error_log("スラッグ '{$slug}' は既にユーザーID {$existing_user->ID} が使用しています。スキップしました。");
      continue;
    }

    // スラッグを更新
    $result = wp_update_user([
      'ID' => $user_id,
      'user_nicename' => $slug,
    ]);

    if (is_wp_error($result)) {
      error_log("ユーザーID {$user_id} の更新に失敗しました: " . $result->get_error_message());
    } else {
      error_log("ユーザーID {$user_id} の user_nicename を '{$slug}' に更新しました。");
    }
  }
}
// 実行が完了したら以下の add_action() をコメントアウト
add_action('init', 'bulk_update_user_nicenames');
ユーザー編集画面で変更

functions.php に以下を記述して、ユーザー編集画面で user_nicename を変更することもできます。

show_user_profile と edit_user_profile アクションでプロフィール画面に独自の nicename フィールドを追加し、personal_options_update と edit_user_profile_update で値を保存します。

プロフィール画面に独自の nicename フィールドを追加する際は、「ニックネーム」と「ブログ上の表示名」の間に表示するように、フィールドを追加後に JavaScript を使って、要素を移動しています。また、念の為、エラー表示のスタイルをクリアします。

保存の際は、指定された nicename が既存ユーザーと重複している場合は user_profile_update_errors アクションを使ってエラーを表示し(保存を中断して)、現在(これまで)の nicename を復元します。

/**
 * ユーザー編集画面に nicename フィールドを追加する
 *
 * @param WP_User $user 現在編集しているユーザーオブジェクト
 */

function my_add_nicename_field($user) {
  // ユーザーデータを取得
  $userdata = get_userdata($user->ID);
?>
  <table class="form-table" id="my-nicename-field">
    <tr>
      <th><label for="user_nicename">ナイスネーム(nicename)</label></th>
      <td>
        <input name="user_nicename" id="user_nicename" value="<?php echo esc_attr($userdata->user_nicename); ?>" class="regular-text" />
        <span class="description">ユーザー名と異なる値を指定</span>
      </td>
    </tr>
  </table>

  <script>
    document.addEventListener('DOMContentLoaded', () => {
      // 出力された nicename フィールドを、JavaScriptで「ニックネーム」と「ブログ上の表示名」の間に移動

      // ニックネーム(nickname)フィールドの input 要素を探す
      const nicknameInput = document.getElementById('nickname');
      if (!nicknameInput) return; // 見つからなければ終了

      // ニックネームフィールドの <tr> 要素を取得
      const nicknameRow = nicknameInput.closest('tr');

      // nicename フィールドの <tr> 要素を取得
      const nicenameRow = document.querySelector('#my-nicename-field tr');

      // ニックネームフィールドの直後に nicename フィールドを挿入
      if (nicknameRow && nicenameRow) {
        nicknameRow.parentNode.insertBefore(nicenameRow, nicknameRow.nextSibling);
      }

      // nicename フィールドのスタイルとエラーをリセット
      const nicenameInput = document.getElementById('user_nicename');
      if (nicenameInput) {
        nicenameInput.style.backgroundColor = ''; // 背景色をリセット
        nicenameInput.style.padding = '6px';
        const existingError = document.querySelector('.nicename-error-message');
        if (existingError) existingError.remove(); // もしエラーメッセージが残ってたら消す
      }
    });
  </script>
  <?php
}
// ユーザー自身がプロフィールを見る・編集する画面で nicename フィールドを表示
add_action('show_user_profile', 'my_add_nicename_field');
// 管理者が他人のプロフィールを編集する画面でも nicename フィールドを表示
add_action('edit_user_profile', 'my_add_nicename_field');


/**
 * ユーザーの nicename を保存・更新する
 *
 * @param int $user_id 保存対象のユーザーID
 */

function my_update_nicename($user_id) {
  // 現在のユーザーがこのユーザーを編集できるか確認
  if (! current_user_can('edit_user', $user_id)) {
    return false;
  }

  // nicename フィールドが送信されているかチェック
  if (! isset($_POST['user_nicename'])) {
    return;
  }

  // 入力された nicename を正規化(スラッグ化)
  $nicename = sanitize_title_with_dashes($_POST['user_nicename']);

  // すでに同じ nicename を持つ他のユーザーがいないかチェック
  $existing_user = get_user_by('slug', $nicename);

  // nicename が既存ユーザーと重複している場合はエラーを表示
  if ($existing_user && $existing_user->ID != $user_id) {
    // エラー登録 ( $errors->add() でエラーを登録し、エラーメッセージを出して、保存を止める)
    add_action('user_profile_update_errors', function ($errors) use ($nicename) {
      $errors->add(
        'user_nicename_exists',
        sprintf(
          'ナイスネーム「%s」はすでに使用されています。別のものを指定してください。',
          esc_html($nicename)
        )
      );

      // エラー時に JavaScript を出力(admin_footer で JS を設定)
      add_action('admin_footer', function () use ($nicename) {
  ?>
    <script>
      document.addEventListener('DOMContentLoaded', () => {
        const nicenameInput = document.getElementById('user_nicename');
        if (!nicenameInput) return;

        // エラーが出ていなければエラーメッセージを追加
        if (!document.querySelector('.nicename-error-message')) {
          // 入力フィールドに背景色をつける
          nicenameInput.style.backgroundColor = '#fae3ec';

          // エラーメッセージを作成
          const errorMessage = document.createElement('div');
          errorMessage.innerHTML = 'ナイスネーム「<?php echo esc_html($nicename); ?>」はすでに使用されています。<br>以前のナイスネームに復元しました。<br>変更する場合は新しい値を入力してください。<br>変更しない場合も必ず「ユーザーを更新」ボタンをクリックしてください。';
          errorMessage.style.color = 'red';
          errorMessage.style.marginTop = '8px';
          errorMessage.style.lineHeight = '1.5';
          errorMessage.classList.add('nicename-error-message');

          // エラーメッセージをフィールドの下に追加
          nicenameInput.insertAdjacentElement('afterend', errorMessage);

          // フィールドへスムーズスクロール
          nicenameInput.scrollIntoView({
            behavior: 'smooth',
            block: 'center'
          });
        }
      });
    </script>

<?php
      });
    });

    // エラー時は現在の nicename を復元
    $current_userdata = get_userdata($user_id);
    $nicename = $current_userdata->user_nicename;
  }

  // nicename を更新
  $userdata = array(
    'ID'             => $user_id,
    'user_nicename'  => $nicename,
  );

  wp_update_user($userdata);
}
// ユーザー自身がプロフィールを保存する時に nicename を保存
add_action('personal_options_update', 'my_update_nicename');
// 管理者が他人のプロフィールを保存する時も nicename を保存
add_action('edit_user_profile_update', 'my_update_nicename');

上記を保存して、ユーザー編集画面を開くと「ニックネーム(必須)」と「ブログ上の表示名」の間にナイスネームのフィールドが追加され、user_nicename を設定できるようになります。

設定した nicename が既存ユーザーの user_nicename と重複している場合はエラーを表示し、入力された値はクリアされ元の値が復元されます。

上記の状態で、ページを再読込すると、「無効なユーザー ID です」とだけ表示されるので、エラーメッセージには「必ず更新ボタンをクリックしてください」と表示しています。もし、「無効なユーザー ID です」とだけ表示された場合は、ブラウザの「戻る」ボタンをクリックする必要があります。

使用しているアクションフック

アクション 説明
show_user_profile 自分自身のプロフィールページ(/wp-admin/profile.php)を表示するときに呼ばれるアクション。自分のプロフィール画面に独自のフィールドを追加したいときなどに使用。
edit_user_profile 他のユーザーのプロフィールページ(/wp-admin/user-edit.php)を表示するときに呼ばれるアクション。管理者が「他のユーザーのプロフィールを編集」する画面で、独自のフィールドを追加したいときに使用。
personal_options_update 自分自身のプロフィールページを保存するときに呼ばれるアクション。フォーム送信時に「自分の独自のフィールドの値を保存する」処理を記述。
edit_user_profile_update 他のユーザーのプロフィールページを保存するときに呼ばれるアクション。管理者などが「他人のプロフィールを編集して保存」したときに、独自のフィールドの値を保存する処理を記述。
user_profile_update_errors ユーザーのプロフィール(ユーザー情報)を更新しようとしたときに、バリデーションエラー(入力エラー)を追加するために使うアクション。$errors というエラーオブジェクト(WP_Error)を受け取り、この $errors にエラーを追加すると、保存がストップし、画面上にエラーメッセージが表示される
admin_footer 管理画面(ダッシュボード)で、HTMLの</body>タグ直前にフックされるアクション。管理画面の一番下にHTMLやJSを追加したいときに使うフック

WP REST API を無効にする

WordPress 4.7 から「WP REST API」がデフォルトで有効になっています。そのため認証無しでも投稿記事やユーザー情報などのデータを外部から取得する事ができるようになっています。

例えば、以下のアドレスにアクセスしてみると、json 形式のユーザーのデータが表示されます。

「サイトのURL/wp-json/wp/v2/users」や「サイトのURL/?rest_route=/wp/v2/users」

REST APIを無効化することで、匿名の外部アクセスを防止できますが、すべて無効化すると、REST API に依存する WordPress 管理画面の機能(Gutenberg エディターなど)が動作しなくなります。

そのため、無効にする場合は、以下のように rest_authentication_errors フィルターを使って、ログインしていない場合にのみ無効にする方法が推奨されています(REST API Handbook )。

// ログインしていない場合、REST API へのアクセスを制限する
add_filter('rest_authentication_errors', function ($result) {
  // 以前の認証チェックが適用されている場合は、その結果を変更せずに渡す(返す)
  if ( true === $result || is_wp_error( $result ) ) {
    return $result;
  }

  // ログイン済みユーザーは許可
  if (is_user_logged_in()) {
    return $result;
  }

  // ログインしていない場合はエラーを返す
  return new WP_Error(
    'rest_not_logged_in',
    __('You are not currently logged in.', 'text-domain'),
    array('status' => 401)
  );
});

但し、この方法だと、ユーザーがログインしていないと Contact Form7 などの REST API を使用しているプラグインが機能しなくなってしまいます。

特定 API(プラグイン)だけ例外許可

前述のコードの場合、ログインしていないリクエストに対して無条件に WP_Error を返して、REST API を強制的に止めているため、Contact Form 7 などの「ログインしていないユーザー」も対象にして REST API 通信しているプラグインは正常動作できなくなってしまいます。

そのため、プラグインが使う特定のエンドポイントだけを許可する処理を追加する必要がありますが、以下のいずれかの方法で対応可能です(他にも方法はあるかと思います)。

rest_authentication_errors を利用

以下は rest_authentication_errors フィルターを使って、Embed(oEmbed)や Contact Form 7、 Akismet プラグイン、及びカスタムエンドポイントを許可する例です。

許可するルート($allowed_routes)には、REST APIのエンドポイントの /wp-json/ 以降、最初のパス(namespace)にスラッシュを付けた形で指定します(例:/contact-form-7/v1 など)。

add_filter('rest_authentication_errors', function ($result) {
  if (true === $result || is_wp_error($result)) {
    return $result;
  }

  // ログイン済みユーザーは許可(管理画面の機能を使えるように)
  if (is_user_logged_in()) {
    return $result;
  }

  // REST API リクエストルートを取得
  global $wp;
  // /wp-json/ 以降のパスが取れる(例: /contact-form-7/v1 や /wp/v2/posts など)
  $rest_route = isset($wp->query_vars['rest_route']) ? $wp->query_vars['rest_route'] : '';

  // 許可するルート
  $allowed_routes = [
    '/oembed/1.0',             // Embed 用
    '/contact-form-7/v1',      // Contact Form 7 用
    '/akismet/v1',             // Akismet 用
    '/my-theme-api/v1/posts',  // カスタムエンドポイント 用
  ];

  // 対象のルートは許可
  foreach ($allowed_routes as $allowed_route) {
    if (strpos($rest_route, $allowed_route) === 0) {
      return $result; // 許可対象ならOK
    }
  }

  // それ以外はエラーを返す
  return new WP_Error(
    'rest_not_logged_in',
    __('You are not currently logged in.', 'text-domain'),
    array('status' => 401)
  );
});

上記コードでは、ログイン済みユーザーはすべて許可していますが、 投稿・固定ページの編集権限があるユーザーのみ許可(例: Gutenberg使用者)する場合は7-9行目を以下のように書き換えます。

if (current_user_can('edit_posts') || current_user_can('edit_pages')) {
  return $result;
}

上記のコードにより、ログインユーザー以外は、「サイトのURL/wp-json/wp/v2/users」や「サイトのURL/?rest_route=/wp/v2/users」などにアクセスしても、許可したルート以外は表示されません。

rest_pre_dispatch を利用

以下は、rest_pre_dispatch フィルターを使って、引数 $request の get_route() メソッドをで、ルート判定して特定 API(プラグイン)だけ例外許可する例です。

前述の rest_authentication_errors を使ったコード同様、特定のプラグイン(oEmbed, Contact Form 7, Akismet)だけ許可し、あとは制限(エラー)します。許可の基準は namespace になるので先頭のスラッシュなしで指定します。環境に合わせて30-34行目部分を変更します。

また、以下では編集権限があるユーザーのみ許可していますが、ユーザーがログインしていれば許可するには9行目の判定を is_user_logged_in() に変更します。

// 編集権限のあるユーザー及び指定したプラグインに REST API を許可(それ以外は無効にする)
add_filter('rest_pre_dispatch', function ($result, $server, $request) {
  // 念のため、リクエストオブジェクトの型チェック ( WP_REST_Request 以外なら処理しない )
  if (! ($request instanceof WP_REST_Request)) {
    return $result;
  }

  // 投稿・固定ページの編集権限があるユーザーなら許可(例: Gutenberg使用者)
  if (current_user_can('edit_posts') || current_user_can('edit_pages')) {
    return $result;
  }

  // ルート取得(先頭スラッシュを除去)
  $route = ltrim($request->get_route(), '/');

  // ルートが空の場合は REST API を無効化
  if (empty($route)) {
    return new WP_Error(
      'rest_disabled',
      __('The REST API on this site has been disabled.', 'text-domain'),
      array('status' => 403)
    );
  }

  // スラッシュでルートを分割して namespace を抽出(例: "contact-form-7/v1")
  $parts = explode('/', $route);
  $namespace = isset($parts[0]) && isset($parts[1]) ? $parts[0] . '/' . $parts[1] : '';

  // 許可する namespace 一覧(oEmbed、Contact Form 7、Akismet)
  $permitted_namespaces = [
    'oembed/1.0',          // Embed 用
    'contact-form-7/v1',   // Contact Form 7 用
    'akismet/v1',          // Akismet 用
  ];

  // 許可リストに含まれていれば通過
  if (in_array($namespace, $permitted_namespaces, true)) {
    return $result;
  }

  // それ以外は REST API を無効化(ステータス 403)
  return new WP_Error(
    'rest_disabled',
    __('The REST API on this site has been disabled.', 'text-domain'),
    array('status' => 403)
  );
}, 10, 3);

上記のコードにより、編集権限があるユーザーも含め、「サイトのURL/wp-json/wp/v2/users」や「サイトのURL/?rest_route=/wp/v2/users」などにアクセスしても、許可したルート以外は表示されません。

除外するプラグインを追加

除外したいプラグインが増えた場合は、上記コードの $permitted_namespaces(30-34行目)にそのプラグインのエンドポイントの名前空間を追加で指定します。

REST API を利用しているプラグインの名前空間は「サイトのURL/wp-json/」でアクセスして表示されるデータの namespaces で確認できます(REST API を有効にしておきます)。

rest_authentication_errors を使用する場合との違い

どちらのコードも「処理結果」的には、特定プラグインは許可、それ以外は制限するので同じですが、実行タイミングが異なります。

rest_authentication_errors のコードの場合、認証段階(APIアクセスのかなり早い段階)で実行されますが、rest_pre_dispatch のコードではリクエストの直前(データ取得直前)に実行されます。

/wp-json/wp/v2/users など許可したルート以外にアクセスした場合、rest_authentication_errors のコードでは、ログインユーザーはデータが表示されますが、rest_pre_dispatch のコードでは、編集権限があるユーザーでも表示されません。

この例の場合、rest_authentication_errors のコードでは 401 Unauthorized を返していますが、rest_pre_dispatch のコードでは 403 Forbidden を返しています。

「未ログインユーザー」に厳しく制限したい(API認証時にすぐ弾きたい)場合は rest_authentication_errors を使用し、「一部権限のあるユーザー(編集者など)」にも許可しつつ、柔軟に制御したい場合は rest_pre_dispatch を使うなどが考えられます。