0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

覚書2

Last updated at Posted at 2025-10-21

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) : '';
});

使い方(ページ本文・テンプレート)

  1. 本文(ブロックエディタでもOK)

セッションID: [intra_session]
カテゴリー: [intra_category]

[intra_if set="session"]
このブロックは「session ヘッダーが来ているときだけ」表示されます。
[/intra_if]

  1. テーマ(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 に)。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?