Статьи

Аутентифицируйте свой Rails API с помощью JWT с нуля

Аутентификация является одной из важнейших частей любого веб-приложения. Существуют бесчисленные библиотеки и платформы, которые предоставляют различные варианты для выполнения аутентификации тем или иным способом. Эти библиотеки забирают большую часть основы, необходимой для настройки системы аутентификации, обеспечивая «магию» происходящего за кулисами. Для Rails у нас есть несколько систем аутентификации, известной из которых является Devise .

Devise — это механизм аутентификации, который работает как часть нашего приложения и выполняет всю тяжелую работу, когда дело доходит до аутентификации. Однако зачастую нам не нужно много частей, которые он предоставляет. Например, Devise не очень хорошо работает с системами на основе API, поэтому у нас есть гем авторизации devise token . devise token auth — это библиотека, которая делает то же, что и Devise, но с токенами вместо сессий.

Сегодня мы собираемся исследовать создание нашей собственной системы аутентификации на основе JWT с нуля. Давайте начнем.

ПРИМЕЧАНИЕ. Это руководство предназначено для аутентификации на основе API.

Почему JWT

JWT (JSON Web Token, произносится «jot») — это автономный стандарт аутентификации, разработанный для безопасного обмена данными между системами. Поскольку он самодостаточен, для его работы не требуется никакого резервного хранилища. Кроме того, подход JWT очень надежен и гибок, что позволяет использовать его с любым клиентом. Он не требует никаких дополнительных затрат, и почти во всех языках есть библиотеки, которые облегчают работу с JWT.

У нас уже есть отличный учебник о JWT и о том, как использовать его с Rails. Если вы новичок в JWT, я бы посоветовал вам сначала прочитать его, чтобы получить представление о том, что мы собираемся создать.

модель

Мы начнем с создания модели для нашего приложения. Приложение будет аутентифицировать пользователей, поэтому давайте создадим модель User .

 rails g model user 

Эта команда создаст файл миграции в db / migrate с именем XXX create users , где XXX — текущая дата. Добавьте в этот файл следующий код, который добавит нужные нам столбцы:

 create_table :users do |t| t.string :email, null: false t.string :password_digest, null: false t.string :confirmation_token t.datetime :confirmed_at t.datetime :confirmation_sent_at t.timestamps end 

Запустите rake db:migrate чтобы запустить миграцию.

Здесь мы добавляем столбцы email и password_digest , которые являются основными столбцами, необходимыми для регистрации или аутентификации пользователя. Конечно, вы можете добавить больше столбцов, которые имеют смысл, если хотите. Для confirmation_token пользователя требуются столбцы confirmation_token , confirmation_token , confirmed_at и Подтверждение. Вы можете пропустить это, если не хотите подтверждать электронные письма.

Validations

Давайте добавим наши проверки в модели / user.rb :

 validates_presence_of :email validates_uniqueness_of :email, case_sensitive: false validates_format_of :email, with: /@/ 

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

Callbacks

Мы собираемся использовать надежный пароль Rails для обработки хеширования пароля.

Для начала добавьте gem bcrypt (он, вероятно, есть, но закомментирован) в ваш Gemfile и запустите пакетную bundle install . После установки гема добавьте следующую строку в класс User в файле models / user.rb :

 class User < ApplicationRecord has_secure_password ... 

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

Добавьте эти before обратными вызовами в файл models / user.rb :

 before_save :downcase_email before_create :generate_confirmation_instructions 

Мы переносим письмо, прежде чем сохранить его в БД. Инструкции по подтверждению должны генерироваться только во время создания пользовательской записи. Давайте добавим методы:

 def downcase_email self.email = self.email.delete(' ').downcase end def generate_confirmation_instructions self.confirmation_token = SecureRandom.hex(10) self.confirmation_sent_at = Time.now.utc end 

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

Постановка на учет

Создайте

Теперь у нас есть настройка модели, поэтому давайте добавим конечную точку для создания пользователя. Создайте наш пользовательский контроллер:

 rails g controller users 

Добавьте следующую строку в config / rout.rb :

 resources :users, only: :create 

Теперь мы сгенерировали наш UsersController . Перейдите к контроллеру ( app / controllers / users_controller.rb ) и добавьте следующие строки:

 def create user = User.new(user_params) if user.save render json: {status: 'User created successfully'}, status: :created else render json: { errors: user.errors.full_messages }, status: :bad_request end end private def user_params params.require(:user).permit(:email, :password, :password_confirmation) end 

Теперь у нас есть конечная точка API для создания пользователя! Вы можете попробовать это, запустив сервер и отправив запрос POST с данными пользователя в виде JSON в теле. Вот пример того, что вы можете опубликовать на http://localhost:3000/users :

 { user: { email: '[email protected]', password: 'anewpassword', password_confirmation: 'anewpassword' } } 

Вы должны получить сообщение « User created successfully в качестве ответа. Последующие запросы с теми же данными должны отвечать сообщениями об ошибках. Это наши проверки в игре. Для тех, кто пропускает часть подтверждения, вы можете пропустить следующее и перейти в раздел Login .

подтверждение

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

Для начала мы должны отправить электронное письмо пользователю, когда запись будет успешно создана. Мы не будем рассказывать, как отправлять электронные письма, так как уже есть хорошие учебники, в которых рассказывается, как это сделать. Мы должны добавить строку для отправки электронной почты сразу после user.save в users_controller .

 ... if user.save #Invoke send email method here ... 

Просто убедитесь, что вы включили user.confirmation_token в вашу электронную почту. В идеале URL должен приводить к конечной точке, которая выбирает токен и отправляет его в наш API. Давайте создадим эту конечную точку API поста.

Добавьте маршрут в наш файл config / rout.rb для конечной точки подтверждения:

 resources :users, only: :create do collection do post 'confirm' end end 

Теперь создайте действие confirm в UsersController :

 def confirm token = params[:token].to_s user = User.find_by(confirmation_token: token) if user.present? && user.confirmation_token_valid? user.mark_as_confirmed! render json: {status: 'User confirmed successfully'}, status: :ok else render json: {status: 'Invalid token'}, status: :not_found end end 

Посмотрим, что мы здесь делаем. Сначала мы выбираем токен из params , вызывая to_s чтобы обработать случай, когда токен не отправлен в запросе. Затем выберите соответствующего пользователя на основе токена подтверждения.

Если пользователь присутствует и подтверждение не истекло, вызовите метод модели mark_as_confirmed! и ответьте сообщением об успехе. Мы должны добавить confirmation_token_valid? и mark_as_confirmed! методы к нашей модели User :

 def confirmation_token_valid? (self.confirmation_sent_at + 30.days) > Time.now.utc end def mark_as_confirmed! self.confirmation_token = nil self.confirmed_at = Time.now.utc save end 

confirmation_token_valid? Метод проверяет, было ли отправлено подтверждение за последние 30 дней и, следовательно, не истек ли срок действия. Вы можете изменить его на любое значение по вашему желанию.

mark_as_confirmed! сохраняет подтвержденное время и аннулирует токен подтверждения, так что одно и то же электронное письмо с подтверждением не может быть использовано для подтверждения пользователя снова.

Теперь у нас есть конечная точка для подтверждения пользователя. Вы можете проверить это, отправив запрос на отправку users/confirm?token=<CONFIRMATION_TOKEN> конечной точки users/confirm?token=<CONFIRMATION_TOKEN> и проверить значения confirmed_at и confirmation_token для пользователя. Вы должны получить User confirmed successfully . Последующие запросы с тем же токеном должны возвращать Invalid token .

Теперь у нас есть рабочая регистрация как часть нашего приложения. Давайте создадим последний кусок: конечную точку входа в систему.

Авторизоваться

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

контроллер

Давайте добавим маршрут входа в наш файл config / rout.rb под ресурсом users :

 resources :users, only: :create do collection do post 'confirm' post 'login' end end 

Мы создали маршрут для users/login . Контроллеры требуют некоторой модификации для извлечения кода json_web_token и создания соответствующего действия в UsersController .

В controllers / application_controller.rb добавьте строку ниже сразу после определения класса:

 require 'json_web_token' 

В controllers / users_controller.rb добавьте следующий фрагмент:

 def login user = User.find_by(email: params[:email].to_s.downcase) if user && user.authenticate(params[:password]) auth_token = JsonWebToken.encode({user_id: user.id}) render json: {auth_token: auth_token}, status: :ok else render json: {error: 'Invalid username / password'}, status: :unauthorized end end 

Давайте рассмотрим этот метод шаг за шагом. Сначала мы выбираем пользователя из электронного письма и, если он есть, вызываем метод authenticate передавая предоставленный пароль. Метод authenticate предоставляется помощником has_secure_password .

Как только мы проверим адрес электронной почты и пароль, JsonWebToken идентификатор пользователя в токен JWT с помощью нашего метода кодирования из JsonWebToken который нам еще предстоит создать. Затем верните токен.

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

 ... if user && user.authenticate(params[:password]) if user.confirmed_at? auth_token = JsonWebToken.encode({user_id: user.id}) render json: {auth_token: auth_token}, status: :ok else render json: {error: 'Email not verified' }, status: :unauthorized end else ... 

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

Библиотека JWT

Теперь давайте добавим библиотеку JWT. Начните с добавления следующего Gemfile в ваш Gemfile и выполните bundle install :

 gem 'jwt' 

После этого создайте файл с именем json web token.rb в lib / и добавьте следующие строки:

 require 'jwt' class JsonWebToken # Encodes and signs JWT Payload with expiration def self.encode(payload) payload.reverse_merge!(meta) JWT.encode(payload, Rails.application.secrets.secret_key_base) end # Decodes the JWT with the signed secret def self.decode(token) JWT.decode(token, Rails.application.secrets.secret_key_base) end # Validates the payload hash for expiration and meta claims def self.valid_payload(payload) if expired(payload) || payload['iss'] != meta[:iss] || payload['aud'] != meta[:aud] return false else return true end end # Default options to be encoded in the token def self.meta { exp: 7.days.from_now.to_i, iss: 'issuer_name', aud: 'client', } end # Validates if the token is expired by exp parameter def self.expired(payload) Time.at(payload['exp']) < Time.now end end 

Давайте пройдемся по коду. Сначала в методе encode объедините полезную нагрузку, представляющую собой идентификатор пользователя, с метаинформацией, такой как срок действия, издатель и аудитория. Вы можете узнать об этих «мета» утверждениях из учебника JWT, который приведен в начале этого поста. После объединения кодируйте полезную нагрузку, используя метод JWT.encode вместе с секретным ключом с нашего сервера. Важно, чтобы этот ключ был защищенным, поскольку он является главным ключом для всех токенов, возникающих в нашем приложении.

Затем метод jwt , который использует метод jwt из jwt gem для (как вы уже догадались) декодирования полезной нагрузки с использованием секретного ключа. У нас есть пара других вспомогательных методов, один из которых — valid_payload который проверяет, был ли изменен полезный груз, и метод expired который проверяет, истек ли токен. Срок действия по умолчанию устанавливается в meta методе, который составляет 7 дней, но вы можете изменить его согласно вашему требованию.

Теперь у нас есть работающая конечная точка входа, которую мы можем использовать для входа пользователя. Попробуйте позвонить users/login в конечную точку, users/login указанные ниже данные в теле запроса:

 { "email": "[email protected]", "password": "anewpassword" } 

Вы должны увидеть ответ, похожий на этот,

 { "auth_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE0NzUzMTM5OTQsImlzcyI6Imlzc3Vlcl9uYW1lIiwiYXVkIjoiY2xpZW50In0.5P3qJKelCdbTixnLyIrsLKSVnRLCv2lvHFpXqVKdPOs" } 

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

Помощник по аутентификации

Хорошо, мы собираемся создать вспомогательный метод, который получает токен из заголовка, проверяет полезную нагрузку и выбирает соответствующего пользователя из БД. Откройте файл /app/controllers/application_controller.rb и добавьте следующее:

 protected # Validates the token and user and sets the @current_user scope def authenticate_request! if !payload || !JsonWebToken.valid_payload(payload.first) return invalid_authentication end load_current_user! invalid_authentication unless @current_user end # Returns 401 response. To handle malformed / invalid requests. def invalid_authentication render json: {error: 'Invalid Request'}, status: :unauthorized end private # Deconstructs the Authorization header and decodes the JWT token. def payload auth_header = request.headers['Authorization'] token = auth_header.split(' ').last JsonWebToken.decode(token) rescue nil end # Sets the @current_user with the user_id from payload def load_current_user! @current_user = User.find_by(id: payload[0]['user_id']) end 

Здесь authenticate_request! это вспомогательный метод, который мы собираемся использовать для аутентификации действий контроллера. Он выбирает полезную нагрузку из заголовка Authorization запроса, затем проверяет полезную нагрузку с valid_payload метода valid_payload , который мы видели ранее. После подтверждения действительного, он выбирает пользователя, используя user_id в полезной нагрузке, загружая запись пользователя в область.

Теперь мы можем добавить метод authenticate_request в качестве before_filter к любому действию контроллера, которое мы хотим аутентифицировать для пользователя.

Вывод

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

Пример приложения размещен на github . Не стесняйтесь раскошелиться и возиться с этим.

Я благодарю вас за чтение этого учебника, и я надеюсь, что он служит вашим целям. Счастливого обучения.