Я решил написать серию сообщений, которые покажут, как я делаю определенные вещи с graphql-ruby в серверных приложениях Rails.
Сегодня я покажу, как я обрабатываю аутентификацию. Понимание этой статьи требует некоторых знаний GraphQL, Rails и того, как использовать devise (или работает какой-либо другой гем аутентификации).
Для введения в GraphQL вы можете прочитать этот пост и эти слайды. Чтобы увидеть, как создать простой GraphQL API с Rails, вы можете проверить это репо.
Пример приложения rails со всем, что описано в этой статье, можно найти на моем гитхабе здесь.
Любые конструктивные отзывы приветствуются.
Придумать
Devise - довольно популярный, довольно мощный гем для обработки аутентификации в приложении Rails. Я не буду объяснять, как установить / настроить устройство, документация довольно пояснительная. Несколько замечаний:
- Как обычно, мы создаем
User
модель. Вы можете не указывать модуль:confirmable
(не забудьте отредактировать перенос, если вы это сделаете). - В этом руководстве нет необходимости создавать какие-либо представления.
- Вам также потребуется установить и настроить этот гем devise-token_authenticatable. Это позволяет пользователю войти в систему с помощью токена аутентификации. Этот токен может быть предоставлен с помощью строки запроса или базовой аутентификации HTTP.
Тип пользователя
Вот простой пример GraphQL UserType
, с которым мы будем работать.
module Types UserType = GraphQL::ObjectType.define do name 'User' description 'Example User' field :lastName, !types.String, property: :last_name field :firstName, !types.String, property: :first_name field :email, !types.String end end
Регистрация пользователя
Теперь мы создаем мутацию, которая позволяет пользователю регистрироваться / регистрироваться. Однако, прежде чем мы это сделаем, давайте определим UserInputType
, который будет передан в качестве аргумента Мутации.
module Types module Input UserInputType = GraphQL::InputObjectType.define do name 'UserInputType' description 'Properties for registering a new User' argument :lastName, !types.String argument :firstName, !types.String argument :email, !types.String argument :password, !types.String end end end
А ниже у нас есть мутация:
module Mutations RegisterUser = GraphQL::Field.define do name 'RegisterUser' argument :registrationDetails, !Types::Input::UserInputType type Types::UserType resolve ->(_obj, args, _ctx) { input = Hash[args['registrationDetails'].to_h.map {|k, v| [k.to_s.underscore.to_sym, v]}] begin @user = User.create!(input) rescue ActiveRecord::RecordInvalid => invalid GraphQL::ExecutionError.new("Invalid Attributes for #{invalid.record.class.name}: #{invalid.record.errors.full_messages.join(', ')}") end } end end
Следует обратить внимание на приведенный выше фрагмент кода. Давайте посмотрим на строку ниже.
input = Hash[args['registrationDetails'].to_h.map {|k, v| [k.to_s.underscore.to_sym, v]}]
По соглашению типы, поля и аргументы GraphQL должны быть записаны в camelCase
. С другой стороны, в Ruby принято snake_case
почти все, кроме имен классов и модулей.
Это означает, что нам нужно легко преобразовать хэш нашего camelCase
аргумента в его snake_case
версию. Вот что делает эта строка.
Остальная часть фрагмента довольно проста; он пытается создать пользователя с вводом, и если по какой-то причине это не удается, выдает GraphQL::ExecutionError
.
Войти
module Types AuthType = GraphQL::ObjectType.define do name 'AuthType' field :authenticationToken, !types.String, property: :authentication_token end end
Выше показан тип, который вернет мутация SignIn
, а ниже - сама мутация.
module Mutations SignIn = GraphQL::Field.define do name 'SignIn' argument :email, !types.String argument :password, !types.String type Types::AuthType resolve ->(_obj, args, _ctx) { @user = User.find_for_database_authentication(email: args[:email]) if @user if @user.valid_password?(args[:password]) authentication_token = @user.authentication_token return OpenStruct.new(authentication_token: authentication_token) else GraphQL::ExecutionError.new('Incorrect Email/Password') end else GraphQL::ExecutionError.new('User not registered on this application') end } end end
Это также довольно просто и использует методы, которые Devise автоматически добавляет в модель User
.
Это простой шаг за шагом, как это работает:
- Убедитесь, что пользователь существует.
- Если пользователя не существует, выдать
GraphQL::ExecutionError
. - В противном случае проверьте, действителен ли пароль.
- Если пароль недействителен, вывести
GraphQL::ExecutionError
. - В противном случае верните
authentication_token
.
Авторизация
Все, что осталось сделать, это защитить определенные запросы или изменения от доступа, когда пользователь не вошел в систему.
Когда клиентское приложение вызывает мутацию SignIn
и получает взамен токен, теперь оно может делать дальнейшие запросы как входящие в систему, добавляя токен в заголовок запроса (Authorization: Bearer <token-is-here>
).
Вы можете протестировать передачу заголовка в свой GraphQL запрос с помощью Альтаира.
Поскольку devise & devise-token_authenticatable справляется со стрессом, связанным с проверкой заголовков для проверки токена и определения того, какой пользователь вошел в систему, остальное довольно легко сделать.
Во-первых, вы убедитесь, что передали current_user
(который является вспомогательным методом, предоставляемым разработчиком для пользователя, который в настоящее время вошел в систему) в контекст всех преобразователей запросов / мутаций GraphQL. Это сделано в вашем GraphqlController
class GraphqlController < ApplicationController def execute variables = ensure_hash(params[:variables]) query = params[:query] operation_name = params[:operationName] context = { current_user: current_user } result = TixSchema.execute(query, variables: variables, context: context, operation_name: operation_name) render json: result end ...
Во-вторых, мы создадим собственный простой помощник, который будет называться AuthorizeUser
.
AuthorizeUser
- это простой класс Ruby с методом call
. Он также должен иметь простой пользовательский initializer
.
module Resolvers module Helpers class AuthorizeUser def initialize(resolve_func) @resolve_func = resolve_func end def call(obj, args, ctx) if ctx[:current_user].blank? GraphQL::ExecutionError.new('User not signed in') else @resolve_func.call(obj, args, ctx) end end end end end
Когда используется AuthorizeUser.new(....
, он ожидает метод / лямбда-преобразователь GraphQL в качестве аргумента, который затем устанавливает как переменную экземпляра.
В этом методе call
он принимает те же самые аргументы, что и метод преобразователя / лямбда.
Сначала он проверяет, существует ли current_user
, который мы передали в контексте. Как обычно, если его не существует, выдает GraphQL::ExecutionError
. Однако, если current_user
существует, он передает его аргументы нетронутыми в call
метод @resolve_func
, который был передан во время инициализации.
Думайте об этом AuthorizeUser
как о функции преобразователя, которая оборачивается вокруг другой функции преобразователя.
Скажем, например, у нас была мутация под названием CreateTicket
, которая была примерно такой.
module Mutations CreateTicket = GraphQL::Field.define do name 'CreateTicket' argument :ticket, Types::Input::TicketInputType type Types::TicketType resolve ->(_obj, args, _ctx) { # perform ticket creation } end end
Если бы мы хотели сделать его таким, чтобы CreateTicket
мог использоваться только зарегистрированным пользователем, он бы выглядел так, используя приведенный выше вспомогательный класс.
module Mutations CreateTicket = GraphQL::Field.define do name 'CreateTicket' argument :ticket, Types::Input::TicketInputType type Types::TicketType resolve Resolvers::Helpers::AuthorizeUser.new(->(_obj, args, _ctx) { # perform ticket creation }) end end
Мы просто передали преобразователь CreateTicket
в качестве аргумента AuthorizeUser.new
, который затем выполнит содержимое преобразователя, только если current_user
присутствует.
Вот и все!
Помните, вы можете найти образец кода со всем, через что мы прошли здесь.