WordPress Logo WordPress 特定ページにBasic認証を設定する方法【functions.php使用】

WordPress サイトの一部のページや投稿にアクセス制限をかけたい場合、Basic 認証はシンプルかつ手軽な方法の一つです。以下では、functions.php を使って特定のページやカテゴリに Basic 認証をかける方法から、.htpasswd を使ったよりセキュアな実装まで、実例を交えて詳しく解説します。

更新日:2025年05月21日

作成日:2025年05月17日

特定のスラッグ(ページ)に Basic 認証を設定

以下のコードは、WordPress の functions.php に記述することで、特定のスラッグ(この例ではスラッグが foo のページ)に Basic 認証をかけるものです。

// WordPress の「template_redirect」アクションに無名関数を登録
add_action('template_redirect', function () {
  // Basic認証をかけたいページのスラッグ(URLの一部)を指定
  $target_slug = 'foo';

  // 現在のリクエストのパス部分を取得(例: /foo など)
  $request_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

  // リクエストパスがターゲットスラッグ(例: /foo または /foo/)と一致するか確認
  if (preg_match('#/' . preg_quote($target_slug, '#') . '/?$#', $request_path)) {
    // 認証に使用するユーザー名とパスワードを設定(username と password は任意の値に変更します)
    $user = 'username';
    $pass = 'password';

    // ブラウザから送られてきた認証情報が正しいか確認
    if (
      !isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) || // 認証情報が送られていない
      $_SERVER['PHP_AUTH_USER'] !== $user ||                        // ユーザー名が一致しない
      $_SERVER['PHP_AUTH_PW'] !== $pass                             // パスワードが一致しない
    ) {
      // 認証が必要であることをブラウザに伝えるヘッダーを送信
      header('WWW-Authenticate: Basic realm="Restricted Login Page"');
      // 認証失敗時のステータスコード(401 Unauthorized)を送信
      header('HTTP/1.0 401 Unauthorized');
      // 認証エラーメッセージを表示して処理を終了
      echo 'Authentication required.';
      exit;
    }
  }
});

上記コードでは、https://example.com/foo や https://example.com/category/foo など URL に /foo または /foo/ が含まれるページにアクセスする場合に認証を求められます。

is_page() や is_single() などの条件分岐タグを使ってページを判定する方法もあります。

template_redirect アクション

WordPress がテンプレートを読み込む直前に実行されるアクションで認証チェックを行います。

template_redirect は、どのテンプレート(固定ページ・投稿・カテゴリなど)を表示するか決まった後に実行されるため、特定のスラッグやページだけに処理を限定しやすく、意図しないページへの影響を避けやすいメリットがあります。

ターゲットページのチェック

アクセスされたページの URL のパス部分のみを parse_url() で PHP_URL_PATH を指定して取得し、スラッグ foo と一致するかを preg_match で確認します。

スラッグに正規表現の特殊文字が含まれていた場合にパターンが壊れてしまうのを防ぐために preg_quote() を使用しています(第2引数にはパターンの両端に使うデリミタを指定します)。

上記の場合、$target_slug = 'foo'; としているので、URL(パス)に /foo または /foo/ が含まれる場合にマッチします。

/category/foo にマッチさせるには、$target_slug = 'category/foo' のように指定します(/foo や /foo/ にはマッチしません)。

より柔軟にマッチさせる(スラッグが URL パス中の途中に現れるケースにも対応させる)には、例えば preg_match() を以下のように変更します。

if (preg_match('#(^|/)' . preg_quote($slug, '#') . '/?$#', $request_path))

(^|/) の ^ は文字列の先頭を意味し、/ はスラッグの前にスラッシュがあってもよいという意味なので、(^|/) により、先頭でも途中でも一致可能になります。

また、/foo/subpage や /bar/details などの子ページを含めたい場合は、例えば、以下のように変更するなど、preg_match() に指定する正規表現で調整することができます。

if (preg_match('#^/' . preg_quote($slug) . '(/|$)#', $request_path))

Basic 認証の実施

アクセスしてきたユーザーの PHP_AUTH_USER(ユーザー名)と PHP_AUTH_PW(パスワード)をチェックします。設定した値(username / password)と一致しない場合は、ブラウザに認証が必要であることを通知します。

$_SERVER['PHP_AUTH_USER'] と $_SERVER['PHP_AUTH_PW'] は、Basic 認証を使った際に、ブラウザから送られてくるユーザー名とパスワードを PHP 側で受け取るためのスーパーグローバル変数です。

認証失敗時の処理

401 Unauthorized ステータスと共に、エラーメッセージを表示して処理を停止します。

注意点

  • Basic 認証は情報を暗号化せず送信するので、パスワードが平文で盗まれる可能性があるため、HTTPS を必ず使う必要があります(傍受されると簡単にデコードされてしまいます)。必要に応じて PHP で自動的に HTTPS にリダイレクトさせることもできます。

  • .htaccess で Basic 認証が設定されていると、PHP での認証処理と競合する場合があります。

  • このページで使用しているコードはシンプルで手軽ですが、本格的な会員制サイトなどには WordPress のログイン機能や専用プラグインの使用を検討するのが安全です。

template_redirect と init の使い分け

通常の PHP ベースの Basic 認証では template_redirect を使うケースも多いのですが、「どの URL に対して」「どんな目的で」Basic 認証をかけたいかによって、init と template_redirect のどちらを使うべきかが変わってきます。

フック名 タイミング 特徴と用途
template_redirect テンプレートの読み込み前 テーマ表示の直前。ページ内容の分岐やリダイレクトに便利だが、出力バッファが始まっている可能性もある。
init WordPressの早い段階(URL確定直後) ルーティング前。URL判定やリダイレクト、認証のような処理に適する。出力はまだ始まっていない。

template_redirect を使うべきケース

WordPress の通常ページ(投稿・固定ページなど)に認証をかけたい場合

  • 投稿や固定ページ、カスタム投稿タイプなど既存のテンプレート表示に対して認証をかけたい場合
  • URLルーティングが確定し、is_ 条件タグが使える状態で処理したい

init を使うべきケース

仮想 URL など「WordPress 的に存在しない」URLに対して認証をかけたい場合

  • 仮想的なルート(例:プラグイン WPS Hide Login で生成される特殊な URL など)
    • WordPressのルーティングが始まる前にURLを処理したい
  • 認証がルートレベルで必要な場合(早い段階で弾きたい)
  • is_404() や $wp_query に依存しない処理

HTTPS にリダイレクト

Basic 認証は、アクセス元が HTTPS でない場合、パスワードが平文で送信されるので、盗聴リスクがあります。以下は、アクセスが HTTP だった場合に、PHP で自動的に HTTPS へリダイレクトさせる例です。

※ サーバー側で HTTPS を強制している場合は、リダイレクト処理(4-15行目)は不要です。

通常、$_SERVER['HTTPS'] は HTTPS 接続時に 'on'(または '1' や true)になります。

但し、Cloudflare や Nginx などのリバースプロキシや CDN を利用している場合、$_SERVER['HTTPS'] が正しく検出されないため、代わりに $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' を使って、リクエストが HTTPS 経由かどうかを確認します。

【注意】MAMP など http のローカル環境では、以下は機能しません。※ 301 permanent でリダイレクトしているので、以下を実行した場合は、ブラウザのキャッシュを削除するなどが必要になります。

add_action('init', function () {
  $target_slug = 'foo'; // Basic認証をかけたいスラッグ

  // HTTPS判定(通常 + プロキシ環境)
  $is_https = (
    (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
    (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https')
  );

  // もし HTTP でアクセスしていたら、HTTPS にリダイレクト
  if (!$is_https) {
    $https_url = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
    wp_redirect($https_url, 301);
    exit;
  }

  // 現在のURLパスを取得(例: /foo や /foo/ など)
  $request_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

  // パスが対象スラッグと一致しているかどうか確認
  if (preg_match('#/' . preg_quote($target_slug, '#') . '/?$#', $request_path)) {
    $user = 'username';
    $pass = 'password';

    // Basic認証チェック
    if (
      !isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) ||
      $_SERVER['PHP_AUTH_USER'] !== $user ||
      $_SERVER['PHP_AUTH_PW'] !== $pass
    ) {
      // 認証ダイアログを表示させるヘッダー
      header('WWW-Authenticate: Basic realm="Restricted Login Page"');
      header('HTTP/1.0 401 Unauthorized');
      echo 'Authentication required.';
      exit;
    }
  }
});

以下はサーバー側で HTTPS を強制する一般的な例です。

サーバー側で、例えば以下のような設定がされていれば上記のリダイレクト処理は不要です。

RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
server {
  listen 80;
  server_name example.com;
  return 301 https://$host$request_uri;
}

複数スラッグに対応

以下は、複数のスラッグ(ページ)に対応できるよう書き換えたコードです。

この場合、Basic 認証をかけたいページのスラッグを $target_slugs = ['foo', 'bar', 'secret-page'] のように配列で管理できます。

foreach で $request_path に一致するスラッグがあるかチェックし、一致したら Basic認証を実施し、マッチ後はループを抜けて無駄な処理を防止しています。

add_action('template_redirect', function () {
  // Basic認証をかけたいスラッグの一覧を配列で指定
  $target_slugs = ['foo', 'bar', 'secret-page'];

  // 現在のURLパスを取得(例:/foo, /bar/ など)
  $request_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

  // 各スラッグに対して現在のパスが一致するかチェック
  foreach ($target_slugs as $slug) {
    if (preg_match('#/' . preg_quote($slug, '#') . '/?$#', $request_path)) {
      // 認証に使うユーザー名とパスワード
      $user = 'username';
      $pass = 'password';

      // Basic認証処理
      if (
        !isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) ||
        $_SERVER['PHP_AUTH_USER'] !== $user ||
        $_SERVER['PHP_AUTH_PW'] !== $pass
      ) {
        header('WWW-Authenticate: Basic realm="Restricted Login Page"');
        header('HTTP/1.0 401 Unauthorized');
        echo 'Authentication required.';
        exit;
      }
      break; // 該当スラッグに一致したらループを抜ける
    }
  }
});

ユーザー名とパスワードを wp-config.php で定義

以下は認証に使うユーザー名とパスワードを wp-config.php で定数として定義する例です。

この方法により、認証情報をコードから分離できるため、セキュリティとメンテナンス性が向上します。

wp-config.php にユーザー名とパスワードを定義します。

define('BASIC_AUTH_USER', 'username');
define('BASIC_AUTH_PASS', 'password');

ユーザー名とパスワードを wp-config.php に定義された定数から取得します。

add_action('template_redirect', function () {
  $target_slugs = ['foo', 'bar', 'secret-page'];
  $request_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

  // wp-config.php に定義された定数から、ユーザー名とパスワードを取得(定数が未定義の場合に備えて空文字を代入)
  $user = defined('BASIC_AUTH_USER') ? BASIC_AUTH_USER : '';
  $pass = defined('BASIC_AUTH_PASS') ? BASIC_AUTH_PASS : '';

  foreach ($target_slugs as $slug) {
    if (preg_match('#/' . preg_quote($slug, '#') . '/?$#', $request_path)) {
      // Basic認証処理
      if (
        !isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) ||
        $_SERVER['PHP_AUTH_USER'] !== $user ||
        $_SERVER['PHP_AUTH_PW'] !== $pass
      ) {
        header('WWW-Authenticate: Basic realm="Restricted Login Page"');
        header('HTTP/1.0 401 Unauthorized');
        echo 'Authentication required.';
        exit;
      }
      break; // 該当スラッグに一致したらループを抜ける
    }
  }
});

セキュリティ補足

Git を使用している場合は、wp-config.php を Git に含めないようにします(.gitignore に含める)。

また、標準でされていることが多いかと思いますが、例えば、以下のように .htaccess で wp-config.php の直接アクセスを防ぐ設定をしておきます。

<files wp-config.php>
  order allow,deny
  deny from all
</files>

Apache 2.4 以降では、Order, Allow, Deny ディレクティブは非推奨(廃止)になっているので、代わりに Require ディレクティブを使用します。

<Files wp-config.php>
  Require all denied
</Files>

Apache 2.2 と 2.4 の両方に対応したい場合は、バージョンチェック付きの記述が可能です。

<Files wp-config.php>
  <IfModule mod_authz_core.c>
    Require all denied
  </IfModule>
  <IfModule !mod_authz_core.c>
    Order allow,deny
    Deny from all
  </IfModule>
</Files>

スラッグごとに異なる認証情報を設定

以下はスラッグ(ページ)ごとに異なる認証情報を設定する例です。

認証情報をスラッグ別に定義します。

//スラッグ foo の認証情報
define('BASIC_AUTH_USER_FOO', 'foo_user');
define('BASIC_AUTH_PASS_FOO', 'foo_pass');

//スラッグ secret-page の認証情報
define('BASIC_AUTH_USER_SECRET', 'secret_user');
define('BASIC_AUTH_PASS_SECRET', 'secret_pass');

ページ(スラッグ)ごとに 異なるユーザー名・パスワード を設定する場合は、スラッグをキーとしてユーザー名とパスワードのセットを持つ連想配列を使うのが簡単です。

add_action('template_redirect', function () {
  // スラッグごとにユーザー名とパスワードを設定
  $protected_pages = [
    'foo' => [
      'user' =>  defined('BASIC_AUTH_USER_FOO') ? BASIC_AUTH_USER_FOO : '',
      'pass' =>  defined('BASIC_AUTH_PASS_FOO') ? BASIC_AUTH_PASS_FOO : ''
    ],
    'secret-page' => [
      'user' =>  defined('BASIC_AUTH_USER_SECRET') ? BASIC_AUTH_USER_SECRET : '',
      'pass' =>  defined('BASIC_AUTH_PASS_SECRET') ? BASIC_AUTH_PASS_SECRET : ''
    ],
  ];

  // 現在のURLパスを取得
  $request_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

  // スラッグをキーとした連想配列をループ
  foreach ($protected_pages as $slug => $credentials) {
    if (preg_match('#/' . preg_quote($slug, '#') . '/?$#', $request_path)) {
      // 対象スラッグに対応するユーザー名・パスワード
      $user = $credentials['user'];
      $pass = $credentials['pass'];
      // Basic認証
      if (
        !isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) ||
        $_SERVER['PHP_AUTH_USER'] !== $user ||
        $_SERVER['PHP_AUTH_PW'] !== $pass
      ) {
        header('WWW-Authenticate: Basic realm="Restricted Page"');
        header('HTTP/1.0 401 Unauthorized');
        echo 'Authentication required for this page.';
        exit;
      }
      break; // 認証が必要なページに一致したらループ終了
    }
  }
});
ページが多くなった場合

もし今後ページが増えることが想定されるなら、ユーザー名・パスワードの取得を関数化し、スラッグごとに異なるユーザー名・パスワードを「一定の形式」で wp-config.php に定義することで、スラッグごとの認証情報の追加・変更が簡単になります。

スラッグごとのユーザー名・パスワードの定義では、例えば、以下のように BASIC_AUTH_USER_SLUG と BASIC_AUTH_PASS_SLUG を定義します。SLUG の部分はスラッグを大文字・アンダースコア形式に変換したものを指定します(関数側で形式を定義)。

/*
wp-config.php に BASIC_AUTH_USER_SLUG と BASIC_AUTH_PASS_SLUG を定義
SLUG の部分は大文字・アンダースコア形式
*/
// スラッグ "foo" 用
define('BASIC_AUTH_USER_FOO', 'foo_user');
define('BASIC_AUTH_PASS_FOO', 'foo_pass');

// スラッグ "secret-page" 用
define('BASIC_AUTH_USER_SECRET_PAGE', 'secret_user');
define('BASIC_AUTH_PASS_SECRET_PAGE', 'secret_pass');

以下のように、ユーザー名・パスワードを取得する関数 get_basic_auth_credentials() を定義することで、前述のスラッグごとにユーザー名とパスワードを設定した変数 $protected_pages はスラッグ名だけの配列 $protected_slugs にできるので簡潔になります。

add_action('template_redirect', function () {

  // Basic 認証情報をスラッグから取得する関数を定義
  function get_basic_auth_credentials($slug) {
    // スラッグを大文字・アンダースコア形式に変換(例: secret-page → SECRET_PAGE)
    $const_suffix = strtoupper(str_replace('-', '_', $slug));
    $user_const = 'BASIC_AUTH_USER_' . $const_suffix;
    $pass_const = 'BASIC_AUTH_PASS_' . $const_suffix;
    return [
      'user' => defined($user_const) ? constant($user_const) : '',
      'pass' => defined($pass_const) ? constant($pass_const) : '',
    ];
  }

  // Basic 認証をかけたいスラッグ一覧
  $protected_slugs = ['foo', 'secret-page'];

  // 現在の URL のパス部分を取得
  $request_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

  foreach ($protected_slugs as $slug) {
    if (preg_match('#/' . preg_quote($slug, '#') . '/?$#', $request_path)) {
      // Basic 認証情報をスラッグから取得する関数
      $credentials = get_basic_auth_credentials($slug);
      $user = $credentials['user'];
      $pass = $credentials['pass'];
      // Basic認証処理
      if (
        !isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) ||
        $_SERVER['PHP_AUTH_USER'] !== $user ||
        $_SERVER['PHP_AUTH_PW'] !== $pass
      ) {
        header('WWW-Authenticate: Basic realm="Restricted Page"');
        header('HTTP/1.0 401 Unauthorized');
        echo 'Authentication required for this page.';
        exit;
      }
      break; // 一致したスラッグが見つかればループ終了
    }
  }
});

このようにすれば、wp-config.php 側で認証情報を追加して、functions.php ではスラッグ一覧にスラッグを追加するだけで済むので、管理が簡単になります。

.htpasswd を使った Basic 認証

以下は、特定の URL(スラッグ)にアクセスがあった場合に、.htpasswd ファイルに登録されたユーザー情報を使って Basic 認証をかける例です。

  • パスワード情報を wp-config.php などに直接書かず、外部ファイル .htpasswd で安全に管理。
  • 複数のスラッグを指定でき、それぞれに 複数ユーザーの認証が可能。
  • .htpasswd が存在しない・読めない場合は、ログに記録してエラーを返します。

.htpasswd ファイルとは?

.htpasswd は、以下のように1行ごとに「ユーザー名:ハッシュ化されたパスワード」を記述する認証情報ファイルです。

alice:$2y$10$abcDEF...
bob:$2y$10$xyz123...
  • パスワードは password_hash() などでハッシュ化したものを記述します。
  • .htpasswd はドキュメントルート外(Webアクセス不可の場所)に配置します。
    • 例: /home/youruser/.htpasswds/mydomain.com/passwd/.htpasswd

関連ページ:パスワード暗号化(ハッシュ化)生成サンプル

以下のコードでは、

  • 指定されたスラッグ(例: /foo, /bar)にアクセスがあった場合に限り、Basic 認証を要求します。
  • .htpasswd にあるユーザーの認証情報が一致すれば、ページにアクセス可能にします。
  • .htpasswd が存在しない、または読み込めない場合は、ログを出力し 500 エラーを返します。

主なポイントとしては、

  • $target_slugs に認証をかけたいスラッグ(URLの末尾)を指定します。
  • .htpasswd ファイルの各行を読み取り、ユーザー名とパスワードハッシュを確認します。
  • password_verify() によって、ユーザーが送信したパスワードをハッシュと照合します。
  • 認証失敗時には 401 ステータスで再度認証を促します。
add_action('template_redirect', function () {
  // Basic認証をかけたいスラッグの一覧を配列で指定
  $target_slugs = ['foo', 'bar', 'secret-page'];

  // .htpasswdファイルの絶対パス(実際の環境に合わせて変更してください)
  $htpasswd_file = '/home/youruser/.htpasswds/mydomain.com/passwd/.htpasswd';

  // 現在のURLのパス(クエリを除く)を取得
  $request_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

  // HTTP認証ヘッダー(401レスポンス)を返す関数
  function send_auth_request($message = 'Authentication required.') {
    header('WWW-Authenticate: Basic realm="Restricted Login Page"');
    header('HTTP/1.0 401 Unauthorized');
    echo $message;
    exit;
  }

  // 各スラッグと現在のパスが一致するかループでチェック
  foreach ($target_slugs as $slug) {
    // 正規表現で末尾のスラッシュの有無も含めて一致を確認
    if (preg_match('#/' . preg_quote($slug, '#') . '/?$#', $request_path)) {

      // 認証ヘッダーが送信されていない場合、認証を要求
      if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) {
        send_auth_request();
      }

      // ブラウザから送信されたユーザー名とパスワードを取得
      $username = $_SERVER['PHP_AUTH_USER'];
      $password = $_SERVER['PHP_AUTH_PW'];

      $authenticated = false;

      // .htpasswd ファイルの存在と読み込み権限を確認
      if (!file_exists($htpasswd_file) || !is_readable($htpasswd_file)) {
        error_log("htpasswd file not found or not readable: $htpasswd_file");
        header('HTTP/1.0 500 Internal Server Error');
        echo 'Server configuration error.';
        exit;
      }

      // .htpasswd の各行を取得(空行や改行を除く)
      $lines = file($htpasswd_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

      // .htpasswd ファイルの読み込みに失敗した場合
      if ($lines === false) {
        error_log("Failed to load htpasswd file despite file_exists(): $htpasswd_file");
        header('HTTP/1.0 500 Internal Server Error');
        echo 'Server error.';
        exit;
      }

      // .htpasswd ファイルの各行(ユーザー名:ハッシュ)を処理して、該当するユーザー名があるか確認
      foreach ($lines as $line) {
        // ユーザー名とハッシュに分割(.htpasswd にある各行を $user と $hash に分解)
        list($user, $hash) = explode(':', $line, 2);

        // 入力ユーザー名が一致し、パスワードも正しければ認証成功
        if ($user === $username && password_verify($password, $hash)) {
          $authenticated = true;
          break;
        }
      }

      // 認証に失敗した場合
      if (!$authenticated) {
        send_auth_request('Invalid credentials.');
      }

      break; // 一致したスラッグが見つかればループ終了(以降のチェックをスキップ)
    }
  }
});

スラッグが認証対象の場合は、.htpasswd ファイルの存在と読み込み権限を file_exists()is_readable() で確認してから .htpasswd ファイル を file() で読み込みます。

$lines = file(...) → .htpasswd の中身を1行ずつ配列として読み込みます。

list($user, $hash) = explode(':', $line, 2); → 各行の ユーザー名:パスワードハッシュを分解して list() で $user, $hash に格納。

.htpasswd ファイルが「存在しない」のか「読めない」のかをログ上で明確にしたい場合は、36-41行目部分を以下のように記述します。

if (!file_exists($htpasswd_file)) {
  error_log("htpasswd file not found: $htpasswd_file");  // 存在しない
  echo 'Server configuration error.';
  exit;
}

if (!is_readable($htpasswd_file)) {
  error_log("htpasswd file not readable: $htpasswd_file");  // 読めない
  echo 'Server configuration error.';
  exit;
}
スラッグごとに .htpasswd を分ける

スラッグごとに .htpasswd ファイルを分けて認証を個別に管理することができます。この方法により、ページ単位でアクセス権を柔軟に管理でき、特定のページに対してそれぞれ異なるユーザーアクセス権を設定することが可能になります。

例えば、/foo は全社向けにして、/secret-page は役員専用といったように、.htpasswd に登録するユーザーをスラッグごとに異なる設定をすることができます。この構成では、各スラッグごとに異なる認証情報を管理できるため、細やかなアクセス管理が可能になります。

以下は実装例です。

スラッグごとに認証ファイルを分ける場合、基本的にはスラッグとファイルパスの連想配列を用いて、各ページに対応する .htpasswd ファイルのパスを指定します。これにより、各スラッグに対する個別の認証が可能になります。

add_action('template_redirect', function () {
  // スラッグと対応する .htpasswd ファイルのパス
  $protected_pages = [
    'foo' => '/home/youruser/.htpasswds/mydomain.com/foo.htpasswd',
    'bar' => '/home/youruser/.htpasswds/mydomain.com/bar.htpasswd',
    'secret-page' => '/home/youruser/.htpasswds/mydomain.com/secret-page.htpasswd',
  ];

  $request_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

  // 共通の401レスポンス処理
  function send_auth_request($message = 'Authentication required.') {
    header('WWW-Authenticate: Basic realm="Restricted Page"');
    header('HTTP/1.0 401 Unauthorized');
    echo $message;
    exit;
  }

  // スラッグと .htpasswd ファイルパスの連想配列を使って、各スラッグと現在のパスが一致するかループでチェック
  foreach ($protected_pages as $slug => $htpasswd_file) {
    // パスがスラッグに一致するか確認(末尾スラッシュにも対応)
    if (preg_match('#/' . preg_quote($slug, '#') . '/?$#', $request_path)) {

      // PHP_AUTH_* が存在しない場合は認証要求
      if (!isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
        send_auth_request();
      }

      // .htpasswd ファイルの存在と可読性をチェック
      if (!file_exists($htpasswd_file) || !is_readable($htpasswd_file)) {
        error_log("htpasswd file not found or not readable: $htpasswd_file");
        header('HTTP/1.0 500 Internal Server Error');
        echo 'Server configuration error.';
        exit;
      }

      $lines = file($htpasswd_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
      if ($lines === false) {
        error_log("Failed to read htpasswd file: $htpasswd_file");
        header('HTTP/1.0 500 Internal Server Error');
        echo 'Server error.';
        exit;
      }

      $username = $_SERVER['PHP_AUTH_USER'];
      $password = $_SERVER['PHP_AUTH_PW'];
      $authenticated = false;

      foreach ($lines as $line) {
        list($user, $hash) = explode(':', $line, 2);
        if ($user === $username && password_verify($password, $hash)) {
          $authenticated = true;
          break;
        }
      }

      if (!$authenticated) {
        send_auth_request('Invalid credentials.');
      }

      break; // 該当スラッグが見つかったら以降の処理は不要
    }
  }
});
複数スラッグ対応版(改良版)

前述の例の場合、複数のスラッグ(例 baz1、baz2)に対して同じ .htpasswd ファイルを指定するには、以下のように記述することができますが、使い勝手がよくありません。

$protected_pages = [
  'foo' => '/home/youruser/.htpasswds/mydomain.com/foo.htpasswd',
  'bar' => '/home/youruser/.htpasswds/mydomain.com/bar.htpasswd',
  'baz1' => '/home/youruser/.htpasswds/mydomain.com/baz.htpasswd', // 同じ .htpasswd ファイル
  'baz2' => '/home/youruser/.htpasswds/mydomain.com/baz.htpasswd', // 同じ .htpasswd ファイル
  'secret-page' => '/home/youruser/.htpasswds/mydomain.com/secret-page.htpasswd',
];

可読性と保守性を高めるには、複数のスラッグをまとめて1つの .htpasswd ファイルにマッピングする構成に変更します。

以下のように、「.htpasswd ファイルのパス → スラッグの配列」の形式で定義し、処理中に一致を探すようにすることで、同じ .htpasswd を複数スラッグに割り当てる構造を簡潔に記述できます。

add_action('template_redirect', function () {
  // .htpasswd ファイルと、それに対応するスラッグの配列
  $htpasswd_map = [
    '/home/youruser/.htpasswds/mydomain.com/foo.htpasswd' => ['foo'],
    '/home/youruser/.htpasswds/mydomain.com/bar.htpasswd' => ['bar'],
    '/home/youruser/.htpasswds/mydomain.com/baz.htpasswd' => ['baz1', 'baz2'], // 複数スラッグ対応
    '/home/youruser/.htpasswds/mydomain.com/secret-page.htpasswd' => ['secret-page'],
  ];

  $request_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

  function send_auth_request($message = 'Authentication required.') {
    header('WWW-Authenticate: Basic realm="Restricted Page"');
    header('HTTP/1.0 401 Unauthorized');
    echo $message;
    exit;
  }

  // .htpasswd ファイルとそれに対応するスラッグの配列をループ
  foreach ($htpasswd_map as $htpasswd_file => $slugs) {
    // スラッグの配列をループ
    foreach ($slugs as $slug) {
      if (preg_match('#/' . preg_quote($slug, '#') . '/?$#', $request_path)) {

        // Basic 認証ロジック(前のコードと同じ)
        if (!isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
          send_auth_request();
        }

        if (!file_exists($htpasswd_file) || !is_readable($htpasswd_file)) {
          error_log("htpasswd file not found or not readable: $htpasswd_file");
          header('HTTP/1.0 500 Internal Server Error');
          echo 'Server configuration error.';
          exit;
        }

        $lines = file($htpasswd_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        if ($lines === false) {
          error_log("Failed to read htpasswd file: $htpasswd_file");
          header('HTTP/1.0 500 Internal Server Error');
          echo 'Server error.';
          exit;
        }

        $username = $_SERVER['PHP_AUTH_USER'];
        $password = $_SERVER['PHP_AUTH_PW'];
        $authenticated = false;

        foreach ($lines as $line) {
          list($user, $hash) = explode(':', $line, 2);
          if ($user === $username && password_verify($password, $hash)) {
            $authenticated = true;
            break;
          }
        }

        if (!$authenticated) {
          send_auth_request('Invalid credentials.');
        }

        break 2; // スラッグにマッチしたらすべてのループを終了
      }
    }
  }
});

上記構造のメリット

  • 同じパスを複数回書く必要がない
  • baz3 を追加したいときも ['baz1', 'baz2', 'baz3'] に追記するだけ
  • .htpasswd ファイル単位でスラッグをまとめて管理できるため、設定が視覚的にわかりやすい

条件分岐タグを使って特定のページに Basic 認証

これまでの例では、アクセスされたページの URL のパス部分が指定されたスラッグに一致するかで対象ページを判定していますが、以下は is_page()is_single() などの条件分岐タグを使って、特定の投稿・ページにだけ Basic 認証をかける例です。

add_action('template_redirect', function () {
  // 認証をかけたいページ
  $slug = 'foo';
  $post_type = 'page';

  // Basic認証のユーザー情報
  $user = 'username';
  $pass = 'password';

  // 対象判定
  $is_target = false;
  if ($post_type === 'page' && is_page($slug)) {
    $is_target = true;
  } elseif ($post_type === 'post' && is_single($slug)) {
    $is_target = true;
  }

  if ($is_target) {
    if (
      !isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) ||
      $_SERVER['PHP_AUTH_USER'] !== $user ||
      $_SERVER['PHP_AUTH_PW'] !== $pass
    ) {
      header('WWW-Authenticate: Basic realm="Restricted Login Page"');
      header('HTTP/1.0 401 Unauthorized');
      echo 'Authentication required.';
      exit;
    }
  }
});

対象判定の分岐が増える場合は、以下のように switch を使えば簡潔に書けます(上記11-16行目)。

switch ($post_type) {
  case 'page':
    $is_target = is_page($slug);
    break;
  case 'post':
    $is_target = is_single($slug);
    break;
}

特定のカテゴリーに Basic 認証をかける

以下は指定したカテゴリーに属する投稿に Basic 認証をかける例です。

判定では、まず、is_single() を使って投稿だけを対象とします。そして get_queried_object() で投稿オブジェクトを取得して has_category() の第2引数に指定して、カテゴリ判定をしています。

add_action('template_redirect', function () {
  // 認証対象カテゴリのスラッグ(配列で複数指定可能)
  $cat_slugs = ['foo'];

  // 認証情報
  $user = 'username';
  $pass = 'password';

  // 投稿であることを確認し、対象カテゴリに属しているか判定
  if (is_single()) {

    // 現在のリクエストに対応するオブジェクト(投稿・ページなど)を取得
    $post = get_queried_object();

    // $post が投稿であることを確認し、指定したカテゴリに属しているかをチェック
    if ($post instanceof WP_Post && has_category($cat_slugs, $post)) {
      if (
        !isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) ||
        $_SERVER['PHP_AUTH_USER'] !== $user ||
        $_SERVER['PHP_AUTH_PW'] !== $pass
      ) {
        header('WWW-Authenticate: Basic realm="Restricted Login Page"');
        header('HTTP/1.0 401 Unauthorized');
        echo 'Authentication required.';
        exit;
      }
    }
  }
});

投稿オブジェクトを明示的に取得する

has_category() などの多くのテンプレートタグは、現在の「投稿ループ内」や「グローバルな $post オブジェクト」に依存して動作します。しかし、template_redirect アクションの段階ではまだ「ループ」は始まっていないので、has_category('foo') のように書いた場合、WordPress が現在の投稿は何かを特定できないことがあり、意図した動作にならないことがあります。

has_category() は投稿が指定カテゴリに属しているかを調べる関数で、第2引数に調べたい投稿を渡すことで、明示的にチェック対象を指定できます。

そのため、get_queried_object() で現在の投稿を取得し、has_category() の第2引数に指定しています。

複数ページ対応

以下は複数の固定ページ・投稿に対して Basic 認証をかけるコードです。各固定ページや投稿ごとに、スラッグと投稿タイプを指定し、それぞれに対して認証が可能です。

$targets 配列に、スラッグと投稿タイプのペアを追加します。この例の場合、username と password は全ページ共通です。

add_action('template_redirect', function () {
  // 認証対象のページ・投稿の一覧(スラッグ => 投稿タイプ)
  $targets = [
    'foo' => 'page',
    'bar' => 'post',
    'secret-page' => 'page',
    'hidden-post' => 'post',
  ];

  // 認証情報(全対象で共通)
  $username = 'username';
  $password = 'password';

  // 現在のページが認証対象か判定
  foreach ($targets as $slug => $post_type) {
    if (
      ($post_type === 'page' && is_page($slug)) ||
      ($post_type === 'post' && is_single($slug))
    ) {
      // 認証情報のチェック
      if (
        !isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) ||
        $_SERVER['PHP_AUTH_USER'] !== $username ||
        $_SERVER['PHP_AUTH_PW'] !== $password
      ) {
        header('WWW-Authenticate: Basic realm="Restricted Area"');
        header('HTTP/1.0 401 Unauthorized');
        echo 'Authentication required.';
        exit;
      }

      break; // 対象が見つかって処理したらループ終了
    }
  }
});

.htpasswd を使った Basic 認証

以下は前述のコードと同様ですが、認証情報をコードに直接記述せず、.htpasswd ファイルで管理する例です。

add_action('template_redirect', function () {
  // 認証対象のページ・投稿の一覧(スラッグ => 投稿タイプ)
  $targets = [
    'foo' => 'page',
    'bar' => 'post',
    'secret-page' => 'page',
    'hidden-post' => 'post',
  ];

  // .htpasswdファイルの絶対パス(実環境に合わせて変更してください)
  $htpasswd_file = '/home/youruser/.htpasswds/mydomain.com/passwd/.htpasswd';

  // HTTP認証ヘッダーを返す関数
  function send_auth_request($message = 'Authentication required.') {
    header('WWW-Authenticate: Basic realm="Restricted Login Page"');
    header('HTTP/1.0 401 Unauthorized');
    echo $message;
    exit;
  }

  // 現在のページが認証対象か判定
  foreach ($targets as $slug => $post_type) {
    $is_target = false;

    if ($post_type === 'page' && is_page($slug)) {
      $is_target = true;
    } elseif ($post_type === 'post' && is_single($slug)) {
      $is_target = true;
    }

    if ($is_target) {
      // 認証ヘッダーが送られていない場合、認証を要求
      if (!isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
        send_auth_request();
      }

      // ブラウザから送信されたユーザー名とパスワードを取得
      $username = $_SERVER['PHP_AUTH_USER'];
      $password = $_SERVER['PHP_AUTH_PW'];
      $authenticated = false;

      // .htpasswd ファイルの存在と読み込み権限を確認
      if (!file_exists($htpasswd_file) || !is_readable($htpasswd_file)) {
        error_log("htpasswd file not found or not readable: $htpasswd_file");
        header('HTTP/1.0 500 Internal Server Error');
        echo 'Server configuration error.';
        exit;
      }

      // .htpasswd の各行を取得(空行や改行を除く)
      $lines = file($htpasswd_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

      // .htpasswd ファイルの読み込みに失敗した場合
      if ($lines === false) {
        error_log("Failed to load htpasswd file despite file_exists(): $htpasswd_file");
        header('HTTP/1.0 500 Internal Server Error');
        echo 'Server error.';
        exit;
      }

      // .htpasswd ファイルの各行(ユーザー名:ハッシュ)を処理して、該当するユーザー名があるか確認
      foreach ($lines as $line) {
        // ユーザー名とハッシュに分割
        list($user, $hash) = explode(':', $line, 2);

        // 入力ユーザー名が一致し、パスワードも正しければ認証成功
        if ($username === $user && password_verify($password, $hash)) {
          $authenticated = true;
          break;
        }
      }

      // 認証失敗時は再度認証を要求
      if (!$authenticated) {
        send_auth_request('Invalid credentials.');
      }

      break; // 認証対象ページに一致し処理が完了したので終了
    }
  }
});

以下は「特定のカテゴリーに Basic 認証をかける」のコードを .htpasswd ファイルで管理するように変更したものです。

add_action('template_redirect', function () {
  // 認証対象カテゴリのスラッグ(配列で複数指定可能)
  $cat_slugs = ['concrete5', 'inmotion'];

  // .htpasswdファイルの絶対パス(実環境に合わせて変更してください)
  $htpasswd_file = '/home/youruser/.htpasswds/mydomain.com/passwd/.htpasswd';

  // HTTP認証ヘッダーを返す関数
  function send_auth_request($message = 'Authentication required.') {
    header('WWW-Authenticate: Basic realm="Restricted Login Page"');
    header('HTTP/1.0 401 Unauthorized');
    echo $message;
    exit;
  }

  // 投稿であることを確認し、対象カテゴリに属しているか判定
  if (is_single()) {

    // 現在のリクエストに対応するオブジェクト(投稿・ページなど)を取得
    $post = get_queried_object();

    // $post が投稿であることを確認し、指定したカテゴリに属しているかをチェック
    if ($post instanceof WP_Post && has_category($cat_slugs, $post)) {
      // 認証ヘッダーが送られていない場合、認証を要求
      if (!isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
        send_auth_request();
      }

      // ブラウザから送信されたユーザー名とパスワードを取得
      $username = $_SERVER['PHP_AUTH_USER'];
      $password = $_SERVER['PHP_AUTH_PW'];
      $authenticated = false;

      // .htpasswd ファイルの存在と読み込み権限を確認
      if (!file_exists($htpasswd_file) || !is_readable($htpasswd_file)) {
        error_log("htpasswd file not found or not readable: $htpasswd_file");
        header('HTTP/1.0 500 Internal Server Error');
        echo 'Server configuration error.';
        exit;
      }

      // .htpasswd の各行を取得(空行や改行を除く)
      $lines = file($htpasswd_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

      // .htpasswd ファイルの読み込みに失敗した場合
      if ($lines === false) {
        error_log("Failed to load htpasswd file despite file_exists(): $htpasswd_file");
        header('HTTP/1.0 500 Internal Server Error');
        echo 'Server error.';
        exit;
      }

      // .htpasswd ファイルの各行(ユーザー名:ハッシュ)を処理して、該当するユーザー名があるか確認
      foreach ($lines as $line) {
        // ユーザー名とハッシュに分割
        list($user, $hash) = explode(':', $line, 2);
        // 入力ユーザー名が一致し、パスワードも正しければ認証成功
        if ($username === $user && password_verify($password, $hash)) {
          $authenticated = true;
          break;
        }
      }

      // 認証失敗時は再度認証を要求
      if (!$authenticated) {
        send_auth_request('Invalid credentials.');
      }
    }
  }
});

PHP での Basic 認証のメリット・デメリット

PHP コードで Basic 認証を実装する場合と、.htaccess(Apacheの機能)を使う場合には、それぞれメリット・デメリットや注意点があります。以下に両者を比較して整理します。

メリット

PHP で Basic 認証を実装する場合のメリットとしては以下のようなものがあります。

項目 説明
柔軟な制御が可能 投稿タイプ、スラッグ、ログインユーザー、日付、カスタムフィールドなど、WordPress の条件分岐タグ(is_page()など)を使って動的に制御できる。
.htpasswd に依存しない構成も可能 WordPress 管理画面内にユーザーや認証情報を登録し、PHP 側で照合するような独自認証システムも構築可能。
WordPress 環境と連携可能 WordPress 内で完結でき、wp-config.php や .env などと連携しやすい。認証後に WordPress 関数やテンプレートタグを使って処理を分岐できる。
サーバー設定不要 .htaccess やサーバー設定ファイルの編集権限がない共有サーバーなどでも使える。

デメリット

PHP で Basic 認証を実装する場合のデメリットとしては以下のようなものがあります。

項目 説明
処理コストが高い 毎回 WordPress を起動して template_redirect や init などのフックまで進む必要がある。
PHP が動く前に制御できない .htaccess による認証は Apache の段階でアクセスをブロックできるが、PHP 方式はすでに WordPress が起動した後の処理になる。そのため、認証失敗でも WordPress 本体の起動、クエリ実行、テンプレート読込が始まっている可能性がある。
ファイル単位で保護できない 画像や PDF などの静的ファイルには適用できない。これらには PHP による Basic 認証は無効。
HTTP 認証の挙動がブラウザ依存 PHP 経由の Basic 認証は、失敗後に再認証されない、認証解除が難しいなど、挙動がブラウザやセキュリティ設定に依存する問題が出ることもある。

.htaccess での Basic 認証のメリット・デメリット

メリット

.htaccess で Basic 認証を実装する場合のメリットとしては以下のようなものがあります。

項目 説明
軽量かつ高速 PHPを介さず、Apacheがリクエスト段階でブロックするため、高速でサーバー負荷も低い。
静的ファイルも保護可能 HTML、画像、PDFなどあらゆるファイルをディレクトリ単位で保護できる。
攻撃対象を絞らせない PHP が動く前に止めるため、悪意あるスクリプト実行やボットの負荷を減らせる。
セキュリティが標準的 OS レベルのセキュリティポリシーに近いため、堅牢性が高い。

デメリット

.htaccess で Basic 認証を実装する場合のデメリットとしては以下のようなものがあります。

項目 説明
条件分岐ができない WordPress のような CMS 的な動的条件による保護ができない(例:カテゴリが X の投稿だけ認証など)。
柔軟性が低い 認証対象をプログラム的に制御できない。
ユーザーやパスワードの管理が手動 .htpasswd を使う必要がある。UI 管理は不可。動的なユーザー登録・削除には不向き。

どちらを使うべきか?

要件(目的) 推奨手段
高セキュリティ(静的ファイル保護・リソース保護) .htaccess による Basic 認証
柔軟に条件を変更したい PHP による Basic 認証
複数の条件やログイン済み判定などと組み合わせたい PHP による制御が便利
両方のメリットを取り入れたい .htaccess + PHP 連携やログイン認証と組み合わせた仕組み