Умный редирект на 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 (forcessl=1), принудительно отключить редирект (forcessl=0), или вернуть автоматический режим (forcessl=auto), передав любой странице по любому протоколу параметр forcessl с нужным значением. Выбор запоминается в 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 current scheme. REQUEST_SCHEME is not reliable since Apache might be behind Nginx.
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]

# Allow UIR if Upgrade-Insecure-Requests is in the request headers.
# UIR support: IE18+ (2018), Firefox 48+ (2016), Chrome 44+ (2015), Safari 10.1+ (2017, MacOS 10.10+).
RewriteRule ^ - [E=PERMIT_UIR:0]
RewriteCond %{HTTP:Upgrade-Insecure-Requests} ^1$
RewriteRule ^ - [E=PERMIT_UIR:1]

# Allow Strict-Transport-Security if UIR is supported, and browser trusts Let's Encrypt's "ISRG Root X1":
# - 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).
# STS support: IE11+ (2015), Firefox 4+ (2011), Chrome 4+ (2010), Safari 7+ (2013), Android 4.4+ (2013).
# Windows XP: Firefox 52- (2017), Chrome 49- (2016), IE8. Windows 7-8.1: Firefox 115- (2023), Chrome 109- (2023), IE11.
RewriteRule ^ - [E=PERMIT_STS:0]
RewriteCond %{ENV:PERMIT_UIR} ^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=PERMIT_STS:1]

# Force HTTPS if STS is allowed and forcessl cookie is not 0.
RewriteRule ^ - [E=URL_SCHEME:%{ENV:REQ_SCHEME}]
RewriteCond %{ENV:PERMIT_STS} ^1$
RewriteCond %{HTTP_COOKIE} !(?:^|;)\s*forcessl=0\s*(?:;|$)
RewriteRule ^ - [E=URL_SCHEME:https]

# Use HTTPS if forcessl cookie is 1.
RewriteCond %{HTTP_COOKIE}  (?:^|;)\s*forcessl=1\s*(?:;|$)
RewriteRule ^ - [E=URL_SCHEME:https]

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

# Set forcessl=1 if STS isn't allowed, forcessl cookie isn't set, and current scheme is HTTPS.
# RewriteCond %{ENV:REQ_SCHEME} ^https$
# RewriteCond %{ENV:PERMIT_STS} !^1$
# RewriteCond %{HTTP_COOKIE} !(?:^|;)\s*forcessl=[01]\s*(?:;|$)
# RewriteCond %{ENV:SET_FORCESSL_COOKIE} !^[-01]$
# RewriteRule ^ - [E=SET_FORCESSL_COOKIE:1]

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

# Decide if we want to send STS and UIR headers. Never send them when current request is HTTP.
RewriteCond %{ENV:REQ_SCHEME} ^https$
RewriteRule ^ - [E=SET_FORCESTS_HEADER:0,E=SET_FORCEUIR_HEADER:0]
RewriteCond %{ENV:REQ_SCHEME} ^https$
RewriteCond %{ENV:URL_SCHEME} ^https$
RewriteRule ^ - [E=SET_FORCESTS_HEADER:%{ENV:PERMIT_STS},E=SET_FORCEUIR_HEADER:%{ENV:PERMIT_UIR}]

# Try to fix stupid adding of the "REDIRECT_" prefix to environment variable names on URL rewrite.
SetEnvIf REDIRECT_PERMIT_UIR            (.+) PERMIT_UIR=$1
SetEnvIf REDIRECT_PERMIT_STS            (.+) PERMIT_STS=$1
SetEnvIf REDIRECT_REQ_SCHEME            (.+) REQ_SCHEME=$1
SetEnvIf REDIRECT_URL_SCHEME            (.+) URL_SCHEME=$1
SetEnvIf REDIRECT_URL_PREFIX            (.+) URL_PREFIX=$1
SetEnvIf REDIRECT_SET_FORCESSL_COOKIE   (.+) SET_FORCESSL_COOKIE=$1
SetEnvIf REDIRECT_SET_FORCESTS_HEADER   (.+) SET_FORCESTS_HEADER=$1
SetEnvIf REDIRECT_SET_FORCEUIR_HEADER   (.+) SET_FORCEUIR_HEADER=$1

# Send cookies in modern format with Max-Age. In old browsers like IE11 they will be stored until the browser is closed.
# According to RFC 6265, "host-only" cookie (which won't be sent to subdomains) shouldn't have "Domain=" attribute.
Header always add Set-Cookie "forcessl=; Path=/; Max-Age=-1"            "expr= '%{ENV:SET_FORCESSL_COOKIE}' == '-'"
Header always add Set-Cookie "forcessl=0; Path=/; Max-Age=34560000"     "expr= '%{ENV:SET_FORCESSL_COOKIE}' == '0'"
Header always add Set-Cookie "forcessl=1; Path=/; Max-Age=34560000"     "expr= '%{ENV:SET_FORCESSL_COOKIE}' == '1'"

# Send the STS and UIR headers if required. 2592000 seconds = 30 days.
Header always set Strict-Transport-Security "max-age=2592000"           "expr= '%{ENV:SET_FORCESTS_HEADER}' == '1'"
Header always set Strict-Transport-Security "max-age=0"                 "expr= '%{ENV:SET_FORCESTS_HEADER}' == '0'"
Header always set Content-Security-Policy "upgrade-insecure-requests"   "expr= '%{ENV:SET_FORCEUIR_HEADER}' == '1'"

# Remove the forcessl URL parameter.
RewriteCond %{QUERY_STRING} ^(.*&|)forcessl=(?: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 %{ENV:REQ_SCHEME}:%{ENV:URL_SCHEME} !^(.+):\1$
RewriteRule ^/?(.*)$ %{ENV:URL_PREFIX}/$1 [NE,R=302,END]

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

В самом начале в переменную 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]

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

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