Хорошее приложение - это безопасное приложение

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

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

После сбоя на собеседовании и в духе роста я решил снова погрузиться в эту тему, чтобы создать небольшую демонстрацию приложения с аутентификацией с использованием BCrypt и веб-токенов JSON.

Создание сервера Rails

Для начала мы создадим проект Rails с помощью следующей команды:

rails new <project_name> --api --database=postgresql

Здесь мы используем PostgreSQL в качестве базы данных и указали Rails, что это будет API с флагом --api. Без этого флага мы бы сгенерировали много дополнительных ненужных файлов, включенных в полную структуру MVC.

Мы установим драгоценные камни jwt и active_model_serializers с помощью:

bundle add jwt && bundle add active_model_serializers

Затем перейдите к Gemfile и раскомментируйте gem 'rack-cors', что позволит нам установить совместное использование ресурсов между источниками (CORS) в API. Мы также раскомментируем gem 'bcrypt'. Наконец, запустите bundle install в терминале, чтобы установить эти библиотеки.

Чтобы включить CORS, перейдите к config/initializers/cors.rb и раскомментируйте следующее:

А пока измените 'example.com' на '*'. Это позволит всем доменам делать запросы к нашему API. Это влияет на безопасность, поэтому для чего-либо, кроме демонстрации, я бы рекомендовал ограничить доступ только доменом вашего внешнего интерфейса.

Затем мы создадим модель пользователя, контроллер и сериализаторы с помощью следующих команд:

rails g model User username password password_digest bio avatar
rails g controller api/v1/users
rails g serializer user

При создании модели не стесняйтесь включать в схему любые столбцы, которые вам нравятся. username, password и password_digest - это почти необходимый минимум.

Чтобы создать базу данных, запустите:

rails db:create
rails db:migrate

Большой! Теперь у нас есть базовая серверная часть API, и мы можем приступить к реализации логики аутентификации.

BCrypt

В нашем приложении мы используем BCrypt для добавления паролей пользователей в открытый текст. Соль - это немного случайных данных, которые добавляются к паролю пользователя. Затем соленый пароль обрабатывается функцией хеширования. Хеш-функция фактически является односторонним процессом, поскольку невозможно проанализировать хеш-код для определения исходного пароля. Благодаря BCrypt у нас теперь есть «переваренные» пароли, которые можно безопасно хранить в базе данных. Никогда не храните пароли в открытом виде в базе данных.

Мы могли бы создать authenticate метод в модели User, чтобы сравнивать пароли пользователей в открытом виде с дайджестом паролей. Это выглядело бы примерно так:

Однако мы будем использовать встроенный ActiveModel#has_secure_password. Мы добавим это в модель User с некоторыми проверками для проверки уникальности имени пользователя:

class User < ApplicationRecord
  has_secure_password
  validates :username, uniqueness: { case_sensitive: false }
end

Создание пользователей

Сначала мы сосредоточимся на создании новых действующих пользователей. В UsersController мы определим метод create, а также некоторые сильные параметры:

Здесь мы использовали встроенные символы кода состояния Rails, status: :created и status: :not_acceptable. Они выдадут коды 201 и 406 соответственно. Мы добавим к нашему UserSerializer атрибуты, которые мы хотели бы отображать на стороне клиента:

class UserSerializer < ActiveModel::Serializer
  attributes :username, :avatar, :bio
end

Перейдите к config/routes.rb, чтобы добавить маршруты, необходимые для нашего сервера. Нам нужны маршруты для обработки двух действий. Это когда существующий пользователь входит в систему и отправляет запрос на просмотр своего профиля. Для обоих этих действий потребуется веб-токен JSON, но об этом позже.

На данный момент наш бэкэнд оборудован для обработки запросов POST к конечной точке api/v1/users. Я бы посоветовал быстро создать интерфейс Vanilla JavaScript или React, чтобы проверить, можно ли успешно создавать новых пользователей с переваренными паролями. При использовании fetch запрос будет выглядеть примерно так:

Веб-токены JSON (JWT)

JWT предоставляют способ безопасной передачи информации в виде объекта JSON. Они позволяют нам использовать аутентификацию без сохранения состояния, то есть на сервере не хранится информация о текущем пользователе. Пользовательские данные кодируются с помощью JWT, и этот токен хранится на стороне клиента (в браузере). Затем он отправляется с каждым аутентифицированным запросом данных. График ниже очень хорошо описывает процесс:

JWT имеют общую структуру из трех строк, разделенных точками, причем каждая строка содержит закодированные данные. Они напоминают что-то вроде этого:

xxxxxxxxxxx.yyyyyyyyyyyyyy.zzzzzzzzzzzzz

Первая строка содержит данные заголовка. Сюда входит тип токена jwt и алгоритм подписи, используемый для цифровой подписи и проверки полезной нагрузки:

{
  "alg": "HS256",
  "typ": "JWT"
}

Средняя строка включает полезную нагрузку. В нашем случае это информация, относящаяся к пользователю (имя пользователя, ID и т. Д.).

Третья строка представляет собой подпись. Он создается путем объединения заголовка, закодированной полезной нагрузки, секрета и алгоритма, указанного в заголовке. Подпись подтверждает, что данные не были изменены или подделаны во время запроса, а также может подтвердить, что отправитель JWT является тем, кем они себя называют:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

У JWT есть два основных метода: encode и decode. encode принимает три аргумента полезной нагрузки для кодирования, секрет приложения по вашему выбору и, возможно, используемый алгоритм хеширования. Этот метод возвращает строку JWT. decode принимает аналогичные три аргумента строки JWT, секрет приложения и, необязательно, алгоритм хеширования.

JWT и ApplicationController

Поскольку другим контроллерам потребуется возможность аутентификации и авторизации пользователей, мы поместим эту функцию на верхний уровень в ApplicationController. Для начала мы создадим метод для кодирования полезной нагрузки запроса с помощью токена и метод для декодирования токена, отправленного с последующими запросами:

Когда пользователь запрашивает доступ к маршрутам или ресурсам, защищенным авторизацией, ему необходимо отправить JWT. Маркер включается в заголовок запроса fetch с использованием схемы носителя: Authorization: Bearer <token>. На практике это выглядит так:

Это позволяет нам изменить наш decoded_token метод так, чтобы он ожидал заголовок авторизации с включенным JWT:

Выше мы использовали синтаксис begin/rescue, который позволяет нам выйти из исключения в случае, если серверу передан недопустимый токен. Вместо сбоя сервер вернет nil.

Мы также напишем current_user метод, который будет захватывать связанного пользователя всякий раз, когда токен предоставляется для авторизации. Мы также можем определить метод logged_in?, который будет возвращать логическое значение в зависимости от возврата метода current_user:

В завершение мы определим метод, который будет запрашивать у пользователя авторизацию и использовать before_action для требования авторизации перед любым запросом к маршрутам или ресурсам:

Назначение токенов в UsersController

Теперь, когда у нас есть возможность создавать и читать JWT в ApplicationController, давайте вызовем encode_token в UsersController, чтобы автоматически назначать пользователям токен при регистрации. Мы также должны обязательно пропустить авторизацию перед созданием пользователя. Регистрация или создание пользователей - единственное действие, которое не требует входа пользователя в систему для доступа:

Создание токена при входе в систему

Не только токен должен быть создан при регистрации пользователя, но и новый токен должен создаваться каждый раз, когда существующий пользователь входит в систему. Чтобы справиться с этим, мы создадим AuthController:

rails g controller api/v1/auth

Внутри нашего контроллера мы напишем методы для создания аутентификации для вошедшего в систему пользователя, а также некоторые надежные параметры:

Используя метод encode_token, унаследованный от ApplicationController, мы создаем токен, добавляя идентификатор существующего пользователя в полезную нагрузку. Затем мы возвращаем этот новый JWT с данными пользователя. Затем данные пользователя могут быть сохранены в состоянии на интерфейсе (при использовании React или Redux), а токен может храниться на стороне клиента.

В нашем интерфейсе fetch запрос на выполнение действия входа будет включать метод POST для /api/v1/login конечной точки. Как мы указали в наших маршрутах, это вызовет метод create в AuthController:

Доступ к профилю пользователя

Ранее мы обсуждали, как JWT должен быть включен в заголовок авторизации для доступа к защищенным маршрутам и ресурсам, таким как профиль пользователя. Давайте создадим функциональные возможности для вошедшего в систему пользователя, чтобы он мог просматривать свой профиль. Мы напишем метод profile в UsersController, который будет отправлять объект JSON, содержащий данные пользователя, при условии, что пользователь авторизован:

На этом наш сервер Rails завершает аутентификацию и авторизацию! Как я уже сказал, я считаю полезным протестировать все в базовом интерфейсе. Хотя это не обсуждается в этой статье, вы можете найти мой интерфейсный репозиторий на моем GitHub.

использованная литература