Добро пожаловать! Наша кухня разработки Ruby on Rails снова в этом. Мы надеемся, что вы готовы изучить следующую часть рецепта. Как обычно, вам не понадобятся блюда, ложки или ножи. Только ты и твой компьютер, готовый к программированию и созданию.
Тем, кто пропустил первую и вторую части нашего урока, не забудьте прочитать их в первую очередь.
Третий класс посвящен регистрации и авторизации . Они нужны каждой платформе электронной коммерции, поэтому мы не можем пропустить это важное требование. Безопасные покупки в Интернете очень важны и должны быть максимально частными и удобными.
Начнем с регистрации. Для начала создайте модель User с такими полями, как name: string , email: string , password_digest: string . Для этого выполните в консоли следующую команду:
$ rails generate model User name email password_digest
Затем не забудьте запустить другую команду:
$ rake db:migrate
Перейдите в файл app / models / user.rb и добавьте строку кода:
class User < ActiveRecord::Base
has_secure_password
end
Давайте подробно рассмотрим метод has_secure_password . Он используется для сохранения пароля, его проверки и шифрования (для него используется криптографическая хеш-функция BCrypt). Этот метод работает с полем password_digest, поэтому мы назвали его следующим. has_secure_password автоматически соединяет 3 проверки:
- Пароль должен быть введен во время создания.
- Длина пароля должна быть не более 72 символов.
- Подтверждение пароля (для этого используется атрибут password_confirmation ).
Чтобы has_secure_password работал правильно, вам необходимо подключить гем bcrypt . Перейдите в Gemfile и добавьте туда следующее:
gem 'bcrypt'
Не забудьте про команду:
$ bundle install
Давайте также добавим валидации к полям name и email . Мы будем использовать гем email_validator для проверки электронной почты. Его описание можно найти здесь: https://github.com/balexand/email_validator . Подключите его и напишите валидации в модели User .
class User < ActiveRecord::Base
has_secure_password
validates :name, presence: true
validates :email, presence: true, uniqueness: { case_sensitive: false }, email: true
end
Теперь мы должны покрыть только что созданную модель тестами. Для него будет использован драгоценный камень musta-matchers . Описание здесь: https://github.com/thoughtbot/shoulda-matchers . Перейдите в файл spec / models / user_spec.rb :
require 'rails_helper'
RSpec.describe User, type: :model do
it { should have_secure_password }
it { should validate_presence_of :name }
it { should validate_presence_of :email }
it { should validate_uniqueness_of(:email).case_insensitive }
it { should_not allow_value('test').for(:email) }
it { should allow_value('test@test.com').for(:email) }
end
Не забудьте запустить тесты:
$ rake
Давайте создадим синглтон-ресурс api / user с действием #create . Перейдите в config / rout.rb и код:
Rails.application.routes.draw do
namespace :api do
...
resource :user, only: [:create]
end
end
Затем создайте контроллер users_controller.rb в каталоге app / controllers / api и напишите в нем следующее:
class Api::UsersController < ApplicationController
private
def build_resource
@user = User.new resource_params
end
def resource
@user
end
def resource_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
end
Нам не нужно определять метод действия #create, как мы уже создали и определили его в ApplicationController в первой части нашего урока.
def create
build_resource
resource.save!
end
Вот почему этого будет достаточно, чтобы переопределить частные методы. После этого накройте контроллер тестами. Создайте файл spec / controllers / api / users_controller_spec.rb и напишите тест:
require 'rails_helper'
RSpec.describe Api::UsersController, type: :controller do
it { should route(:post, 'api/user').to(action: :create) }
describe '#create.json' do
let(:params) do
{
name: 'Test name',
email: 'test@test.com',
password: '12345678',
password_confirmation: '12345678'
}
end
let(:user) { stub_model User }
before { expect(User).to receive(:new).with(params).and_return(user) }
before { expect(user).to receive(:save!) }
before { post :create, user: params, format: :json }
it { should render_template :create }
end
end
В качестве следующего шага вам нужно создать UserDecorator и написать там метод #as_json, чтобы в ответе на этот запрос вернулись правильные данные.
Мы уже делали то же самое в первой части урока при создании продуктов.
class UserDecorator < Draper::Decorator
delegate_all
def as_json *args
{
name: name,
email: email
}
end
end
Не забудьте прикрыть декоратор тестами:
require 'rails_helper'
RSpec.describe UserDecorator do
describe '#as_json' do
let(:user) { stub_model User, name: 'Test name', email: 'test@test.com' }
subject { user.decorate.as_json }
its([:name]) { should eq 'Test name' }
its([:email]) { should eq 'test@test.com' }
end
end
Затем создайте app / view / application / create.json.erb :
<%= sanitize resource.decorate.to_json %>
После этого вам нужно подумать, как показать пользователю ошибки проверки. Перейдите в ApplicationController, и вы увидите, что метод #save! вызывается в методе #create в строке:
resource.save!
Этот метод довольно своеобразен: если при сохранении ресурса произойдет сбой, он вызовет ошибку ActiveRecord :: RecordInvalid . Благодаря этому мы можем перехватить (спасти) эту ошибку в ApplicationController :
class ApplicationController < ActionController::Base
...
rescue_from ActiveRecord::RecordInvalid do
render :errors, status: :unprocessable_entity
end
...
end
Это означает, что если вы не пройдете валидацию, будет отображен шаблон «Ошибки» со статусом 422 (необработанный объект).
Давайте создадим шаблон app / view / application / errors.json.erb :
<%= sanitize({ errors: resource.errors }.to_json) %>
Последнее, что нам нужно сделать, это перейти к ApplicationController и добавить:
class ApplicationController < ActionController::Base
...
skip_before_action :verify_authenticity_token, if: :json_request?
private
def json_request?
request.format.json?
end
...
end
Это отключит защиту от подделки межсайтовых запросов для запросов в формате json .
Превосходно! Запрос на регистрацию готов. Давайте проверим его производительность с помощью тестов:
$ rake
Если ошибок не возникает, мы можем проверить их производительность с помощью команды curl :
$ curl "http://localhost:3000/api/user" -H "Accept: application/json" -X POST -d "user[name]=test&user[email]=test@test.com&user[password]=test&user[password_confirmation]=test"
Вы должны увидеть следующее на экране:
{
"email": "test@test.com",
"name": "test"
}
Попробуйте повторить тот же запрос, и вы увидите ошибку:
{
"errors": {
"email": [
"has already been taken"
]
}
}
Выполнено. Регистрация завершена, и мы можем перейти к следующему пункту нашей сегодняшней задачи и создать авторизацию для наших пользователей. Для этого мы будем использовать аутентификацию на основе токенов .
Сначала нам нужно создать модель AuthToken со значением поля : строка и с внешним ключом user_id . Мы будем хранить пользовательские токены здесь. Запустите следующую команду в консоли:
$ rails generate model AuthToken
Затем перейдите к файлу миграции (отметка времени) _create_auth_tokens.rb и напишите:
class CreateAuthTokens < ActiveRecord::Migration
def change
create_table :auth_tokens do |t|
t.string :value
t.references :user, index: true, foreign_key: true
t.timestamps null: false
end
end
end
После этого запустите команду:
$ rake db:migrate
Откройте сгенерированную модель AuthToken и добавьте туда код:
class AuthToken < ActiveRecord::Base
belongs_to :user
validates :value, presence: true
end
Добавьте следующее в модель User :
class User < ActiveRecord::Base
...
has_one :auth_token, dependent: :destroy
...
end
Это означает, что мы определили соединение один к одному для моделей User и AuthToken . Зависимый вариант с уничтожить средства значения , что при удалении записи пользователя из базы данных связная запись из auth_tokens таблицы будет удалены тоже.
Также добавьте проверку для auth_token и затем напишите тесты для модели AuthToken :
require 'rails_helper'
RSpec.describe AuthToken, type: :model do
it { should belong_to :user }
it { should validate_presence_of :value }
end
Продолжите добавление теста в модель User :
it { should have_one(:auth_token).dependent(:destroy) }
В качестве следующего шага создайте файл / сеанс синглтон- ресурса с действием #create #destroy . Перейдите в config / rout.rb :
Rails.application.routes.draw do
namespace :api do
...
resource :session, only: [:create, :destroy]
end
end
Перед созданием SessionsController нам нужно создать класс Session , в котором будет сгенерирован токен для новой версии и проведена проверка сеанса. Поместите этот класс в каталог / lib . Для этого нам нужно написать способ загрузки файла в config / application.rb :
module Shop
class Application < Rails::Application
...
config.eager_load_paths << config.root.join('lib').to_s
end
end
Теперь мы можем начать писать класс Session :
class Session
include ActiveModel::Validations
attr_reader :email, :password, :user
def initialize params
params = params.try(:symbolize_keys) || {}
@user = params[:user]
@email = params[:email]
@password = params[:password]
end
validate do |model|
if user
model.errors.add :password, 'is invalid' unless user.authenticate password
else
model.errors.add :email, 'not found'
end
end
def save!
raise ActiveModel::StrictValidationFailed unless valid?
user.create_auth_token value: SecureRandom.uuid
end
def destroy!
user.auth_token.destroy!
end
def auth_token
user.try(:auth_token).try(:value)
end
def as_json *args
{ auth_token: auth_token }
end
def decorate
self
end
private
def user
@user ||= User.find_by email: email
end
end
Давайте проанализируем код. Конструктор ( метод initialize ) принимает хеш в качестве параметра и записывает в экземпляр такие переменные, как @email , @user и @password . Два метода get ( электронная почта и пароль ) также определены в строке:
attr_reader :email, :password
ActiveModel :: Validations позволяет использовать валидации в этом классе. Следующие методы связаны: ошибки , неверно? , Действует? , подтвердить , validates_with .
Поиск пользователя по электронной почте осуществляется в методе #user . Если это успешно, @user записывается в экземпляр, если ничего не найдено, то это ноль .
Нам нужно проверить валидацию, чтобы убедиться, что есть такой пользователь с адресом электронной почты и правильно ли введен пароль.
validate do |model|
if user
model.errors.add :password, 'is invalid' unless user.authenticate password
else
model.errors.add :email, 'not found'
end
end
Если пользователь недействителен, #save! Метод возвращает ошибку ActiveModel :: StrictValidationFailed . В противном случае создается новый токен.
#Destroy! Метод удаляет токен пользователя. Метод #auth_token возвращает значение токена.
Давайте определим методы #as_json и #decorate вместо использования декоратора.
Нам нужно покрыть этот класс тестами spec / lib / session_spec.rb .
require 'rails_helper'
RSpec.describe Session, type: :lib do
it { should be_a ActiveModel::Validations }
let(:session) { Session.new email: 'test@test.com', password: '12345678' }
let(:user) { stub_model User }
subject { session }
its(:email) { should eq 'test@test.com' }
its(:password) { should eq '12345678' }
its(:decorate) { should eq subject }
describe '#user' do
before { expect(User).to receive(:find_by).with(email: 'test@test.com') }
it { expect { subject.send :user }.to_not raise_error }
end
context 'validations' do
subject { session.errors }
context do
before { expect(session).to receive(:user) }
before { session.valid? }
its([:email]) { should eq ['not found'] }
end
context do
before { expect(session).to receive(:user).twice.and_return(user) }
before { expect(user).to receive(:authenticate).with('12345678').and_return(false) }
before { session.valid? }
its([:password]) { should eq ['is invalid'] }
end
end
describe '#save!' do
context do
before { expect(subject).to receive(:valid?).and_return(false) }
it { expect { subject.save! }.to raise_error(ActiveModel::StrictValidationFailed) }
end
context do
before { expect(subject).to receive(:user).and_return(user) }
before { expect(subject).to receive(:valid?).and_return(true) }
before { expect(SecureRandom).to receive(:uuid).and_return('XXXX-YYYY-ZZZZ') }
before { expect(user).to receive(:create_auth_token).with(value: 'XXXX-YYYY-ZZZZ') }
it { expect { subject.save! }.to_not raise_error }
end
end
describe '#destroy!' do
before do
#
# subject.user.auth_token.destroy!
#
expect(subject).to receive(:user) do
double.tap do |a|
expect(a).to receive(:auth_token) do
double.tap do |b|
expect(b).to receive(:destroy!)
end
end
end
end
end
it { expect { subject.destroy! }.to_not raise_error }
end
describe '#auth_token' do
context do
before { expect(subject).to receive(:user) }
its(:auth_token) { should eq nil }
end
context do
let(:auth_token) { stub_model AuthToken, value: 'XXXX-YYYY-ZZZZ' }
let(:user) { stub_model User, auth_token: auth_token }
before { expect(subject).to receive(:user).and_return(user) }
its(:auth_token) { should eq 'XXXX-YYYY-ZZZZ' }
end
end
describe '#as_json' do
before { expect(subject).to receive(:auth_token).and_return('XXXX-YYYY-ZZZZ') }
its(:as_json) { should eq auth_token: 'XXXX-YYYY-ZZZZ' }
end
end
Перейдите в ApplicationController и добавьте ошибку ActiveModel :: StrictValidationFailed в rescue_from :
rescue_from ActiveRecord::RecordInvalid, ActiveModel::StrictValidationFailed do
render :errors, status: :unprocessable_entity
end
Затем вы можете начать писать app / controllers / session_controller.rb :
class Api::SessionsController < ApplicationController
private
def build_resource
@session = Session.new resource_params
end
def resource
@session ||= Session.new user: current_user
end
def resource_params
params.require(:session).permit(:email, :password)
end
end
Не забывайте, что действия #create и #destroy определены в ApplicationController .
В методе #resource current_user является объектом класса User текущего пользователя. О его создании мы расскажем позже.
Таким образом, пользователь нашего интернет-магазина теперь может создать новый сеанс. Но как мы можем проверить это в запросах, которые запрашивают авторизацию? Может помочь API RoR ( метод authenticate_or_request_with_http_token, если быть более точным). Этот метод вызывает два других метода внутри себя: authenticate_with_http_token и request_http_token_authentication .
authenticate_with_http_token анализирует заголовок http с именем Authorization и возвращает значение токена. После этого нам нужно выполнить поиск токена в базе данных внутри блока этого метода. Заголовок должен выглядеть следующим образом:
«Authorization: Token token =» value «» Authorization — имя заголовка.
Токен означает, что авторизация основана на токене.
token = «» — это атрибут со значением токена. Request_http_token_authentication метод визуализации Ошибка 401 с текстом «HTTP Токен: отказано в доступе.» если метод authenticate_with_http_token возвращает ноль .
Перейдите в ApplicationController и добавьте:
class ApplicationController < ActionController::Base
....
before_action :authenticate
attr_reader :current_user
private
def authenticate
authenticate_or_request_with_http_token do |token, options|
@current_user = User.joins(:auth_token).find_by(auth_tokens: { value: token })
end
end
....
end
Теперь все запросы API запрашивают авторизацию, т.е. передачу токена. Но, например, запрос на создание сеанса не должен требовать авторизации. Нам нужно это исправить. Перейдите в SessionsController и добавьте:
class Api::SessionsController < ApplicationController
skip_before_action :authenticate, only: [:create]
....
end
Затем вам также нужно изменить ProductsController и UsersController :
class Api::ProductsController < ApplicationController
skip_before_action :authenticate
....
end
class Api::UsersController < ApplicationController
skip_before_action :authenticate
....
end
Большой! Давайте попробуем сделать пару запросов через curl в командной строке. Например:
$ curl "http://localhost:3000/api/session" -H "Accept: application/json" -X POST -d "session[email]=test@test.com&session[password]=test"
Если вы все сделали правильно, вы увидите сгенерированный токен на экране. Скопируйте, как нам понадобится для дальнейших запросов.
Попробуем сделать запрос, требующий авторизации, без передачи токена.
$ curl "http://localhost:3000/api/session" -H "Accept: application/json" -X DELETE
Вот что вы увидите на экране:
HTTP-токен: доступ запрещен.
Давайте сделаем тот же запрос на удаление сеанса, но с токеном на этот раз.
$ curl "http://localhost:3000/api/session" -H "Accept: application/json" -H "Authorization: Token token="your token"" -X DELETE
При успешной обработке этот запрос вернет пустое тело со статусом 200. Если вы попытаетесь повторить его, вы увидите текст «HTTP-токен: доступ запрещен». потому что токен был удален из базы данных.
Ну, пока об этом. Сегодняшние задачи были выполнены. Мы сделали регистрацию и авторизацию для интернет-магазина.
Вы движетесь в правильном направлении, и ваш проект электронной коммерции растет. Не пропустите следующую часть урока.
Мы продолжим наш урок в 2016 году! Команда разработчиков MLS желает вам счастливого Рождества и счастливого Нового года! Приятного отдыха, сохраняйте спокойствие и используйте Ruby!
Ваше домашнее задание — выполнить тесты для кода, который не рассматривается в этой части!
Ссылки:
API электронной коммерции Ruby on Rails на GitHub
Блог разработчиков MLS:
API электронной коммерции Ruby on Rails для начинающих — часть 1
API электронной коммерции Ruby on Rails для начинающих — часть 2
API электронной коммерции Ruby on Rails для начинающих — часть 3