SSL化したら画像・CSSが表示されない(混在コンテンツ)の対処法

実測 に検証

SSL 証明書を入れて https でアクセスできるようになった。でも画像が出ない、 デザインが崩れる、鍵マークに警告が付く。

原因はページは https なのに、中で読んでいるリソースが http のままという 状態です。ブラウザがそれをブロックします。これを混在コンテンツと呼びます。

最初にやること: どこに http が残っているか数える

修正の前に、どのテーブルに何件あるかを把握します。 場所によって対処が変わります。

実測環境で数えた結果です。

場所件数
wp_posts.post_content(記事本文)2
wp_posts.guid42
wp_options7
wp_postmeta0
SELECT 'post_content' AS place, COUNT(*) FROM wp_posts WHERE post_content LIKE '%http://%'
UNION ALL SELECT 'guid',        COUNT(*) FROM wp_posts WHERE guid LIKE '%http://%'
UNION ALL SELECT 'options',     COUNT(*) FROM wp_options WHERE option_value LIKE '%http://%'
UNION ALL SELECT 'postmeta',    COUNT(*) FROM wp_postmeta WHERE meta_value LIKE '%http://%';

guid が圧倒的に多いのが普通です。そして guid置換してはいけない列です(後述)。

置換は SQL の REPLACE() ではなく WP-CLI で

wp_optionswp_postmeta には直列化されたデータ(PHP の serialize())が 入っています。直列化データは文字列の長さを内部に持っているため、 SQL の REPLACE() で文字数が変わると壊れます。

a:1:{s:19:"http://example.test";}
     ↑ この 19 が実際の長さと合わなくなると読めなくなる

WP-CLI の search-replace は直列化を解いて置換し直します。 実測すると、対象が直列化データかどうかが Type 列に出ます。

$ wp search-replace 'http://example.com' 'https://example.com' --all-tables --dry-run
Table       Column         Replacements  Type
wp_options  option_value   2             PHP     ← 直列化データとして処理
wp_posts    post_content   2             SQL
wp_posts    guid           42            SQL
wp_users    user_url       1             SQL
Success: 47 replacements to be made.

TypePHP の行は、SQL の REPLACE() では壊れていた箇所です。

guid は除外する

guid は投稿を一意に識別するための値で、RSS リーダーが「同じ記事か」を 判断するのに使います。変更すると、購読者側で全記事が新着として再配信されます。

実測では、除外すると件数が大きく変わりました。

置換件数
そのまま47 件
--skip-columns=guid5 件

guid は表示には使われないので、http のままで問題ありません。

wp search-replace 'http://example.com' 'https://example.com' \
  --all-tables --skip-columns=guid --dry-run

必ず --dry-run で件数を確認してから実行します。 そして実行前にデータベースのバックアップを取ります。

落とし穴: wp-config.php の定数

データベースを置換しても URL が変わらないことがあります。 wp-config.php に定数があると、データベースの値は無視されます。

実測です。DB を書き換えても実効値は変わりませんでした。

DB の siteurl / homehttp://127.0.0.1:8080
実際に使われた値http://localhost:8080
// これがあると DB より優先される
define( 'WP_HOME', 'http://example.com' );
define( 'WP_SITEURL', 'http://example.com' );

管理画面の「設定 > 一般」で URL 欄がグレーアウトしている場合は、 この定数があります。https に書き換える場所は wp-config.php です。

プロキシ配下では WordPress が https だと気づかない

ロードバランサ・CDN・リバースプロキシが SSL を終端する構成では、 サーバーには http で届きます。WordPress は「自分は http で動いている」と 判断し、https のページなのに http の URL を出力し続けます。

DB を置換しても直りません。wp-config.php に次を入れます。

// wp-settings.php を読み込む行より前に置く
if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
	$_SERVER['HTTPS'] = 'on';
}

これを入れずに .htaccess で https に強制すると、 無限リダイレクトになります。リダイレクトを繰り返す

ブロックされる/されないの違い

すべての http リソースが同じ扱いになるわけではありません。

リソースhttps ページから http で読むと
JS / CSS / iframeブロックされる(動かない・デザインが崩れる)
画像 / 音声 / 動画ブラウザが自動で https に変えて試す(失敗すれば表示されない)
リンク(<a href>)ブロックされない(警告も出ない)

JS と CSS が最も影響が大きいので、「SSL 化したら崩れた」は まずこの 2 つを確認します。 → CSS が効かない・JavaScript が動かない

コンソールに出るメッセージはこの形です。

Mixed Content: The page at 'https://example.com/' was loaded over HTTPS,
but requested an insecure stylesheet 'http://example.com/style.css'.
This request has been blocked.

検出方法

検証環境では HTTPS のリスナーを用意していないため、 ブラウザがブロックする様子そのものは再現できていません。 検出と置換の部分だけが実測値です。ここは分けて書いておきます。

実際のサイトでの確認方法は 2 つです。

1. ブラウザのコンソール

Mixed Content で検索します。ブロックされたリソースの URL が出ます。

2. ページの HTML を直接見る

curl -s https://example.com/ | grep -oE 'http://[^"'"'"' ]*' | sort -u | head -20

自ドメインの http:// が出てきたら要修正です。 外部サービスの http:// は、そのサービスが https に対応しているか確認します。

置換しても残るもの

データベースの置換で直らない場所があります。

場所対処
テーマ・プラグインのコードに直書きされた URLファイルを検索して修正(grep -r "http://" wp-content/themes/)
ウィジェット内の HTML直列化データなので search-replace で対応できる
外部サービスの埋め込み(古い広告タグ、地図、SNS)提供元の https 版に差し替える
CDN の設定CDN 側のオリジン URL を https に
キャッシュ置換後にキャッシュを全消去する(古い HTML が残る)

置換したのに直らない場合、まずキャッシュを疑います。 CDN・キャッシュプラグイン・ブラウザの 3 層すべてです。

作業の順番

  1. バックアップを取る(DB とファイル)
  2. wp-config.php の定数を確認する(あれば https に書き換える)
  3. プロキシ配下なら X-Forwarded-Proto の対応を入れる
  4. --dry-run で件数を確認する(--skip-columns=guid
  5. 置換を実行する
  6. キャッシュを全消去する
  7. コンソールとアセットを確認して、残りを個別に直す
  8. 最後に http → https のリダイレクトを 1 箇所だけ設定する

8 を先にやるとループするので最後です。

再現手順(検出・置換の部分)

# どこに何件あるか
mysql -e "SELECT COUNT(*) FROM wp_posts WHERE post_content LIKE '%http://%';"

# 置換件数の確認(直列化データは Type=PHP と表示される)
wp search-replace 'http://localhost:8080' 'https://localhost:8080' \
  --all-tables --dry-run --report-changed-only
# → 47 件

wp search-replace 'http://localhost:8080' 'https://localhost:8080' \
  --all-tables --dry-run --report-changed-only --skip-columns=guid
# → 5 件