WordPress全ページを「社内ヘッダーが無いと見られない」にする最小実装(共有鍵なし・通行証クッキー方式)
要約:最初のアクセスだけ、社内アプリから X-Intra-Session と X-Intra-Dept をヘッダーで付けてWPに入る。WPは署名付きクッキー(通行証)を発行し、以後はヘッダー無しでも全ページ閲覧OK。ヘッダーもクッキーも無いアクセスは外部ログインURLへ302リダイレクト。
⸻
こんな時に使う
• 「共有キー/HMACは要らない。とにかくヘッダーが来てたら通すだけでいい」と言われた
• でも静的ファイル(CSS/JS/画像)も含めて全ページを閉じたい
• 外部アプリ(Flutter/Firebaseログイン済み)とはヘッダー2本だけのゆるい連携でOK
⸻
仕組み(図解イメージ)
1. 初回アクセス(社内アプリ → WP)
X-Intra-Session, X-Intra-Dept ヘッダー付き
→ WPが通行証クッキーを発行(署名はサーバ内の秘密鍵で実施/外部共有不要)
2. 以後のアクセス(ブラウザ)
クッキーが自動送信されるので、ヘッダー無しで閲覧OK
3. どちらも無ければ、外部ログインURLに302リダイレクト
⸻
設置手順
1. wp-content/mu-plugins/ を作成(無ければ)
2. intra-guard-cookie.php を新規作成し、以下のコードを全貼り
3. INTRA_LOGIN_URL と INTRA_GATE_SECRET を自環境に合わせて変更
<?php
/**
* Plugin Name: Intra Header Gate (Site-wide, Cookie Pass)
* Description: 初回は指定ヘッダーがある時だけ通し、以降は署名付きクッキーで全ページを閲覧可にする。共有鍵は外部不要(サーバ内のみで署名)。
* Version: 1.0.0
*/
declare(strict_types=1);
// ====================== 設定 ======================
const INTRA_LOGIN_URL = 'https://example.com/app-login'; // 外部ログインURL
const REQUIRED_HEADERS = [ 'HTTP_X_INTRA_SESSION', 'HTTP_X_INTRA_DEPT' ]; // 必須ヘッダー
const INTRA_GATE_COOKIE = 'wp_intra_gate'; // 通行証クッキー名
const INTRA_GATE_TTL_SEC = 7200; // 通行証の有効期限(秒)
const INTRA_GATE_SECRET = 'change-this-to-64char-random'; // サーバ内だけの秘密鍵(外部と共有しない)
// 例外パス(WP運用のため最低限残す/必要に応じて空配列に)
const EXEMPT_PATHS = [
'/wp-login.php',
'/wp-cron.php',
'/wp-admin/', // 管理画面まで閉じるならコメントアウトを外す→保護対象にする
'/wp-json/', // APIを閉じるならコメントアウトを外す→保護対象にする
'/robots.txt',
'/favicon.ico',
'/sitemap.xml',
];
// ====================== ユーティリティ ======================
function ig_path(): string {
$uri = $_SERVER['REQUEST_URI'] ?? '/';
$path = parse_url($uri, PHP_URL_PATH) ?: '/';
return $path;
}
function ig_is_exempt(): bool {
$path = ig_path();
foreach (EXEMPT_PATHS as $ex) {
if ($ex === '') continue;
if (str_ends_with($ex, '/')) {
if (str_starts_with($path, $ex)) return true;
} else {
if ($path === $ex) return true;
}
}
return false;
}
function ig_headers_ok(): bool {
foreach (REQUIRED_HEADERS as $hk) {
if (!isset($_SERVER[$hk]) || $_SERVER[$hk] === '') return false;
}
return true;
}
function ig_base64url_encode(string $bin): string {
return rtrim(strtr(base64_encode($bin), '+/', '-_'), '=');
}
function ig_base64url_decode(string $txt): string|false {
$re = strtr($txt, '-_', '+/');
$pad = strlen($re) % 4;
if ($pad) $re .= str_repeat('=', 4 - $pad);
$out = base64_decode($re, true);
return $out === false ? false : $out;
}
// 署名付きトークン作成 {dept, iat, exp}
function ig_make_token(string $dept, int $ttl): string {
$now = time();
$payload = json_encode(['dept'=>$dept, 'iat'=>$now, 'exp'=>$now + $ttl], JSON_UNESCAPED_SLASHES);
$p64 = ig_base64url_encode($payload);
$sig = hash_hmac('sha256', $p64, INTRA_GATE_SECRET, true);
$s64 = ig_base64url_encode($sig);
return $p64 . '.' . $s64;
}
function ig_verify_token(string $token): array|false {
$parts = explode('.', $token);
if (count($parts) !== 2) return false;
[$p64, $s64] = $parts;
$payload = ig_base64url_decode($p64);
$sig = ig_base64url_decode($s64);
if ($payload === false || $sig === false) return false;
$expect = hash_hmac('sha256', $p64, INTRA_GATE_SECRET, true);
if (!hash_equals($expect, $sig)) return false;
$data = json_decode($payload, true);
if (!is_array($data) || !isset($data['exp'])) return false;
if (time() > (int)$data['exp']) return false;
return $data;
}
function ig_set_gate_cookie(string $dept): void {
$token = ig_make_token($dept, INTRA_GATE_TTL_SEC);
// Cookie 属性は運用に合わせて SameSite/Lax/Strict を調整
$params = [
'expires' => time() + INTRA_GATE_TTL_SEC,
'path' => '/',
'secure' => is_ssl(),
'httponly' => true,
'samesite' => 'Lax',
];
setcookie(INTRA_GATE_COOKIE, $token, $params);
}
function ig_redirect_login_and_exit(): void {
$scheme = is_ssl() ? 'https://' : 'http://';
$current = $scheme . $_SERVER['HTTP_HOST'] . ($_SERVER['REQUEST_URI'] ?? '/');
wp_redirect(INTRA_LOGIN_URL . '?return=' . rawurlencode($current), 302);
exit;
}
// ====================== 本体:全リクエストをガード ======================
add_action('template_redirect', function () {
// 例外(ログイン/管理/cron/APIなど)を除けば、全ページ保護
if (ig_is_exempt()) return;
// 1) すでに通行証クッキーがあればOK
$cookie = $_COOKIE[INTRA_GATE_COOKIE] ?? '';
if (is_string($cookie) && $cookie !== '') {
$tok = ig_verify_token($cookie);
if ($tok !== false) {
// 任意:部門をテンプレで使えるように
set_query_var('intra_dept', sanitize_text_field((string)($tok['dept'] ?? '')));
return; // 表示許可
}
}
// 2) 初回:ヘッダーが揃っていればOK + 通行証クッキー発行
if (ig_headers_ok()) {
$dept = sanitize_text_field((string)($_SERVER['HTTP_X_INTRA_DEPT'] ?? ''));
ig_set_gate_cookie($dept);
set_query_var('intra_dept', $dept);
return; // 表示許可
}
// 3) どちらも無ければ外部ログインへ
ig_redirect_login_and_exit();
}, 1); // 早めに実行
// ====================== 便利ショートコード ======================
// [intra_dept] 部門表示
add_shortcode('intra_dept', function () {
$dept = get_query_var('intra_dept');
return $dept ? esc_html((string)$dept) : '';
});
⸻
外部アプリ(Flutterなど)からの呼び方
• 初回アクセスだけ、下の2ヘッダーを付けて /(どのURLでも可)にGET
• X-Intra-Session: 任意の文字列
• X-Intra-Dept: 部門コード(例:SALES, HR など)
• ブラウザは通行証クッキーを保持するので、以降はヘッダー不要
OK例:初回(通行証クッキー発行)
curl -I https://your-site.example.com/
-H "X-Intra-Session: abc123"
-H "X-Intra-Dept: SALES"
NG例:ヘッダーもクッキーも無し → 302で外部ログインへ
curl -I https://your-site.example.com/
Flutter(概念コード):
final headers = {
'X-Intra-Session': 'abc123',
'X-Intra-Dept': 'SALES',
};
// WebViewやhttp.getで最初の1回だけ上記ヘッダーを付けてアクセス
⸻
オプション調整ポイント
• INTRA_GATE_TTL_SEC:通行証の有効期限(秒)
• EXEMPT_PATHS:編集の都合で開けておくURL(/wp-admin/ 等)。
全閉鎖したいなら空配列に。ただし自分の作業が困難になるので注意。
• クッキーの SameSite/secure は運用に合わせて調整
⸻
よくあるハマり
• CDN/プロキシが X-Intra-* を落とす → 初回が通らない
→ その経路でカスタムヘッダーを許可する設定をONに
• 有効期限切れで見られない → 初回と同じ手順で再入場(ヘッダー付きアクセス)
• 管理画面まで閉じた → EXEMPT_PATHS に /wp-admin/ を戻す
⸻
⸻
WordPressで「HTTPヘッダーの session / category を受け取って使う」だけの最小実装
目的:X-Intra-Session と X-Intra-Category(ヘッダー)を WordPress で受け取り、
テンプレートや本文で表示・分岐に利用できるようにする。認証も署名も不要。
⸻
設置手順
1. wp-content/mu-plugins/ フォルダを作成(無ければ)
2. intra-headers-min.php を新規作成し、下記コードを丸ごとコピペ
3. 以上(mu-plugins は有効化不要で自動読込)
3.
<?php
/**
* Plugin Name: Intra Headers MIN
* Description: セキュリティ無し。HTTPヘッダーの session / category を受け取って、テンプレ・本文で使えるようにするだけの最小実装。
* Version: 1.0.0
*/
declare(strict_types=1);
/**
* 想定する受信ヘッダー(外部アプリ側で付ける)
* X-Intra-Session: セッションID等
* X-Intra-Category: カテゴリー(部門コード等でもOK)
*
* PHP の $_SERVER では下記のキーになる:
* HTTP_X_INTRA_SESSION
* HTTP_X_INTRA_CATEGORY
*/
// ---- 設定(必要なら変更)----
const INTRA_HEADER_SESSION_KEY = 'HTTP_X_INTRA_SESSION';
const INTRA_HEADER_CATEGORY_KEY = 'HTTP_X_INTRA_CATEGORY';
// もし「X-Intra-Dept」を category の代わりに使いたい場合のフォールバック(不要なら false)
const INTRA_FALLBACK_DEPT_AS_CATEGORY = true; // HTTP_X_INTRA_DEPT があれば category として採用
// ヘッダーが来なかったときに「リダイレクト」したい場合のみ true にし、URL を設定
const INTRA_REDIRECT_IF_MISSING = false;
const INTRA_REDIRECT_URL = 'https://example.com/app-login'; // 使うときだけ有効なURLに
// ---- ヘッダーを受け取り、テンプレ/本文で参照できるように query_var へ反映 ----
add_action('init', function () {
add_filter('query_vars', function (array $vars) {
$vars[] = 'intra_session';
$vars[] = 'intra_category';
return $vars;
});
});
add_action('template_redirect', function () {
// 受信
$session = isset($_SERVER[INTRA_HEADER_SESSION_KEY]) ? (string)$_SERVER[INTRA_HEADER_SESSION_KEY] : '';
$category = isset($_SERVER[INTRA_HEADER_CATEGORY_KEY]) ? (string)$_SERVER[INTRA_HEADER_CATEGORY_KEY] : '';
// フォールバック:X-Intra-Dept を category として採用(任意)
if ($category === '' && INTRA_FALLBACK_DEPT_AS_CATEGORY && isset($_SERVER['HTTP_X_INTRA_DEPT'])) {
$category = (string)$_SERVER['HTTP_X_INTRA_DEPT'];
}
// クエリ変数へ(テンプレ/本文/ショートコードで使える)
set_query_var('intra_session', sanitize_text_field($session));
set_query_var('intra_category', sanitize_text_field($category));
// 必須扱いにして未指定ならリダイレクトしたい場合(任意)
if (INTRA_REDIRECT_IF_MISSING && ($session === '' || $category === '')) {
$current = (is_ssl() ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . ($_SERVER['REQUEST_URI'] ?? '/');
wp_redirect(INTRA_REDIRECT_URL . '?return=' . rawurlencode($current), 302);
exit;
}
}, 1);
// ---- テンプレ/本文から直接使えるヘルパ関数 ----
function intra_get_session(): string { return (string)get_query_var('intra_session', ''); }
function intra_get_category(): string { return (string)get_query_var('intra_category', ''); }
// ---- ショートコード ----
// [intra_session] → ヘッダーの session をそのまま表示
add_shortcode('intra_session', function () {
$v = intra_get_session();
return $v !== '' ? esc_html($v) : '';
});
// [intra_category] → ヘッダーの category(or dept)を表示
add_shortcode('intra_category', function () {
$v = intra_get_category();
return $v !== '' ? esc_html($v) : '';
});
// [intra_if set="session|category"]...[/intra_if]
// 指定値が「空でなければ」中身を表示(単純な有無判定)
add_shortcode('intra_if', function ($atts, $content = '') {
$set = isset($atts['set']) ? (string)$atts['set'] : '';
$val = '';
if ($set === 'session') $val = intra_get_session();
if ($set === 'category') $val = intra_get_category();
return ($val !== '') ? do_shortcode($content) : '';
});
⸻
使い方(ページ本文・テンプレート)
- 本文(ブロックエディタでもOK)
セッションID: [intra_session]
カテゴリー: [intra_category]
[intra_if set="session"]
このブロックは「session ヘッダーが来ているときだけ」表示されます。
[/intra_if]
- テーマ(PHP テンプレート)
<?php
$session = intra_get_session(); // 例: "abc123"
$category = intra_get_category(); // 例: "SALES"
if ($session !== '') {
echo '<p>Session: ' . esc_html($session) . '</p>';
}
if ($category !== '') {
echo '<p>Category: ' . esc_html($category) . '</p>';
}
// 単純な出し分け
if ($category === 'SALES') {
// 営業向けの表示
} else if ($category === 'HR') {
// 人事向けの表示
}
⸻
動作確認(curl例)
OK(値が見える)
curl https://your-site.example.com/path
-H "X-Intra-Session: abc123"
-H "X-Intra-Category: SALES"
未指定(値は空のまま。リダイレクトを有効にした場合のみ 302 で飛ぶ)
curl -I https://your-site.example.com/path
⸻
よくある補足
• プロキシ/CDNでカスタムヘッダーが落ちると値が見えません。
経路で X-Intra-* を通す設定が必要な場合があります。
• category の代わりに X-Intra-Dept を使う環境では、
コードは既定で dept → category フォールバックをONにしてあります(不要なら false に)。
⸻