Умный редирект на HTTPS и каноничный хост с или без WWW через .htaccess

В современном вебе стоит использовать HTTPS, и желательно настроить автоматический редирект на HTTPS, если у пользователя достаточно свежий браузер. Предлагаю конфиг для Apache, который используется на моих сайтах с 2020 года, пройдя через несколько небольших доработок.

Фишки этого конфига:

  • Авторедирект на каноничный хост, или автоматическое удаление или добавление WWW из хоста.
  • Если сайт открыт через HTTPS, то автоматически настраиваются Upgrade-Insecure-Requests (все ресурсы, ссылки на которые указаны через HTTP, будут всё равно загружены через HTTPS) и Strict-Transport-Security (браузер всегда будет открывать сайт по HTTPS).
  • Если браузер слишком старый и не поддерживает Upgrade-Insecure-Requests или Let's Encrypt, то редирект на HTTPS не производится, сайт продолжает работать по HTTP.
  • Пользователь может установить принудительный редирект на HTTPS (forcehttps=1), отключить автоматический редирект на HTTPS (forcehttps=0), или вернуть автоматический режим (forcehttps=auto), передав любой странице по любому протоколу параметр forcehttps с нужным значением. Выбор запоминается в cookies.

Код для .htaccess

Поместите следующий код в самое начало файла .htaccess в корне вашего сайта. Требуется Apache 2.4+ с включёнными модулями mod_rewrite, mod_headers и mod_setenvif.

RewriteEngine On
RewriteBase /

# ----------------------------------------------------------------------------------------------------------------------
# Force HTTPS and canonical host if required.
# ----------------------------------------------------------------------------------------------------------------------

# Set canonical host.
RewriteRule ^ - [E=FORCE_HOST:%{HTTP_HOST}]

# Detect the current request scheme. REQUEST_SCHEME is not reliable when Apache is behind a reverse proxy.
RewriteRule ^ - [E=REQ_SCHEME:http]
RewriteCond %{HTTPS} ^on$ [NC,OR]
RewriteCond %{HTTP:X-Forwarded-Proto} https [NC,NV]
RewriteRule ^ - [E=REQ_SCHEME:https]

# Force canonical host. We should do it before we try to set any cookies or any other redirects.
RewriteCond expr "%{ENV:FORCE_HOST} != '' && tolower(%{HTTP_HOST}) != tolower(%{ENV:FORCE_HOST})"
RewriteRule ^/?(.*)$ %{ENV:REQ_SCHEME}://%{ENV:FORCE_HOST}/$1 [NE,R=301,END]

# Enforce HTTPS if forcehttps cookie is 1. Disable automatic HTTPS upgrade if forcehttps cookie is 0.
RewriteCond %{HTTP_COOKIE} (?:^|;)\s*forcehttps=([01])\s*(?:;|$)
RewriteRule ^ - [E=AUTO_HTTPS:%1]

# Prefer HTTPS if the browser supports Upgrade-Insecure-Requests and trusts Let's Encrypt's "ISRG Root X1" certificate:
# - OS: Windows 7+ (2018/07/31), Android 7.1.1+ (2017), iOS 10+ (2017), MacOS 10.12.1+ (2016).
# - Browsers: Firefox 50+ (2016), Chrome 108+ (2022, Windows/MacOS) 114+ (2023, Linux/ChromeOS) 115+ (2023, Android).
RewriteCond %{ENV:AUTO_HTTPS} ^$
RewriteCond %{HTTP:Upgrade-Insecure-Requests} ^1$
RewriteCond %{HTTP_USER_AGENT} !Android\s([1-6]\.|7\.0|7\.1\.0)
RewriteCond %{HTTP_USER_AGENT} !Windows\sNT\s(5\.|6\.0)
RewriteCond %{HTTP_USER_AGENT} !Mac\sOS\sX\s10[_.]1[0-2]
RewriteCond %{HTTP_USER_AGENT} !(iPhone|iPad);\sCPU\s(iPhone\s)?OS\s[1-9][_.]
RewriteCond %{HTTP_USER_AGENT} !Firefox\/[1-4]?[0-9]\.
RewriteCond %{HTTP_USER_AGENT} !Chrome\/([1-9]?[0-9]|10[0-7])\.
RewriteCond %{HTTP_USER_AGENT} !Android.+Chrome\/(10[8-9]|11[0-4])\.
RewriteRule ^ - [E=AUTO_HTTPS:1]

# Upgrade request scheme to HTTPS if AUTO_HTTPS is allowed.
RewriteRule ^ - [E=URL_SCHEME:%{ENV:REQ_SCHEME}]
RewriteCond %{ENV:AUTO_HTTPS} ^1$
RewriteRule ^ - [E=URL_SCHEME:https]

# Set or remove the forcehttps cookie depending on the forcehttps URL parameter.
RewriteCond %{QUERY_STRING} (?:^|&)forcehttps=(?:auto|-|)(?:&|$)
RewriteRule ^ - [E=SET_FORCEHTTPS_COOKIE:-,E=URL_SCHEME:http]
RewriteCond %{QUERY_STRING} (?:^|&)forcehttps=0(?:&|$)
RewriteRule ^ - [E=SET_FORCEHTTPS_COOKIE:0,E=URL_SCHEME:http]
RewriteCond %{QUERY_STRING} (?:^|&)forcehttps=1(?:&|$)
RewriteRule ^ - [E=SET_FORCEHTTPS_COOKIE:1,E=URL_SCHEME:https]

# Set canonical prefix.
RewriteRule ^ - [E=URL_PREFIX:%{ENV:URL_SCHEME}://%{HTTP_HOST}]

# Remove the forcehttps URL parameter.
RewriteCond %{QUERY_STRING} ^(.*&|)forcehttps=(?:auto|[-01]|)(?:&(.*)|)$
RewriteRule ^/?(.*)$ %{ENV:URL_PREFIX}/$1?%1%2 [NE,R=302,END]

# Redirect to HTTPS or HTTP if desired scheme is different from used in this request.
RewriteCond expr "%{ENV:REQ_SCHEME} != %{ENV:URL_SCHEME}"
RewriteRule ^/?(.*)$ %{ENV:URL_PREFIX}/$1 [NE,R=302,END]

# Strip the "REDIRECT_" prefix added to environment variable names after internal URL rewrites.
SetEnvIf REDIRECT_REQ_SCHEME (.+) REQ_SCHEME=$1
SetEnvIf REDIRECT_URL_SCHEME (.+) URL_SCHEME=$1
SetEnvIf REDIRECT_URL_PREFIX (.+) URL_PREFIX=$1
SetEnvIf REDIRECT_AUTO_HTTPS (.+) AUTO_HTTPS=$1

# Use the modern Max-Age cookie attribute. In old browsers like IE11, they will be stored until the browser is closed.
# Use 34560000 seconds (400 days) for "permanent" cookies. Chrome 104+ doesn't allow more.
# According to RFC 6265, a host-only cookie, which is not sent to subdomains, should not have a Domain attribute.
Header always add Set-Cookie "forcehttps=; Path=/; Max-Age=-1"          "expr= '%{ENV:SET_FORCEHTTPS_COOKIE}' == '-'"
Header always add Set-Cookie "forcehttps=0; Path=/; Max-Age=34560000"   "expr= '%{ENV:SET_FORCEHTTPS_COOKIE}' == '0'"
Header always add Set-Cookie "forcehttps=1; Path=/; Max-Age=34560000"   "expr= '%{ENV:SET_FORCEHTTPS_COOKIE}' == '1'"

# Send HSTS header if HTTPS is in use and HTTPS enforcement is allowed. Clear HSTS when redirecting from HTTPS to HTTP.
# HSTS support: IE11+ (2015), Firefox 4+ (2011), Chrome 4+ (2010), Safari 7+ (2013).
Header always set Strict-Transport-Security "max-age=34560000" \
	"expr= '%{ENV:REQ_SCHEME}' == 'https' && '%{ENV:URL_SCHEME}' == 'https' && '%{ENV:AUTO_HTTPS}' != '0'"
Header always set Strict-Transport-Security "max-age=0" \
	"expr= '%{ENV:REQ_SCHEME}' == 'https' && '%{ENV:URL_SCHEME}' == 'http'"

# Send CSP UIR header if HTTPS is in use.
# UIR support: Edge 17+ (2018), Firefox 48+ (2016), Chrome 44+ (2015), Safari 10.1+ (2017).
Header always set Content-Security-Policy "upgrade-insecure-requests" \
	"expr= '%{ENV:REQ_SCHEME}' == 'https' && '%{ENV:URL_SCHEME}' == 'https'"

# NOTE: Use %{ENV:URL_PREFIX} or %{ENV:URL_SCHEME} in all subsequent redirects.

# ----------------------------------------------------------------------------------------------------------------------

В самом начале в переменную FORCE_HOST заносится каноничный хост, куда попутно будет сделан авторедирект при необходимости. В примере выше туда заносится просто текущий хост, то есть функция редиректа на каноничный хост отключена:

RewriteRule ^ - [E=FORCE_HOST:%{HTTP_HOST}]

Если вы хотите перенаправлять пользователя на хост без WWW, используйте такой вариант:

RewriteCond %{HTTP_HOST} ^(?:www\.)+(.+)$ [NC]
RewriteRule ^ - [E=FORCE_HOST:%1]

Либо используйте такой вариант для автоматического добавления WWW к текущему хосту:

RewriteCond %{HTTP_HOST} ^(?:www\.)*(.+)$ [NC]
RewriteRule ^ - [E=FORCE_HOST:www.%1]

Если у вас несколько алиасов, и все должны вести на каноничный хост, задавайте его таким образом:

RewriteRule ^ - [E=FORCE_HOST:example.com]

Если этот же конфиг используется на локальной версии сайта, можно отключить автоматический редирект для локальных хостов, задавая AUTO_HTTPS=0 сразу после задания каноничного FORCE_HOST, используя следующий сниппет:

# Disable automatic HTTPS upgrade for local and non-public host names, or IP addresses.
RewriteCond %{HTTP_HOST} \.(?:localhost|local|test)$ [NC,OR]
RewriteCond %{HTTP_HOST} !\.[a-z]+$ [NC]
RewriteRule ^ - [E=AUTO_HTTPS:0,E=FORCE_HOST:%{HTTP_HOST}]

Если вам нужно добавить какие-то редиректы внутри сайта, то их следует размещать в самом конце. Используйте переменную %{ENV:URL_PREFIX} вместо явного указания протокола и хоста (она содержит строку вида https://example.com), или переменную %{ENV:URL_SCHEME} в качестве предпочитаемого протокола (там будет либо http, либо https).

DNS-запись HTTPS

Помимо умного редиректа, также рекомендуется создать DNS-запись HTTPS для вашего домена. При наличии такой DNS-записи, современные браузеры, поддерживающие эту DNS-запись, смогут сразу обращаться к вашему сайту по HTTPS, минуя HTTP и редирект. Достаточно создать DNS-запись типа HTTPS с содержимым 1 . (да, просто единица и точка) для вашего домена. Если ваш сервер поддерживает HTTP/2, можете указать 1 . alpn="h2", чтобы браузер сразу знал, что можно ожидать HTTP/2. Если же у вас поддерживается и HTTP/3, то укажите 1 . alpn="h2,h3" — это особенно актуально для HTTP/3, так как эта информация позволит браузеру сэкономить немного времени и избежать установки лишнего TCP-соединения при первом подключении.

При наличии этой DNS-записи, современные браузеры будут автоматически переходить на HTTPS независимо от параметра forcehttps, поэтому отключение такого перехода через forcehttps=0 будет полезно прежде всего для старых браузеров.

Комментарии временно закрыты. Обновите эту страницу через минуту.