РНР-безопасность: где и как хранить пароли. Часть 2

    Всем привет! На прошлой неделе мы опубликовали первую часть данной статьи, чем вызвали нешуточный холивар.

    Одной из главных претензий было отсутствие в статье упоминания password_hash, как мы и обещали, вторую часть данного материала начнем как раз таки с хеширования пароля с помощью password_hash. Также напоминаем о том, что написание данной статьи было навеяно запуском новой группы по курсу «Backend-разработчик на PHP», но к программе обучения данный материал отношения не имеет.


    Подробнее о программе обучения можно будет узнать на дне открытых дверей, а на примере бесплатного вебинара по теме «ServerLess PHP», вы можете оценить формат проведения лекций.

    Пожалуй на этом мы завершим и без того затянувшееся предисловие и перейдем непосредственно к статье.

    Хеширование пароля с помощью password_hash


    Эта функция создает хеш пароля в соответствии с теми параметрами, которые мы ей зададим. Она использует односторонний алгоритм.

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

    • PASSWORD_DEFAULT из PHP 5.5 использует Bcrypt в качестве алгоритма по умолчанию. Однако с течением времени это изменяется по мере обнаружения новых, более безопасных алгоритмов либо иных факторов.
    • PASSWORD_BCRYPT создает хеш crypt(). Обычно он содержит 60 символов, опознать его можно по идентификатору в формате "$2y$".
    • PASSWORD-ARGON2I Argon2 на данный момент является одним из наиболее безопасных алгоритмов хеширования. Он доступен только в том случае, если РНР был скомпилирован с Argon2.
    • PASSWORD_ARGON2ID Этот алгоритм хеширования также принадлежит к семейству Argon2 и задействует версию Argon2ID, а не I. Чтобы он работал, также необходимо, чтобы РНР был скомпилирован посредством Argon2.

    У этой функции также есть необязательный параметр, который состоит из ассоциативного массива, принимающего несколько ключей в соответствии с выбранным алгоритмом.
    Если вы предпочитаете использовать Bcrypt, ключ этой последовательности будет значением cost.

    Если вы выберете алгоритм, который использует Argon2, ключами для ассоциативного массива будут: memory_cost (целое число, указывающее максимальное количество памяти, необходимое для вычисления хэша), time_cost (целое число, указывающее максимальное время, необходимое для вычисления хэша) и thread (другое целое число, указывающее количество потоков, используемых при вычислении хэша).

    Не указывайте параметр salt в РНР 7.0, иначе получите предупреждение об устаревшем подходе.

    Теперь мы знаем, какие элементы необходимы для использования функции password_hash(). Давайте посмотрим, как ее прописывать.

    echo password_hash("MySuperPass", PASSWORD_DEFAULT);
    $2y$10$TLayAY8ZaAZ9FE50EylGYO9oEgrb7gsw1yzJemHdBu1gOQfyWrEUm
    $options = ['cost' => 12,];
    
    echo password_hash("MySuperPass", PASSWORD_BCRYPT, $options);
    $2y$12$jhmTbxAuZXVtX2y.Jc8iy.dW/NENqVCeq2vuoFI9/oa4./YlzhpYO
    
    echo password_hash('rasmuslerdorf', PASSWORD_ARGON2I);
    $argon2i$v=19$m=1024,t=2,p=2$YzJBSzV4TUhkMzc3d3laeg$zqU/1IN0/AogfP4cmSJI1vc8lpXRW9/S0sYY2i2jHT0

    Сначала рекомендуется протестировать эту функцию на ваших серверах и настроить параметр cost таким образом, чтобы исполнение функции отнимало менее 100 миллисекунд в интерактивных системах.

    Скрипт в вышеприведенном примере поможет вам задать оптимальное значение cost для вашего аппаратного оборудования.

    Верификация пароля пользователя


    Вы дали пользователям возможность зарегистрироваться в вашем новом приложении, они могут ввести туда свой пароль, и вы прекрасно знаете, как с этим паролем обращаться.

    Хешируя данные в соответствии с последними трендами безопасности, вы ничего не храните в зашифрованном виде, а ваш сервер спрятан в подвале 10-метровой глубины.

    Что теперь?

    Теперь вы должны позволить пользователям залогиниться в приложении. Для этого в РНР предусмотрена встроенная функция, которая проверяет соответствие пароля хешированной последовательности. Эта функция носит название password_verify(). Работает она примерно вот так:

    $hash = '$2y$07$BCryptRequires22Chrcte/VlQH0piJtjXl.0t1XkA8pw9dMXTpOq';
    if (password_verify('rasmuslerdorf', $hash)) {
        echo 'Пароль верен!';
    } else {
        echo 'Пароль не правильный!';
    }

    У нее есть два параметра, и оба должны иметь формат последовательности. Первый параметр — это пароль, который пользователь ввел в форму входа в аккаунт. Второй параметр — это непосредственно хешированные данные, с которыми мы будем сверяться.

    В результате мы получим логическое значение, готовое к использованию в условных операциях. Таким образом, мы можем или пустить пользователя в приложение или сообщить ему, что что-то пошло не так.

    Эта функция работает благодаря тому, что на предыдущем шаге (когда мы хешировали пароль), значение, вернувшееся от password_hash, включало в себя используемый нами алгоритм, cost и salt.

    Таким образом, нам доступна вся информация, необходимая для password_verify().

    Алгоритм работы системы регистрации пользователей на РНР


    Надеюсь, теперь вы понимаете, какие меры безопасности принимают РНР-разработчики при обращении с паролями.

    Сначала необходимо проверить наличие post-запроса, а затем отобрать и подсчитать количество пользователей, чьи данные совпадают с введенными.

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

    Заключение


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

    Читать первую часть
    OTUS. Онлайн-образование
    673,94
    Цифровые навыки от ведущих экспертов
    Поделиться публикацией

    Комментарии 19

      +1
      Какой то кусок текста из официальной документации, если честно.
      Как насчет password_needs_rehash?
      В первый раз слышу про настройку cost. Судя по статье — поставлю я cost=1, менее 100мс? Менее! Зачем её вообще трогать то?
      IMHO, статья ради статьи.
        +1
        Напишите, пожалуйста, о том, как работать с авторизационной кукой, что туда писать.
          +1

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

            0

            Отказаться от сессий в принципе можно, следуя REST архитектуре. Особенно полезно в случае если несколько реплик запущено и лоад балансер перед ними.

              0
              Допустим есть галочка при авторизации «Запомнить меня».
              Это что, сессии должны жить год?
                0
                А что сложного? пишем токен (рандомную строку) и храним её в БД на сервере, если пользователь не авторизован, то ищем эту строку в БД и авторизуем пользователя по токену.
                Или вы просите автора топика скопипастить вам еще одну статью из интернета?
                  0
                  А чтобы не хранить на сервере?
                    0
                    С трудом себе это представляю. Пользователь может менять на своей стороне COOKIE как угодно, значит хранить данные там не безопасно. Т.е. записать в куку ID пользователя или что-то подобное — так себе идея. При очень большом желании можно зашифровать всё это ключем хранимым на сервере и расшифровывать им же, но мне кажется проще вариант с token.
                    Есть еще вариант с Fingerprint, но я никогда его не использовал, только читал про это, помоему даже здесь, на хабре.
                      0
                      HttpOnly cookie
                        0
                        От чего это защитит? От правки пользователем? Это защита от XSS, не более.
                      0

                      Например, в Symfony данные в куке подписываются с помощью секретного ключа, чтоб их нельзя было подделать

                        0
                        Посмотрел, да, интересная реализация. Позволяет обойтись без токенов и если пользователь сменит пароль, то по куке уже не войдет. От подделки кук защищает хэш.
                        php7 — вот то, что вам надо!
                        0
                        Можно в cookie сохранить сертификат SSL со сроком действия в 1 год, и проверять его валидность. А что не так с хранением токена на сервере? + 2 поля (токен и срок действия) в таблице, которая и так есть.
                          0
                          В Yii2 даже проще, $token = TOKEN_STRING_TIME
                          Проверяется через explode('_', $token), сначала TIME, что не протух, потом уже сам токен.
                          Но я обычно переделываю и храню токены в отдельной таблице с внешним ключем и указанием типа токена. Дает возможность хранить все связанные токены в одной таблице, подчищать протухшие, ограничивать кол-во в целом и по времени.
                          Но что то мы из комментариев тостер тут устраиваем.
                          0
                          Тогда токен jwt, который подписывается своим приватным ключом. Токен в бд можно не хранить.
                            0
                            Смысл от этого действия? Просто пустить в систему? Если мы не храним этот токен в БД, то к какому пользователю он относится? Выше ссылка на шикарный пример из Symfony!
                              +2
                              Информация о пользователе находится в payload части токена. Гарантией, что токен не подделан, служит сигнатура, которая подписана секретным ключом. Злоумышленник не сможет написать в payload части токена свой email, потому что у него нет секретного ключа, чтобы сгенерировать верную сигнатуру.
                          0
                          Вообще хранить токены в БД полезно еще для одной фишки: отображение, верификация и управление множественными авторизациями.

                          Так например под одной учеткой можно войти с нескольких устройств.
                          Если токен один — любой логин деавторизует предыдущую сессию, что неудобно.
                          Если храним список токенов для каждой учетки, можем не рвать сессии, и получаем возможность различать каналы авторизации, вести их учет, можем показать пользователю сколько сессий авторизации (живых токенов) у него сейчас активно, когда они были созданы, можем дать возможность управлять ими, инвалидировать токены, которые ему больше не нужны, или продлять время жизни существующих, и т.п.
                            0
                            user
                            ---
                            id
                            *

                            user_token
                            ---
                            id = > uuid
                            user_id = ralation uuid
                            token => random string (unique)
                            type => in UserToken::range()
                            data => json

                            Как по мне — идеальная схема.
                            Можно еще created_at и прочее, но у нас есть json!

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое