Настраиваем HTTP-заголовки веб-сервера
Ненужные заголовки
Посмотреть заголовки веб-сервера можно командой: curl -I https://yandex.ru/ или вы можете протестировать на сайте https://http.itsoft.ru
Давайте для начала посмотрим на заголовки Яндекс и Google.
[igor@new html]$ curl -I https://yandex.ru/ HTTP/1.1 200 Ok Accept-CH: Viewport-Width, DPR, Device-Memory, RTT, Downlink, ECT Accept-CH-Lifetime: 31536000 Cache-Control: no-cache,no-store,max-age=0,must-revalidate Content-Length: 178283 Content-Security-Policy: report-uri https://csp.yandex.net/csp?project=morda&from=morda.big.ru&showid=1587028014.80097.85411.54444&h=stable-morda-man-yp-628&csp=new&date=20200416&yandexuid=4225126811587028014;script-src 'self' 'unsafe-inline' https://an.yandex.ru https://yastatic.net https://yandex.ru https://mc.yandex.ru;img-src https://yabs.yandex.ru https://favicon.yandex.net https://*.strm.yandex.net https://auto.ru https://an.yandex.ru https://strm.yandex.ru https://www.maximonline.ru 'self' https://thequestion.ru https://www.kinopoisk.ru https://yastatic.net https://awaps.yandex.net https://avatars.mds.yandex.net https://*.verify.yandex.ru https://mc.yandex.ru https://leonardo.edadeal.io data: https://resize.yandex.net https://yandex.ru https://mc.admetrica.ru;frame-src 'self' https://mc.yandex.ru https://yandex.ru https://yastatic.net https://st.yandexadexchange.net https://yandexadexchange.net;font-src data: https://an.yandex.ru https://yastatic.net;object-src https://avatars.mds.yandex.net;connect-src https://mc.yandex.ru https://*.cdn.ngenix.net https://games.yandex.ru https://mc.admetrica.ru https://yandex.ru https://portal-xiva.yandex.net https://auto.ru https://*.strm.yandex.net https://strm.yandex.ru https://an.yandex.ru https://mobile.yandex.net https://frontend.vh.yandex.ru https://yabs.yandex.ru https://thequestion.ru https://yastat.net https://api.market.yandex.ru 'self' https://www.kinopoisk.ru https://yastatic.net https://zen.yandex.ru https://www.maximonline.ru wss://portal-xiva.yandex.net;style-src 'unsafe-inline' https://yastatic.net;media-src https://*.strm.yandex.net https://*.cdn.ngenix.net blob:;default-src https://yastatic.net https://yastat.net Content-Type: text/html; charset=UTF-8 Date: Thu, 16 Apr 2020 09:06:54 GMT Expires: Thu, 16 Apr 2020 09:06:55 GMT Last-Modified: Thu, 16 Apr 2020 09:06:55 GMT P3P: policyref="/w3c/p3p.xml", CP="NON DSP ADM DEV PSD IVDo OUR IND STP PHY PRE NAV UNI" Set-Cookie: yp=1589620015.ygu.1; Expires=Sun, 14-Apr-2030 09:06:54 GMT; Domain=.yandex.ru; Path=/ Set-Cookie: mda=0; Expires=Fri, 14-Aug-2020 09:06:54 GMT; Domain=.yandex.ru; Path=/ Set-Cookie: yandex_gid=213; Expires=Sat, 16-May-2020 09:06:54 GMT; Domain=.yandex.ru; Path=/ Set-Cookie: yandexuid=4225126811587028014; Expires=Sun, 14-Apr-2030 09:06:54 GMT; Domain=.yandex.ru; Path=/ Set-Cookie: i=8UHqdTah3NgMTINJMRGysflDelT4++ntHkOo85SGs6jvHhQQwro524Vs41lApmiL/8bGRA1ZejVwI0VKHneCfAPOMrg=; Expires=Sun, 14-Apr-2030 09:06:54 GMT; Domain=.yandex.ru; Path=/; Secure; HttpOnly X-Content-Type-Options: nosniff X-Frame-Options: DENY X-Yandex-Sdch-Disable: 1 [igor@new html]$ curl -I https://google.com/ HTTP/1.1 301 Moved Permanently Location: https://www.google.com/ Content-Type: text/html; charset=UTF-8 Date: Thu, 16 Apr 2020 09:08:18 GMT Expires: Sat, 16 May 2020 09:08:18 GMT Cache-Control: public, max-age=2592000 Server: gws Content-Length: 220 X-XSS-Protection: 0 X-Frame-Options: SAMEORIGIN Alt-Svc: quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,h3-T050=":443"; ma=2592000 [igor@new html]$ curl -I https://www.google.com/ HTTP/1.1 200 OK Date: Thu, 16 Apr 2020 09:08:35 GMT Expires: -1 Cache-Control: private, max-age=0 Content-Type: text/html; charset=ISO-8859-1 P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info." Server: gws X-XSS-Protection: 0 X-Frame-Options: SAMEORIGIN Set-Cookie: 1P_JAR=2020-04-16-09; expires=Sat, 16-May-2020 09:08:35 GMT; path=/; domain=.google.com; Secure Set-Cookie: NID=202=QkgyPv5FrpwHWbJGnSC83K1Sw4wysE4IMVNbd3z83iNOheMyVdbxmJJJE0AFbs9eNH9_iKGAzqdSv6iUqfr-erOlOIap7MmRMOkqQr87iS2y_FyII7AlV5Jx-K4JC2_b8F9xyJYxPpOL2hP-81Msp_HndCCDtGSi33l4gYZ3oXU; expires=Fri, 16-Oct-2020 09:08:35 GMT; path=/; domain=.google.com; HttpOnly Transfer-Encoding: chunked Alt-Svc: quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,h3-T050=":443"; ma=2592000 Accept-Ranges: none Vary: Accept-Encoding
Также вам может пригодиться утилита telnet для просмотра заголовков локального сервера, если при обращении через доменное имя отвечает прокси-сервер nginx.
[igor@new html]$ telnet 192.168.29.35 80 Trying 192.168.29.35... Connected to 192.168.29.35. Escape character is '^]'. HEAD / HTTP/1.1 HOST: itsoft.ru HTTP/1.1 200 OK Date: Tue, 05 May 2020 07:37:23 GMT Server: Apache/2.4.6 (CentOS) Cache-Control: max-age=604800, public Last-Modified: Mon, 27 Apr 2020 12:28:01 GMT Etag: 273b847f0f077f7680a9e0d54a43c8a6 Set-Cookie: a=1144943031; expires=Wed, 06-May-2020 07:37:34 GMT; path=/ X-Robots-Tag: noindex X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block; report=/feedback/http-csp-report.php Strict-Transport-Security: max-age=63072000; includeSubDomains; preload Referrer-Policy: strict-origin Feature-Policy: autoplay 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; usb 'none'; vibrate 'none'; Content-Security-Policy-Report-Only: default-src 'self' data: *.googletagmanager.com *.google.com *.google-analytics.com *.googleapis.com *.gstatic.com *.doubleclick.net mc.yandex.ru api-maps.yandex.ru yastatic.net webvisor.com *.calltouch.ru *.usedesk.ru *.youtube.com *.ytimg.com 'nonce-counters' 'report-sample'; img-src https: data: blob: ; report-uri /feedback/http-csp-report.php Content-Type: text/html; charset=utf-8 Connection closed by foreign host.
Что мы видим?! Нет у Яндекс никаких Server: ***, X-Powered-By: PHP/5.4.16
У Google есть Server. И у Гугла есть бесполезный редирект на домен третьего уровня www, который в России давно умер. Приятно, что здесь в России больше логики, чем на Западе.
Никакого смысла сообщать какой у вас сервер и какое ПО (программное обеспечение) на нём используется нет. Наоборот, это вредно, т.к. если на сайте используется устаревшее ПО, то хакерам,
автоматическим сканерам это только поможет во взломе вашего сайта. К таким заголовкам относится ещё Via, который могут добавлять прокси-сервера.
Заголовок Server в Apache можно заменить перекомпилировав исходный код. Заголовок находится в файле include/ap_release.h. Но это сложно.
Можно сократить информацию до просто Apache с помощью настройки ServerTokens Prod.
Можно с помощью mod_security добавить в конфигурацию строку:
SecServerSignature "ITSOFT"
Чтобы убрать X-Powered-By в
php.ini установите set expose_php = Off
или в самом php-файле вызовите header_remove("X-Powered-By");
Expires: — лишён давно смысла, т.к. в RFC2616 page-127 записано: "Note: if a response includes a Cache-Control field with the max-
age directive (see section 14.9.3), that directive overrides the
Expires field." Cache-Control: max-age=0 переопределяет Expires. О кешировании мы поговорим отдельно ниже.
Если expires отключён, то браузер отправляет "if-modified-since" и веб-сервер сможет ответить 304 Not Modified.
ExpiresActive Off в .htaccess отключает заголовок Expires.'
Сжатие контента
Не все пользователи Интернет сидят на широполосных каналах. Может барахлить связь за счёт удаления от сотовой вышки, перегрузки каналов, большого расстояние, поэтому необходимо включить сжатие текстовых файлов. Картинки сжимать смысла нет, они и так уже сжаты.
<ifModule mod_deflate.c> AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml application/xhtml+xml text/css text/javascript application/javascript application/x-javascript </ifModule>
Заголовки для безопасности
В настройки VirtualHost полезно добавить:
Header set X-Frame-Options DENY
Этот заголовок указывает браузеру, что сайт запрещено загружать в <frame>, <iframe>, <embed> or <object>.
Это должно защитить пользователя от Clickjacking-атак.
В настройки VirtualHost полезно ещё добавить:
Header set X-Content-Type-Options nosniff
Этот заголовок указывает браузеру, что не нужно пытаться понять какой контент пришёл и исполнить его. Например, не нужно выполнять javascript, который пришёл как plain\text.
Подробнее см. тут.
Следующая опция включит в браузере защиту и блокировку от XSS-атак.
Header set X-XSS-Protection "1; mode=block"
X-XSS-Protection: 1; report=https://itsoft.ru/r.php можно задать URL куда браузер отправит отчёт в случае обнаружения атаки.
Очень важная настройка запрещает браузеру коннектиться к сайту по HTTP, т.е. обязывает использоваться только HTTPS. О важности SSL было рассказано в статье Зачем нужны SSL-сертификаты.
Header set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
- max-age указывает, что браузер должен запомнить данный заголовок на два года.
- includeSubDomains говорит, что это правило распространяется и на поддомены.
- preload — это гугловская фишка, в спецификации отсутствует, домен попадёт в базу, которую используют Chrome, Firefox и Safari.
Опция Referrer-Policy указывает браузеру как отправлять HTTP_REFERER. Позволяет скрыть данные QUERY_STRING. Может принимать следующие значения:
- no-referrer — referer передаваться не будет, наверное, это для интравертов и каких-то секретных закрытых сайтов.
- no-referrer-when-downgrade — так браузер ведёт по-умолчанию. Полный URL с QUERY_STRING посылается в случаях перехода с HTTP→HTTP, HTTPS→HTTPS, HTTP→HTTPS. И не посылается при переходы из HTTPS→HTTP.
- origin — посылается только доменное имя.
- origin-when-cross-origin — посылается полностью при переходе по внутренним ссылкам, в прочих случаях посылается — только доменное имя.
- same-origin — посылается полностью при переходе по внутренним ссылкам, в прочих случаях не посылается.
- strict-origin — посылается только доменное имя, но только когда одинаковые протоколы HTTP→HTTP, HTTPS→HTTPS.
- strict-origin-when-cross-origin — посылается полностью при переходе по внутренним ссылкам, посылается только доменное имя, но только когда одинаковые протоколы HTTP→HTTP, HTTPS→HTTPS.
Заголовок Feature-Policy указывает браузеру какими возможностями будет пользоваться сайт. К сожалению, некоторые значения можно переопределить в коде сайта.
Например, так <iframe src="https://site.com" allow="vibrate">. Но заголовок сам по себе очень правильный.
Мы все испытываем боль с приложениями на смартфоне, которые запрашивают привилегий доступа сильно больше, чем им реально нужно.
Боль вызывают и сайты, которые хотят слать нотификации, спрашивают геолокацию, издают звуки и т.п., что нас раздражает.
Сайт как и приложение смартфона должен заранее честно предупредить о том, какую информацию он собирается запрашивать, какую точно не будет запрашивать.
Заголовок имеет следующий синтакис: Feature-Policy: <directive> <allowlist>
allowlist может принимать одно или несколько из следующих значений разделённых пробелом:
- * — разрешено на данной странице и во всех iframe независимо оттого, какой сайт будет загружен в этих ифреймах;
- 'self' — разрешено на данной странице и во всех iframe, если в iframe загружена страница с нашего сайта;
- 'src' — только для iframe, только если в iframe загружена страница с нашего сайта;
- 'none' — запрещено, наиболее разумное значение для многих опций :), сайт должен по-минимуму раздражать посетителя;
- <origin(s)> — разрешает directive для определённого сайта или сайтов, сайты разделяются пробелами.
Ниже идёт список возможных настроек:
- accelerometer — разрешено ли использовать документу Accelerometer интерфейс;
- ambient-light-sensor — может ли документ собирать информацию об освещённости AmbientLightSensor интерфейс$
- autoplay — разрешено и автопроигрывание через HTMLMediaElement;
- battery — Navigator.getBattery();
- camera — можно ли использовать камеру;
- display-capture — можно ли использовать getDisplayMedia();
- document-domain — можно ли установить document.domain;
- encrypted-media — можно ли использовать Encrypted Media Extensions API (EME), Navigator.requestMediaKeySystemAccess();
- execution-while-not-rendered — можно ли исполнять задачи в когда iframe невидим или display: none;
- execution-while-out-of-viewport — выполнять ли задачи для элементов за пределами отображаемой области;
- fullscreen — разрешено ли использовать Element.requestFullScreen();
- geolocation — разрешено ли запрашивать геолокацию getCurrentPosition();
- gyroscope — можно ли запрашивать положение устройства (смартфона);
- layout-animations — можно ли отображать послойную анимацию;
- legacy-image-formats — разрешено ли отображать изображения в устаревших форматах;
- magnetometer — Magnetometer интерфейс;
- microphone — микрофон;
- midi — Web MIDI API, воспроизведение музыки;
- navigation-override — https://www.w3.org/TR/css-nav/
- oversized-images — можно ли отображать большие изображения, данная опция полезна на стадии отладки и тестирования сайта;
- payment — Payment Request API;
- picture-in-picture — можно ли проигрывать видео в режиме Picture-in-Picture;
- publickey-credentials — можно ли использовать Web Authentication API;
- sync-xhr — можно ли делать XMLHttpRequest requests;
- unoptimized-lossy-images — отображать ли неоптимизированные изображения;
- unsized-media — изображения, видео должны иметь размеры;
- usb — WebUSB API;
- vibrate — не поддерживается в браузерах;
- vr — WebVR API, но на смену ему уже идёт WebXR;
- wake-lock — можно ли использовать Wake Lock API, должно ли устройство переходить в режим энергосбережения
- xr-spatial-tracking — WebXR Device API.
Некоторые возможности (accelerometer, ambient-light-sensor, battery, display-capture, encrypted-media, execution-while-not-rendered, execution-while-out-of-viewport, fullscreen, gyroscope, layout-animations, magnetometer, navigation-override, picture-in-picture, publickey-credentials, vr, wake-lock, xr-spatial-tracking ) вообще непонятно зачем ограничивать.
Следующие опции полезно отключить, чтобы показать, что мы не вторгаемся в личное пространство посетителя и ничего не навязываем ему: autoplay, camera, geolocation, gyroscope,
magnetometer, microphone, midi, payment, usb, vibrate.
Header set Feature-Policy "autoplay 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; usb 'none'; vibrate 'none';"
Для отладки и тестирования сайта полезными могут быть следующие опции: legacy-image-formats, oversized-images, unoptimized-lossy-images, unsized-media.
Но на продакшене их не стоит использовать.
Header set Feature-Policy "unsized-media 'none'; oversized-images 'none'; unoptimized-lossy-images 'none'; unoptimized-lossless-images 'none';"
Заголовок Content Security Policy (CSP) это дополнительный уровень безопасности, который помогает обнаруживать и смягчать некоторые типы атак, включая межсайтовый скриптинг (XSS) и data injection атак. Данный заголовок на самом деле не только защищает от исполнения встроенного JS-кода хакерами, но главным образом дисциплинирует разработчиков не разбрасывать JS-код где попало по тексту HTML-документа, не встраивать его в HTML-теги, не использовать eval(), а хранить javascript упорядоченно в подключаемых библиотеках JS-файлов. Тем самым реализуется полное разделение HTML и javascript, и мы жёстко запрещаем плохие практики разработки. Также мы уменьшаем зависимость нашего сайта от контента на других сайтах. Неприятно же когда на странице на отображается картинка, не исполняется JS, не отображаются шрифты, по причине того, что они была подгружены с другого сайта, а теперь их там удалили. Далее рассмотрим примеры возможных настроек.
Content-Security-Policy: default-src 'self' — подгрузка файлов только с домена самого сайта, даже с поддоменов нельзя.
Content-Security-Policy: default-src 'self' *.trusted-site.ru — подгрузка файлов только с домена самого сайта и с поддоменов указанного сайта.
Content-Security-Policy: default-src 'self'; img-src *; media-src mysite.ru mysite.org; script-src trusted-lib.com — подгрузка файлов с домена самого сайта, картинок с любых сайтов (это уже плохая практика), картинок и видео с сайтов mysite.ru mysite.org, скрипты можно загружать с сайта trusted-lib.com.
Content-Security-Policy: default-src https://itsoft.ru — подгрузка файлов только с указанного домена и обязательно по протоколу https.
Content-Security-Policy: default-src 'self' *.oursite.ru; img-src * — подгрузка файлов только с домена самого сайта и поддменов *.oursite.ru, картинок откуда угодно.
Content-Security-Policy: default-src 'self'; report-uri https://itsoft.ru/report-csp.php — указывается скрипт, куда браузер будет отправлять отчёты о нарушении правил.
Отчёт будет отправлен методом POST в формате JSON. В нашем скрипте можно просто перенаправить этот отчёт в почту или Slack разработчикам.
На первое время можно задать правила в заголовке Content-Security-Policy-Report-Only, чтобы получать только отчеты.
Content-Security-Policy-Report-Only: default-src 'self'; report-uri https://itsoft.ru/report-csp.php
Список возможных директив:
- default-src
- script-src
- object-src
- style-src
- img-src
- media-src
- frame-src
- font-src
- connect-src
Чтобы запретить использование объектов нужно добавить object-src 'none'. Для script-src можно использовать значение unsafe-inline и unsafe-eval. Это разрешит использование встроенных скриптов, ухудшит дисциплину разработчиков, гарантирует бардак в коде. Для style-src можно использовать unsafe-inline. Лучше эти значения не использовать для новых проектов.
Content-Security-Policy: upgrade-insecure-requests — это аналог Strict-Transport-Security.
Заголовки Last-Modified и Content-Length
Заголовки Last-Modified и Content-Length сообщаю когда документ был изменён последний раз и его размер. Поисковые роботы и браузеры отправляют в запросе заголовок If-Modified-Since, на который веб-сервер может либо выдать новую страницу либо 304 Not Modified. Для статических файлов эти параметры определяются легко.
Content-Type: image/png Content-Length: 5922 Last-Modified: Mon, 27 Jan 2020 10:48:16 GMT
Сложнее с динамическими файлами, например с php. ETag взаимосвязан с датой последней модификации и размером содержимого. Поэтому всё же определять эти значения придётся. Во всех инструкциях по настройке кеширования сайта, которые нам удалось прочитать, этому вопросу не уделялось внимание. Предполагалось, что дата последней модификации известна. Однако это не так. И это очень непростой вопрос. Проблема в том, что дата последней моификации файла зависит от всех подключаемых php-файлов, от данных в базе данных, от заголовков запроса браузера, кук, IP-адреса посетителя, курсов валют, времени, сторонних сервисов.
Для php можно использовать следующий код, который выдаст дату последней модификации всех подключаемых файлов:
<?php
function getLastModified() {
$all = get_included_files();
$all = array_filter($all, "is_file");
$lms = array_map('filemtime', $all);
$lm = max($lms);
return $lm;
}
?>
Может изменяться сам код php-файла, а данные и html-код которые он выдаёт могут не измениться. В этом случае неправильно говорить, что Last-Modified изменился. Но для простоты можно считать, что любое изменение файла ведёт к изменению Last-Modified. Для данных мы должны ориентироваться на дату изменения данных в строке таблицы. Эта задача может быть нетривиальной для страниц, где отображается множество данных из разных таблиц. Но если страница отображает одну сущность, например, конкретную новость, статью, счёт, платёж, то там дата последней модификации известна. Для страниц, где собирается множество данных из разных таблиц нужно иметь таблицу в которой lastmodified соответствующей строки будет обновляться триггером.
Можно ориентироваться на хеш от выдаваемых данных. Если хеш md5 изменился, то меняем Last-Modified. Эту задачу лучше всего поручить проксирующему серверу Nginx.
Для проверки ответов веб-сервер вы можете испоьзовать следующие команды:
- curl -I --header 'If-Modified-Since: Mon, 30 Apr 2029 18:13:12 GMT' https://itsoft.ru
- curl -I --header 'If-None-Match: "a49f2da878be351c6c73a1ec0524d8ea"' https://itsoft.ru
- curl -I --header 'Accept-Encoding: gzip' https://itsoft.ru
Заголовки для кеширования
Когда веб-сервер отдаёт какой-нибудь файл: картинку, css, js или контент страницы, то он должен сказать сколько можно хранить эти данные в кеше. Картинки можно хранить там вечно, они никогда не изменяются. Многие любят выдавать запрет на кеширование в виде заголовков:
Cache-Control: no-cache,no-store,max-age=0,must-revalidate Expires: -1 Expires: Thu, 19 Nov 1981 08:52:00 GMT Pragma: no-cache
На самом деле, смысла в этом никакого нет для большинства данных, только нагрузка на сервер. И не надо бояться сказать, что файл или страницу сайта можно закешировать. Во-первых, потому что браузер при следующем обращении к этому контенту при условии, что время хранения кеша просрочено спросит: "а не изменился ли ресурс с момента последнего моего запроса". И если не изменился, то веб-сервер должен вернуть заголовок 304 Not Modified и не отправлять запрашиваемый файл. Во-вторых, пользователь всегда может нажать обновить страницу и принудительно послать запрос к серверу не уточняя изменился ресурс или нет. Важно только правильно определить время хранения кеша. В решение этого вопроса можно исходить из того как часто обновляются данные и насколько это важные данные. Что случится, если посетитель увидит обновление с опозданием. Какое это разумное опоздание? Подавляющее большинство страниц в интернете чаще умирает, чем обновляется. Обновляются ленты новостей, а страница с конкретной новостью живёт вечно. Поэтому для подобных страниц можно смело выставить время жизни веша сутки или неделю, т.е. то разумное время в которое посетитель может вернуться на эту страницу или ваш сайт.
Итак, давайте рассмотрим как браузер спросит не изменился ли запрашиваемый ресурс. Для этого есть два варианта. Первый — отправить в запросе If-Modified-Since с датой предыдущего запроса. Второй — отправить в запросе If-None-Match с меткой ETag, которую он получил при предыдущем обращении.
При ответе веб-серве посылает следующие заголовки:
ETag: "1722-59d1cd83fc41f" Cache-Control: max-age=31536000, public
ETag — является уникальной меткой, а Cache-Control сообщает можно ли хранить кеш на промежуточных прокси-сервера и сколько его можно хранить.
Итак, самый последний вопрос в который мы упёрлись — это сколько можно хранить кеш. На этот вопрос мы должны ответить в зависимости от контента и конкретных страниц нашего сайта. Для статических файлов max-age=31536000 в один год представляется вполне разумным. При этом сами файлы нужно либо подключать с параметром времени последней модификации либо изменять их имя после модификации. Для вывода параметра последней модификации файла можете использовать функцию:
function printFilenameWithTimestamp($filename)
{
print "$filename?t=" . date('U', filemtime($_SERVER['DOCUMENT_ROOT'] . $filename));
}
В .htaccess укажите:
Header set Cache-Control "max-age=31536000, public"
В заголовой php-скрипта добавьте следующий код если он не обращается к базе и не зависит от других сервисов.
$last_modified = gmdate('D, d M Y H:i:s', getlastmod());
$etag = md5($last_modified);
if($_SERVER['REQUEST_METHOD']=='GET')
{
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && ($_SERVER['HTTP_IF_NONE_MATCH'] == $etag))
{
header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified');
exit();
}
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime(substr($_SERVER['HTTP_IF_MODIFIED_SINCE'], 5))>=strtotime("$last_modified GMT"))
{
header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified');
exit;
}
}
header('Cache-Control: max-age=604800, public');
header("Last-Modified: $last_modified GMT");
header("Etag: $etag");
Vary: Accept-Encoding
Vary: Accept-Encoding по сути обязательный заголовок, т.к. сейчас все веб-серверы поддерживают свжатие контента. Способ сжатия зависит от поддерживаемых браузером способов, которые браузер передаёт в заголовке Accept-Encoding. Но скорее всегого сервер добавит этот заголовок автоматически получив в запросе Accept-Encoding: gzip, deflate, br.
Итог
[root@localhost ~]# curl -I https://itsoft.ru HTTP/1.1 200 OK Server: nginx/1.16.0 Date: Tue, 05 May 2020 05:57:34 GMT Content-Type: text/html; charset=utf-8 Connection: keep-alive Keep-Alive: timeout=200 Cache-Control: max-age=604800, public Last-Modified: Wed, 01 Apr 2020 18:49:15 GMT Etag: a49f2da878be351c6c73a1ec0524d8ea Set-Cookie: a=1961012078; expires=Wed, 06-May-2020 05:57:34 GMT; path=/; secure; HttpOnly X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block; report=/feedback/http-csp-report.php Strict-Transport-Security: max-age=63072000; includeSubDomains; preload Referrer-Policy: same-origin Feature-Policy: autoplay 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; usb 'none'; vibrate 'none'; Content-Security-Policy-Report-Only: default-src 'self' data: *.googletagmanager.com *.google.com *.google-analytics.com *.googleapis.com *.gstatic.com *.doubleclick.net mc.yandex.ru api-maps.yandex.ru yastatic.net webvisor.com *.calltouch.ru *.usedesk.ru *.youtube.com *.ytimg.com 'nonce-counters' 'report-sample'; img-src https: data: blob: ; report-uri /feedback/http-csp-report.php X-Frame-Options: SAMEORIGIN