Что именно идентифицирует посетителя сайта?

Air

Дамы и Господа, изучая backend, столкнулся с такой проблемой недопонимания: есть сайт, сервер Nodejs (в принципе, какой угодно, но меня интересует именно Nodejs) и зашел на сайт посетитель. Не важно есть ли регистрация на сайте или нет — что именно идентифицирует посетителя?

Я знаю, что управляется это сессиями. Читал тут, но не понял откуда именно и чего и с каких данных server понимает, что вот сейчас именно тот самый посетитель? Допустим, на сайт зашли два посетителя: один из Москвы другой из Воркуты и на сайте есть чат. Чат простой и банальный - можно без регистрации отправить какой-нибудь пост или поболтать. Надо в чате сделать так, чтобы сообщение пользователя из Воркуты было на синем фоне, а сообщения из Москвы — на зеленом. Тут же на этот сайт зашел новый посетитель тоже из Москвы и с того же IP адреса, но с другого компа который в соседней комнате нашего москвича (допустим это его супруга), вот ее (супруги) сообщение должен иметь фон оранжевый...

В итоге, не прошу никакого кода (если это только не обязательно для наглядного примера). Прошу объяснить саму логику восприятия сервером посетителей. Как определить кто есть кто?

3 ответа

Air

Перечислю все известные мне способы идентификации пользователя.

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 шлём каждый раз во всех последующих запросах.

Достоинства:

  • простота. HTTP-вариант в веб-браузерах и веб-серверах уже сделан, ничего изобретать не надо. Если делать свой вариант, то достаточно реализовать проверку логина-пароля в каждом запросе без дополнительных сложностей.

Проблемы:

  • без HTTPS безопасность вообще никакая: логин-пароль по сути ходят по интернету в открытом виде. Клиент тоже вынужден помнить у себя пароль в открытом виде;

  • HTTP-вариант в браузерах работает только в пределах текущей сессии; после перезапуска браузера логин-пароль нужно вводить снова.

Справедливости ради отмечу, что HTTP умеет не только голые логин-пароль (возможно полный список спосбов авторизации), но останавливаться на других способах не буду в связи с их низкой распространённостью.

Случайная строка

Самый простой, самый сбалансированный в отношении «безопасность/удобство» и самый популярный способ идентификации. Самая распространённая в мире (наверно) кука PHPSESSID — это именно оно. Суть такова:

  • при первом посещении сайта у клиента ничего нет. Сайт замечает это, создаёт новую случайную строку (подлиннее, чтоб трудно было подобрать; символов 30 хотя бы) и вместе с обычным ответом на запрос тем или иным образом отправляет эту сгенерированную строку (Set-Cookie, редирект на специальную ссылку или просто в теле ответа, если это например JSON API)

  • клиент вместе с ответом получает эту строку и запоминает её где-нибудь (браузер сам хранит в cookies, SPA может положить её в localStorage и т.п.)

  • при последующих посещениях сайта клиент добавляет эту строку к своему запросу (cookies, HTTP-заголовок Authentication или просто GET-параметр в запрашиваемом адресе)

  • если нужно идентифицировать клиента более конкретно (вход по логину-паролю, например), сайт в своей базе данных после записывает, что такой-то случайной строке соответствует такой-то логин, а потом при последующих запросах считывает эту информацию из базы.

Достоинства:

  • простота, очевидно;

  • при смене IP-адреса (а на мобильниках это частое явление) идентификация не слетает;

  • реализация кнопочки «Разлогинить меня на всех устройствах» сводится к простому удалению всех записей в базе.

Проблемы:

  • генератор случайной строки должен быть действительно случайным (или не совсем случайным, но криптостойким, не uniqid()), так как псевдослучайность злоумышленник может попытаться подобрать (например, подбор состояния генератора в PHP или Python, или подбор сессий, созданных черех uniqid(), в Invision Power Board). Ни в коем случае в качестве строки нельзя использовать хэш логина, хэш пароля, текущее время, одну-единственную заранее заготовленную строку и прочие неслучайные вещи, так как это сильно упрощает подбор. Как получить настоящую случайность, читайте в документации к вашему языку программирования;

  • дополнительная нагрузка на сервер. Чтобы узнать, какой именно пользователь прячется за случайной строкой, ему приходится обращаться к базе данных. Не проблема для подавляющего большинства сайтов, но для гигантов типа гугла уже проблема;

  • куки иногда баганутые: например, IE11 добавляет куки к поддоменам, даже когда его не просят (в Edge уже исправлено), что может привести к утечке данных на сторонние CDN, например. Поэтому следите за тем, как браузеры, для которых вы затачиваете сайт, манипулируют с куками. Ну и про HttpOnly не забывайте, чтобы нельзя было угнать куки через XSS (и про Secure, если сайт использует HTTPS).

Неслучайная, но защищённая строка (например, JWT)

Суть такова: нагло нарушаем вышеупомянутый запрет на неслучайные данные и пихаем в строку, например, ID пользователя и, опционально, имеющиеся права доступа (например, админ ли он), срок годности строки и какие-нибудь ещё данные. Но! Дополнительно к этой строке добавляем какой-нибудь хэш, который считается по данным плюс некой секретной строке, которую знает только сайт и никому не отдаёт. При запросе от клиента сайт, соответственно, проверяет, что хэш правильный. Это защищает от подбора и подделок: чтобы подделать данные, нужно пересчитать хэш, а злоумышленник, не зная секретной строки, этого сделать не сможет. (Секретная строка должна быть ОЧЕНЬ длинной, символов сто, чтоб вообще не подобрать, так как на ней вся безопасность.) (В JWT также вместо просто секретной строки можно применять RSA для подписи, что повышает безопасность, но расписывать все детали реализации не буду, и так длинно получилось)

Достоинства:

  • меньшая нагрузка на сервер. Клиент уже сам прислал все нужные данные, серверу остаётся лишь посчитать хэш от этих данных и секретной строки и проверить, что он совпадает с присланным. В базу данных ходить не надо: секретная строка обычно лежит в какой-нибудь переменной поблизости, так что всё это делается быстро;

  • клиент сам может прочитать JWT и понять, кто он такой (если данные только защищать хэшем, а не шифровать);

  • при смене IP-адреса тоже не слетает.

Проблемы:

  • реализация усложняется. Если делать всё самому, то можно накосячить и получить дырку в безопасности, поэтому лучше брать готовые реализации вроде того же JWT;

  • кнопочку «Разлогинить меня на всех устройствах» сделать вообще нельзя. Чтобы пользовательская строка с данными стала недействительной, нужно или сменить секретную строку, или запомнить где-то в базе, что именно такая-то строка с такими-то данными стала недействительна. Но это всё довольно проблематично и сводит на нет все преимущества данного способа идентификации. Поэтому такие строки, как правило, делают короткоживущими: например, Google в своих API выдаёт JWT, действительный всего полчаса (информация о сроке годности хранится прямо в JWT, в базу ходить не надо).

  • информация может протухнуть. Например, если записать в JWT, что пользователь является админом, а потом отобрать права админа, то сайт, опираясь на данные JWT, будет продолжать считать клиента админом, пока сам JWT не протухнет целиком. Можно брать информацию из базы, но тогда опять становится проще использовать случайную строку.

  • JWT и аналоги из-за того, что содержат всю необходимую информацию, обычно длинные; при большом количестве данных строка может, например, не влезть в cookies.

Суперкуки и прочий фингерпринтинг

Суть в использовании технологий не по назначению. У каждого браузера и каждой ОС есть свои особенности поведения, и по этим особенностям можно довольно точно идентифицировать, кто именно зашёл. Например, они рисуют текст немного по-разному, и по мелким отличиям в пикселях текста браузеры можно различать. Не буду расписывать всё во всех подробностях, оставлю ссылки для дальнейшего чтения:

Достоинства:

  • хрен выпилишь. Если захотеть, можно, конечно, но уж очень много мороки. Это уже не просто кнопочку «Очистить cookies» нажать. Устройство клиента будет идентифицировано независимо от того, сменил ли он IP-адрес, почистил ли куки и т.п.

Проблемы:

  • точность не стопроцентная. Все айфоны довольно одинаковые, и отличить один айфон X от другого айфона X вряд ли получится (хотя это касается только фингерпринтинга, для суперкук попроще);

  • пользователи вас найдут и больно побьют.


Air

Как правило используются Cookies - определенная строка данных, которая хранится у пользователя в браузере. Алгоритм их генерации вы можете сделать сами, либо использовать родной движка, например PHPSESSID в PHP (смотри функцию session_start()).

Можно так же идентифицировать пользователя без них, но в данном случае необходимо будет использовать другие параметры, к которым есть доступ. Это, в первую очередь User-agent (браузер пользователя) и его IP адрес. В случае PHP эти переменные хранятся в $_SERVER:

1. $_SERVER['HTTP_USER_AGENT'] 2. $_SERVER['REMOTE_ADDR']

Соответственно, второй вариант обладает меньшей точностью, но будет работать, если у пользователя отключено хранение Cookie. Пример ошибки во втором случае - два пользователя находятся за NAT и используют одинаковый браузер.


Air

В дополнение к сказанному о недостатках кук можно сказать что:

  • Прозрачный серверный кеш запросов становится невозможен если используются куки (будь это 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, то при следующем обращении браузер снова перешлёт значение этого заголовка. Пользователю никак не видно что вы его так отслеживаете, то есть знаете, что это вот тот человек зашел ещё раз, потому такие приёмы не приветствуются.

Итого

Если вы можете не идентифицировать ваших пользователей, то ваш сайт сможет работать быстрее.

licensed under cc by-sa 3.0 with attribution.