Изучая backend, столкнулся с такой проблемой недопонимания: есть сайт, сервер Nodejs (в принципе, какой угодно, но меня интересует именно Nodejs) и зашел на сайт посетитель. Не важно есть ли регистрация на сайте или нет — что именно идентифицирует посетителя?
Я знаю, что управляется это сессиями. Читал тут, но не понял откуда именно и чего и с каких данных server понимает, что вот сейчас именно тот самый посетитель? Допустим, на сайт зашли два посетителя: один из Москвы другой из Воркуты и на сайте есть чат. Чат простой и банальный - можно без регистрации отправить какой-нибудь пост или поболтать. Надо в чате сделать так, чтобы сообщение пользователя из Воркуты было на синем фоне, а сообщения из Москвы — на зеленом. Тут же на этот сайт зашел новый посетитель тоже из Москвы и с того же IP адреса, но с другого компа который в соседней комнате нашего москвича (допустим это его супруга), вот ее (супруги) сообщение должен иметь фон оранжевый...
В итоге, не прошу никакого кода (если это только не обязательно для наглядного примера). Прошу объяснить саму логику восприятия сервером посетителей. Как определить кто есть кто?
Перечислю все известные мне способы идентификации пользователя.
Указываю этот способ потому, что он единственный, который невозможно подделать. Его можно позаимствовать у других (прокси, VPN, Tor, просто динамический IP), но это обычно сложнее, чем, например, почистить куки. Удалить IP-адрес, аналогично чистке cookies, нельзя: какой-нибудь обязательно будет. В связи с его относительной надёжностью (не всем не лень держать наготове сотни прокси-серверов для смены IP) его часто используют для усиления безопасности: например, ограничивают максимальное число запросов в секунду/минуту/час с одного IP. Однако разных людей, сидящих через один интернет, IP различить не даст, что противоречит условию вопроса, поэтому едем дальше.
Суть проста: тупо шлём логин и пароль в каждом запросе. Один из вариантов реализации этого способа уже присутствует в самом протоколе HTTP, через заголовок Authorization
, уже реализован во всех основных веб-браузерах и веб-серверах.
В HTTP-варианте суть такова:
при первом посещении сайта у клиента ничего нет и никакой дополнительной информации серверу не шлёт. Сервер отвечает ошибкой 401 Unauthorized
и добавляет HTTP-заголовок WWW-Authenticate
с информацией о способах входа (для простого логина-пароля это Basic realm="default"
)
клиент получает это всё и просит у пользователя логин и пароль. После чего отправляет свой запрос повторно, но уже с HTTP-заголовком Authorization
, в котором содержится логин-пароль в base64: Basic YWRtaW46MTIzNDU2
. Если этот пример раскодировать, получим admin:123456
— логин и пароль, разделённые двоеточием
сайт это всё проверяет и или отвечает нормально, или опять 401 и запрашиваем логин-пароль на новый
Этот Authorization: Basic YWRtaW46MTIzNDU2
шлём каждый раз во всех последующих запросах.
Достоинства:
Проблемы:
без HTTPS безопасность вообще никакая: логин-пароль по сути ходят по интернету в открытом виде (base64 не является шифрованием). Клиент тоже вынужден помнить у себя пароль в открытом виде, да и сервер тоже знает пароль (существуют схемы аутентификации, при которых сервер может и не знать пароль, но вопрос не об этом);
HTTP Basic Auth в браузерах работает только в пределах текущей сессии; после перезапуска браузера логин-пароль нужно вводить снова.
Справедливости ради отмечу, что HTTP умеет не только голые логин-пароль (возможно полный список спосбов авторизации), но останавливаться на других способах не буду в связи с их низкой распространённостью.
Самый простой, самый сбалансированный в отношении «безопасность/удобство» и самый популярный способ идентификации. Самая распространённая в мире (наверно) кука PHPSESSID — это именно оно. Суть такова:
при первом посещении сайта у клиента ничего нет. Сайт замечает это, создаёт новую случайную строку (подлиннее, чтоб трудно было подобрать; символов 30 хотя бы) и вместе с обычным ответом на запрос тем или иным образом отправляет эту сгенерированную строку (Set-Cookie, редирект на специальную ссылку или просто в теле ответа, если это например JSON API)
клиент вместе с ответом получает эту строку и запоминает её где-нибудь (браузер сам хранит в cookies, SPA может положить её в localStorage и т.п.)
при последующих посещениях сайта клиент добавляет эту строку к своему запросу (cookies, HTTP-заголовок Authentication или просто GET-параметр в запрашиваемом адресе — на некоторых старых PHP-форумах до сих пор можно наблюдать id сессии прямо в адресной строке)
если нужно идентифицировать клиента более конкретно (вход по логину-паролю, например), сайт в своей базе данных после записывает, что такой-то случайной строке соответствует такой-то логин, а потом при последующих запросах считывает эту информацию из базы.
Если говорить о PHP, то всё это в него встроено: при вызове функции session_start()
создаётся кука PHPSESSID
из случайных букв и цифр (или читается существующая, если она уже есть). Данные, которые связаны с этой кукой, доступны через массив $_SESSION
(а физически хранятся по умолчанию где-то в специальном каталоге на сервере), и вы можете его читать и изменять. При последующих запросах от пользователя содержимое сессии автоматически прочитается из файла при вызове session_start()
, и все данные, которые вы положили в массив $_SESSION
при обработке предыдущих запросов, снова станут доступны. Подробности в документации.
Достоинства:
простота — сравнивать строки тривиально;
при смене IP-адреса (а на мобильниках это частое явление) идентификация не слетает;
реализация кнопочки «Разлогинить меня на всех устройствах» сводится к простому удалению всех записей в базе, а если для каждого устройства создавать отдельную строку, то можно разлогинивать устройства выборочно (некоторые сайты предоставляют такую возможность, например ВК).
Проблемы:
генератор случайной строки должен быть действительно случайным (или не совсем случайным, но криптостойким, не uniqid()
), так как псевдослучайность злоумышленник может попытаться подобрать (например, подбор состояния генератора в PHP или Python, или подбор сессий, созданных черех uniqid(), в Invision Power Board). Ни в коем случае в качестве строки нельзя использовать хэш логина, хэш пароля, текущее время, одну-единственную заранее заготовленную строку и прочие неслучайные вещи, так как это сильно упрощает подбор. Как получить настоящую случайность, читайте в документации к вашему языку программирования. Или просто используйте готовую реализацию вроде session_start()
в PHP;
дополнительная нагрузка на сервер. Чтобы узнать, какой именно пользователь прячется за случайной строкой, ему приходится обращаться к базе данных. Не проблема для подавляющего большинства сайтов, но для гигантов типа гугла уже проблема;
куки иногда баганутые: например, IE11 добавляет куки к поддоменам, даже когда его не просят (в Edge уже исправлено), что может привести к утечке данных на сторонние CDN, например. Поэтому следите за тем, как браузеры, для которых вы затачиваете сайт, манипулируют с куками. Ну и про HttpOnly не забывайте, чтобы нельзя было угнать куки через XSS (и про Secure, если сайт использует HTTPS).
Суть такова: нагло нарушаем вышеупомянутый запрет на неслучайные данные и пихаем в строку, например, ID пользователя и, опционально, имеющиеся права доступа (например, админ ли он), срок годности строки и какие-нибудь ещё данные. Но! Дополнительно к этой строке добавляем какой-нибудь хэш, который считается по данным плюс некой секретной строке, которую знает только сайт и никому не отдаёт. При запросе от клиента сайт, соответственно, проверяет, что хэш правильный. Это защищает от подбора и подделок: чтобы подделать данные, нужно пересчитать хэш, а злоумышленник, не зная секретной строки, этого сделать не сможет. (Секретная строка должна быть ОЧЕНЬ длинной, символов сто, чтоб вообще не подобрать, так как на ней вся безопасность.) (В JWT также вместо просто секретной строки можно применять RSA для подписи, что повышает безопасность, но расписывать все детали реализации не буду, и так длинно получилось)
Достоинства:
меньшая нагрузка на сервер. Клиент уже сам прислал все нужные данные, серверу остаётся лишь посчитать хэш от этих данных и секретной строки и проверить, что он совпадает с присланным. В базу данных ходить не надо: секретная строка обычно лежит в какой-нибудь переменной поблизости, так что всё это делается быстро;
независимость от централизованной базы данных позволяет легко проверять аутентификацию на независимых и никак не связанных друг с другом микросервисах, в том числе географически разбросанных по миру, ведь им достаточно знать лишь секретную строку, которая меняется очень редко, для проверки присланного пользователем хэша, и не нужно связываться с другими микросервисами или с базой;
клиент сам может прочитать JWT и понять, кто он такой (если данные только защищать хэшем, а не шифровать);
при смене IP-адреса тоже не слетает.
Проблемы:
реализация усложняется. Если делать всё самому, то можно накосячить и получить дырку в безопасности, поэтому лучше брать готовые реализации вроде того же JWT (впрочем, в них тоже иногда находят дырки, так что обязательно мониторим новости и почитываем Хабр);
кнопочку «Разлогинить меня на всех устройствах» сделать вообще нельзя. Чтобы пользовательская строка с данными стала недействительной, нужно или сменить секретную строку, или запомнить где-то в базе, что именно такая-то строка с такими-то данными стала недействительна. Но это всё довольно проблематично и сводит на нет все преимущества данного способа идентификации. Поэтому такие строки, как правило, делают короткоживущими: например, Google в своих API выдаёт JWT, действительный всего полчаса (информация о сроке годности хранится прямо в JWT и тоже защищается хэшем, в базу ходить не надо).
информация может протухнуть. Например, если записать в JWT, что пользователь является админом, а потом отобрать права админа, то сайт, опираясь на данные JWT, будет продолжать считать клиента админом, пока сам JWT не протухнет целиком. Можно брать информацию из базы, но тогда опять становится проще использовать случайную строку.
JWT и аналоги из-за того, что содержат всю необходимую информацию, обычно длинные; при большом количестве данных строка может, например, не влезть в cookies. Впрочем, если хранить только id пользователя, то это не проблема.
Суть в использовании технологий не по назначению. У каждого браузера и каждой ОС есть свои особенности поведения, и по этим особенностям можно довольно точно идентифицировать, кто именно зашёл. Например, они рисуют текст немного по-разному, и по мелким отличиям в пикселях текста браузеры можно различать, или же на разных компьютерах будет немножко разный размер окна браузера, развёрнутого на весь экран (поэтому Tor Browser рекомендует не разворачивать его и блокирует сайтам доступ к canvas). Не буду расписывать всё во всех подробностях, оставлю ссылки для дальнейшего чтения:
Evercookie — самые устойчивые куки
Panopticlick 2.0 для фингерпринтинга браузера
Супер-куки на основе HSTS отследят вас даже в приватном режиме
Достоинства:
Проблемы:
точность не стопроцентная. Все айфоны довольно одинаковые, и отличить один айфон X от другого айфона X вряд ли получится (хотя это касается только фингерпринтинга, для суперкук попроще);
пользователи вас найдут и больно побьют.
В дополнение к сказанному о недостатках кук можно сказать что:
Прозрачный серверный кеш запросов становится невозможен если используются куки (будь это Varnish или прозрачное кеширование в nginx). Потому если не ставить куки со стороны сервера, если они не нужны, то страницы сайта смогут использовать серверный кеш и будут открываться быстрее. Попытки кешировать запросы с заголовком Set-Cookie
противоречат просто здравому смыслу, значит они тоже проходят мимо кеша.
Настройка CDN для статических ресурсов требует внимания если используются куки. Если ваш сайт открывается без www
по адресу, например, test.ru
, то, поставив куку на сайте, можно будет считать что эта кука будет передаваться в запросах ко всем поддоменам, включая, например, cdn.test.ru
. Потому на сайтах, которые традиционно открываются без www
, вы можете видеть что для статических ресурсов используется отдельный домен второго уровня, а не поддомен. Например yastatic.net
у Яндекса.
Последнюю проблему должен был исправить RFC 6265, но на момент написания этого ответа некоторые основные браузеры всё ещё не поддерживают его в полней мере. От этих старых браузеров всё ещё нельзя просто так отмахнуться, так как не во всём мире использование старых браузеров одинаково низко: например, в Японии на март 2018 года IE используется для 16% всех запросов. А это страна в которой живёт 127 миллионов человек. Если вы делаете глобальный сервис, то так и так вам придётся действовать будто RFC 6265 ещё нет.
Кроме JWT можно вспомнить другие "длинные" куки вроде ViewState из ASP.NET, которым свойственны те же проблемы с кешированием и CDN, что и со обычными сессионными случайными куками, только хуже. Такие и подобные куки могут быть очень большие, запросто по десятку килобайт, а если они будут передаваться с каждым запросом к любой картинке или статическому файлу на вашем сайте, то это будет определённо сказываться на скорости работы сайта во всех режимах. Майкрософт прямо рекомендуют не пользоваться ими если вам важна скорость сайта.
Вместо кук можно использовать всевозможные заголовки, которые могут попасть в браузерный кеш. Например, если вы однажды послали с какой-то картинкой или файлом заголовок ETag, то при следующем обращении браузер снова перешлёт значение этого заголовка. Пользователю никак не видно что вы его так отслеживаете, то есть знаете, что это вот тот человек зашел ещё раз, потому такие приёмы не приветствуются.
Если вы можете не идентифицировать ваших пользователей, то ваш сайт сможет работать быстрее.
Как правило используются Cookies
- определенная строка данных, которая хранится у пользователя в браузере. Алгоритм их генерации вы можете сделать сами, либо использовать родной движка, например PHPSESSID
в PHP
(смотри функцию session_start()
).
Можно так же идентифицировать пользователя без них, но в данном случае необходимо будет использовать другие параметры, к которым есть доступ.
Это, в первую очередь User-agent (браузер пользователя) и его IP адрес. В случае PHP
эти переменные хранятся в $_SERVER
:
1. $_SERVER['HTTP_USER_AGENT']
2. $_SERVER['REMOTE_ADDR']
Соответственно, второй вариант обладает меньшей точностью, но будет работать, если у пользователя отключено хранение Cookie
. Пример ошибки во втором случае - два пользователя находятся за NAT и используют одинаковый браузер.
Айфон мало держит заряд, разбираемся с проблемой вместе с AppLab
Никак не получается добиться желаемого результата со slider-ом жуквэриЕсть шаг в 5, хотелось бы получить labels в виде 1,5,10,15,20 и т
Есть вот такой код формыОн по сути отправляет директорию, в которой расположено изображение и само название изображения