Аутентификация является одной из важнейших частей любого веб-приложения. Существуют бесчисленные библиотеки и платформы, которые предоставляют различные варианты для выполнения аутентификации тем или иным способом. Эти библиотеки забирают большую часть основы, необходимой для настройки системы аутентификации, обеспечивая «магию» происходящего за кулисами. Для 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 . Не стесняйтесь раскошелиться и возиться с этим.
Я благодарю вас за чтение этого учебника, и я надеюсь, что он служит вашим целям. Счастливого обучения.