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


Камни, которые нам понадобятся:

gem 'bcrypt', '~> 3.1', '>= 3.1.12’

gem 'jwt', '~> 2.5

gem 'rack-cors'

gem 'active_model_serializers', '~> 0.10.12’
Войти в полноэкранный режим

Выйти из полноэкранного режима

После добавления запуска gemfile bundle install


Создайте маршруты

  post "/users", to: "users#create"
  get "/me", to: "users#me"
  post "/auth/login", to: "auth#login"
Войти в полноэкранный режим

Выйти из полноэкранного режима

Мы будем регистрировать новых пользователей, отправляя POST-запрос к /users. Существующий пользователь может войти в систему, отправив почтовый запрос на «/auth/login», а пользователь может получить доступ к данным пользователя, отправив запрос GET на «/me». Нам нужно как минимум 3 маршрута, позже можно будет добавить больше маршрутов.


Добавить КОРС

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
Войти в полноэкранный режим

Выйти из полноэкранного режима

Cross-Origin Resource Sharing (CORS) — это промежуточное ПО, которое будет принимать запросы к API только с одного URL-адреса клиента. URL-адрес клиента, которому мы хотим разрешить сделать запрос, войдет в origins. На данный момент мы устанавливаем * origins, что позволит любому делать запросы к нашему API на данный момент.


Создайте модель пользователя:

rails g model user username password_digest bio --no-test-framework
Войти в полноэкранный режим

Выйти из полноэкранного режима

Добавить has_secure_password макрос в пользовательской модели и проверка имени пользователя:

class User < ApplicationRecord
    has_secure_password
    validates :username, uniqueness: true
end
Войти в полноэкранный режим

Выйти из полноэкранного режима

has_secure_password это метод bcrypt, который шифрует пароль для каждого пользователя. Чтобы этот метод работал, мы добавляем password_digest поле в нашу таблицу базы данных. Однако, когда мы делаем почтовый запрос на наш сервер, мы отправляем password. Bcrypt сделает все остальное за нас.

Пример запроса:

fetch('URL/auth/login',{
method: POST,
headers: {
    'Content-type': 'application/json'
},
body: {
    username: 'randomUserName',
    password: 'ask^dsk34'
})

Войти в полноэкранный режим

Выйти из полноэкранного режима


Добавление JWT в наш API

Веб-токены JSON — это открытый стандарт RFC 7519 для безопасного представления претензий между двумя сторонами. Токен JWT выглядит так: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Описание изображения

Источник — JWT.io

Он состоит из трех частей. Первая часть — это заголовок, содержащий алгоритм и тип токена. Вторая часть — это полезная нагрузка, данные, которые мы хотим сохранить в токене. Третья часть — это подпись, которая содержит Секретный ключ. Мы собираемся сгенерировать токены JWT из application_controller.rb.

class ApplicationController < ActionController::API

    def encode_token(payload)
        JWT.encode(payload, 'hellomars1211') 
    end

    def decoded_token
        header = request.headers['Authorization']
        if header
            token = header.split(" ")[1]
            begin
                JWT.decode(token, 'hellomars1211')
            rescue JWT::DecodeError
                nil
            end
        end
    end

end
Войти в полноэкранный режим

Выйти из полноэкранного режима

encode_token Метод принимает полезную нагрузку в качестве аргумента. Мы передадим идентификатор пользователя в качестве полезной нагрузки. Затем мы вызываем JWT.encode(payload, 'hellomars1211') метод для кодирования нашего токена. Нашей полезной нагрузкой будет идентификатор пользователя, который затем мы можем использовать для поиска правильного пользователя. Обратите внимание, что мы передаем строку: 'hellomars1211'вместе с полезной нагрузкой в ​​качестве аргумента, который будет нашим секретным ключом, который мы также будем использовать для декодирования нашего токена. Секретный ключ может быть любой комбинацией букв, символов, цифр и т. д. Мы будем называть decoded_token метод декодирования токена JWT.

Всякий раз, когда мы делаем запрос к защищенному маршруту или ресурсу, мы передаем токен JWT вместе с нашими данными в запросе, в заголовке авторизации, используя схему Bearer. Пример запроса:

fetch("URL/me", {
  method: "GET",
  headers: {
    Authorization: `Bearer <token>`,
  },
});
Войти в полноэкранный режим

Выйти из полноэкранного режима

Описание изображения

Мы получаем доступ к токену из заголовка и декодируем токен, используя JWT.decode(token, 'hellomars1211', true, algorithm: 'HS256') нам также необходимо передать секретный ключ, чтобы расшифровать токен.

Теперь мы можем получить доступ к user.id из декодированного токена. Мы создадим метод current_user который берет идентификатор пользователя из декодированного токена и находит пользователя, используя тот же идентификатор пользователя. Это даст нам пользователя, который в данный момент вошел в систему. Мы создадим еще один метод authorized чтобы проверить, есть ли у нас текущий_пользователь, который вошел в систему.

def current_user 
    if decoded_token
        user_id = decoded_token[0]['user_id']
        @user = User.find_by(id: user_id)
    end
end

def authorized
    unless !!current_user
    render json: { message: 'Please log in' }, status: :unauthorized
    end
end
Войти в полноэкранный режим

Выйти из полноэкранного режима

Наконец, мы добавим before_action правило контроллеру, который будет вызывать authorized метод, прежде чем что-либо делать, и проверьте, вошел ли пользователь в систему. Для любого несанкционированного запроса мы выведем сообщение: Please log in. Вот как будет выглядеть наш application_controller:

*app/controllers/application_controller.rb*

class ApplicationController < ActionController::API
    before_action :authorized

    def encode_token(payload)
        JWT.encode(payload, 'hellomars1211') 
    end

    def decoded_token
        header = request.headers['Authorization']
        if header
            token = header.split(" ")[1]
            begin
                JWT.decode(token, 'hellomars1211', true, algorithm: 'HS256')
            rescue JWT::DecodeError
                nil
            end
        end
    end

    def current_user 
        if decoded_token
            user_id = decoded_token[0]['user_id']
            @user = User.find_by(id: user_id)
        end
    end

    def authorized
        unless !!current_user
        render json: { message: 'Please log in' }, status: :unauthorized
        end
    end

end
Войти в полноэкранный режим

Выйти из полноэкранного режима


Создайте user_controller

rails g controller users
Войти в полноэкранный режим

Выйти из полноэкранного режима

В пользовательском контроллере мы создадим действие для создания или регистрации нового пользователя. Мы также создадим токен, если пользователь подпишется с действительными данными, и отправим токен вместе с ответом, это заставит пользователя войти в систему сразу после регистрации в нашем приложении. Мы будем обрабатывать функцию регистрации в методе create в файле users_controller.rb.

class UsersController < ApplicationController
    rescue_from ActiveRecord::RecordInvalid, with: :handle_invalid_record

    def create 
        user = User.create!(user_params)
        @token = encode_token(user_id: user.id)
        render json: {
            user: UserSerializer.new(user), 
            token: @token
        }, status: :created
    end

    private

    def user_params 
        params.permit(:username, :password, :bio)
    end

    def handle_invalid_record(e)
            render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
    end
end
Войти в полноэкранный режим

Выйти из полноэкранного режима

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

class UserSerializer < ActiveModel::Serializer
  attributes :id, :username, :bio
end
Войти в полноэкранный режим

Выйти из полноэкранного режима

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

Также мы создадим метод me чтобы получить профиль пользователя, который вернет current_user который мы установили в файле application_controller.

*app/controllers/users_controller.rb*

class UsersController < ApplicationController
    skip_before_action :authorized, only: [:create]
    rescue_from ActiveRecord::RecordInvalid, with: :handle_invalid_record

    def create 
        user = User.create!(user_params)
        @token = encode_token(user_id: user.id)
        render json: {
            user: UserSerializer.new(user), 
            token: @token
        }, status: :created
    end

    def me 
        render json: current_user, status: :ok
    end

    private

    def user_params 
        params.permit(:username, :password, :bio)
    end

    def handle_invalid_record(e)
            render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
    end
end
Войти в полноэкранный режим

Выйти из полноэкранного режима

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

Описание изображения

Поехалиооооооо!! О, подождите! Что еще нам не хватает? Самая важная часть авторизации: АВТОРИЗОВАТЬСЯ!

Чтобы реализовать вход в систему, мы создадим новый контроллер и назовем его auth_controller.

rails g controller auth
Войти в полноэкранный режим

Выйти из полноэкранного режима

*app/controllers/auth_controller.rb*

class AuthController < ApplicationController

    skip_before_action :authorized, only: [:login]
    rescue_from ActiveRecord::RecordNotFound, with: :handle_record_not_found

    def login 
        @user = User.find_by!(username: login_params[:username])
        if @user.authenticate(login_params[:password])
            @token = encode_token(user_id: @user.id)
            render json: {
                user: UserSerializer.new(@user),
                token: @token
            }, status: :accepted
        else
            render json: {message: 'Incorrect password'}, status: :unauthorized
        end

    end

    private 

    def login_params 
        params.permit(:username, :password)
    end

    def handle_record_not_found(e)
        render json: { message: "User doesn't exist" }, status: :unauthorized
    end
end
Войти в полноэкранный режим

Выйти из полноэкранного режима

в login методом мы сначала находим пользователя с именем пользователя, если пользователь не найден, мы возвращаем сообщение об ошибке: "User doesn't exist". После того, как мы находим пользователя, мы аутентифицируем пользователя с помощью пароля, используя метод аутентификации bcrypt. После завершения аутентификации мы создаем токен для пользователя и возвращаем пользователя вместе с токеном. В случае неудачной аутентификации мы возвращаем сообщение об ошибке: 'Incorrect password'. Мы также добавили skip_before_action :authorized, only: [:login] здесь так же, как для метода создания в контроллере пользователей.

Сделаем несколько звонков в почтальоне:

Описание изображения

Описание изображения

Описание изображения

Наша авторизация завершена.

Аутентификация пользователя 3-х кратная. Сначала мы проверяем данные, затем аутентифицируем пользователя с правильным именем пользователя и паролем и, наконец, авторизуем пользователя.


Проверка—————> Аутентификация————> Авторизация

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

fetch("http://localhost:3000/auth/login/", {
      method: "POST",
      headers: {
        "Content-type": "application/json",
      },
      body: JSON.stringify({
        username: username,
        password: password,
      }),
    })
      .then((res) => res.json())
      .then((data) => {
        localStorage.setItem("jwt", data.jwt);
      })
Войти в полноэкранный режим

Выйти из полноэкранного режима

И мы просто удаляем токен jwt из localStorage для выхода из системы:

localStorage.removeItem("jwt")
Войти в полноэкранный режим

Выйти из полноэкранного режима