【WordPress】ログイン不要で非公開投稿を一時的に共有するURLを生成する方法

【WordPress】ログイン不要で非公開投稿を一時的に共有するURLを生成する方法 WordPress

WordPressでは「非公開投稿」はログインユーザーしか閲覧できません。しかし、外部のクライアントや確認者にログインなしで一時的に非公開投稿を共有したいケースもあります。

この記事では、非公開投稿に対して「一時的に有効な共有リンク」を生成し、ログイン不要かつ有効期限付きで閲覧できる仕組みを実装する方法を紹介します。

スポンサーリンク

仕組みの概要

  • 非公開投稿に専用の一時共有トークンと有効期限を発行
  • トークン付きURL(例:/?p=123&token=abc123)へのアクセスをtemplate_redirectフックで直接処理
  • get_post()で投稿を直接取得し、トークン・有効期限が一致した場合のみ手動でコンテンツを描画
  • トークンはランダム生成&投稿ごとに保存

1. 投稿に一時トークンと有効期限を保存する

以下のコードを functions.php に追加して、投稿保存時にトークンと有効期限(48時間後)が未設定なら自動生成します。

functions.php:共有トークンの発行
function add_share_token_on_save($post_id) {
  if (get_post_status($post_id) !== 'private') return;

  if (!get_post_meta($post_id, '_share_token', true)) {
    $token = bin2hex(random_bytes(8));
    update_post_meta($post_id, '_share_token', $token);
    // 有効期限(48時間後)
    update_post_meta($post_id, '_share_token_expires', time() + 48 * HOUR_IN_SECONDS);
  }
}
add_action('save_post', 'add_share_token_on_save');

2. トークン付きアクセスを template_redirect で直接処理する

the_postsフィルターでは実装できない
「クエリ結果の投稿をthe_postsposts_resultsフィルターで受け取り、post_statusを書き換えて表示を許可する」という方式は原理的に機能しません。WordPressコア(wp-includes/class-wp-query.php)には、is_single/is_pageクエリに対する専用の可視性ゲートがあり、the_postsフィルターが発火するより前の時点で、非ログインユーザーが非公開ステータスの投稿にアクセスした場合は$this->postsを無条件で空配列にします。この判定はis_user_logged_in()の結果のみで決まり、権限チェックにすら到達しません。つまり非ログインの訪問者がアクセスした時点で、トークンが正しいかどうかに関わらずクエリフィルターに投稿は渡ってこないのです。

そこで、非公開投稿をWordPressの通常のクエリの可視性チェックに乗せず、template_redirectフックで直接処理します。get_post()はWP_Queryの可視性フィルタを経由しない直接取得のため、非公開投稿でも問題なく取得できます。

functions.php:トークン検証と表示処理
function handle_private_post_share_link() {
  if (is_admin() || !isset($_GET['p'], $_GET['token'])) return;

  $post_id = absint($_GET['p']);
  // get_post()はWP_Queryの可視性チェックを経由しない直接取得なので、
  // 非公開投稿でも取得できる
  $post = get_post($post_id);

  if (!$post || $post->post_status !== 'private') return;

  $token   = get_post_meta($post_id, '_share_token', true);
  $expires = (int) get_post_meta($post_id, '_share_token_expires', true);
  $url_token = sanitize_text_field(wp_unslash($_GET['token']));

  // hash_equals()でタイミング攻撃を防止($token===$url_tokenは不可)
  if (!$token || !hash_equals($token, $url_token)) return;
  if ($expires && time() > $expires) return;

  // キャッシュプラグインにこのレスポンスをキャッシュさせない
  if (!defined('DONOTCACHEPAGE')) define('DONOTCACHEPAGE', true);
  nocache_headers();
  status_header(200);

  global $wp_query;
  $wp_query->is_404 = false;
  $wp_query->is_single = true;
  $wp_query->queried_object = $post;
  $wp_query->queried_object_id = $post->ID;

  setup_postdata($post);

  get_header();
  ?>
  <article class="shared-private-post">
    <h1><?php echo esc_html(get_the_title($post)); ?></h1>
    <div class="entry-content"><?php echo apply_filters('the_content', $post->post_content); ?></div>
  </article>
  <?php
  get_footer();
  exit;
}
add_action('template_redirect', 'handle_private_post_share_link');

トークンの比較には===ではなくhash_equals()を使っています。===は最初に異なる文字が見つかった時点で処理を打ち切るため、比較にかかる時間の差から秘密の値が少しずつ推測されうる「タイミング攻撃」のリスクがあります。hash_equals()は常に一定時間で比較を完了するため、この種の攻撃を防げます。

キャッシュプラグインへの対応も必須
WP Rocket・W3 Total Cache・WP Super Cacheなどの多くのキャッシュプラグインは、初期設定ではクエリ文字列ごとに別々のキャッシュを作成しません。そのため、トークン付きリクエストで描画された非公開コンテンツが、トークンなしの素のURL(/?p=123)のキャッシュとして誤って保存・配信されてしまう危険があります。上記コードのDONOTCACHEPAGE定数(主要なキャッシュプラグインが共通して尊重する「このページをキャッシュしない」の合図)とnocache_headers()は、この事故を防ぐために必須です。

3. 共有URLの取得方法

投稿編集画面で以下のような関数を呼び出して、共有URLを表示するカスタムメタボックスなどを作成できます。

functions.php:共有URLの生成
function get_private_share_url($post_id) {
  $token = get_post_meta($post_id, '_share_token', true);
  return add_query_arg('token', $token, get_permalink($post_id));
}

get_permalink()を文字列連結で?token=...とつなげてはいけません。パーマリンク構造が「基本」(デフォルト設定)の場合、get_permalink()は既に?p=123を含むURLを返すため、単純に連結すると?p=123?token=abc123という「?」が2つある不正なURLになり、$_GET['token']が正しく取得できません。add_query_arg()を使えば、パーマリンク構造に関わらず正しくクエリパラメーターを追加できます。

パスワード保護だけで十分な場合

相手を限定せず「URLとパスワードを知っている人だけに見せたい」という程度であれば、WordPress標準の投稿パスワード保護機能(post_password)だけで十分なケースも多くあります。特定の投稿にだけパスワード保護を自動で適用する方法もあわせて参照してください。今回のようなトークン方式は、パスワード入力なし・URLを知っているだけでアクセスさせたい場合や、相手ごとに個別のURLを発行したい場合に向いています。

注意点と拡張

  • 共有が不要になったら、_share_token_share_token_expiresのメタを削除するか、投稿を非公開のまま放置せず整理しましょう。
  • アクセスログを残したい場合は、handle_private_post_share_link()内でアクセス日時を記録する処理を追加します。
  • より厳密な運用が必要な場合は、投稿ごとではなく共有リンクごとにトークンを複数発行できるよう、専用のテーブルで管理する方法もあります。

よくある質問(FAQ)

Q非公開投稿の一時共有URLを生成するには?
A非公開投稿にランダムトークンをupdate_post_meta()で保存し、add_query_arg()で共有URLを生成します。ただし非公開投稿はWP_Queryの可視性チェックにより非ログインユーザーには最初から取得されないため、the_postsのような通常のクエリフィルターでは表示を許可できません。template_redirectフックでget_post()を使い、可視性チェックを経由せずに直接投稿を取得してから、トークンを検証し手動で描画する必要があります。
Q一時URLに有効期限を設定するには?
Aトークンと合わせて有効期限のタイムスタンプもpost_metaに保存し、template_redirectでの検証時にtime()と比較します。期限切れの場合は何もせずreturnすることで、通常のWordPressの動作(404扱いなど)に任せます。
Q一時URLのアクセス権限チェックをどこで実装するには?
Atemplate_redirectフックで実装します。get_post()による直接取得、hash_equals()によるタイミング攻撃耐性のあるトークン比較、有効期限チェックを行い、条件を満たした場合のみDONOTCACHEPAGEとnocache_headers()でキャッシュを無効化してから手動でコンテンツを出力します。

まとめ

非公開投稿でもトークン付きURLを使えば、ログイン不要で一時的に共有することが可能です。ただしWordPressの非公開投稿の可視性チェックは非ログインユーザーに対して非常に厳格なため、通常のクエリフィルターでは実装できません。template_redirectフックでget_post()による直接取得とトークン検証を組み合わせ、キャッシュプラグインへの対応もあわせて行うことが、この仕組みを安全に成立させるポイントです。