railsチュートリアル挑戦記 第8章 基本的なログイン機構
railsチュートリアルをやりながらメモしたことをそのまま記述しています。
第8章基本的なログイン機構
ログイン機能を実装していく
・8 1 セッション
httpは状態のないプロトコル
ログインするとcreateでセッションを作成して保存
ログアウトするとdestroyでセッションを破棄
トピックブランチを作成する
git checkout -b basic-login
・8 1 1 Sessionsコントローラ
Sessionsコントローラを生成する
----------------------------
rails generate controller Sessions new
----------------------------
リソースを追加して標準的なRESTfulアクションをgetできるようにする red
config/routes.rb
----------------------------
Rails.application.routes.draw do
root 'static_pages#home'
get '/help', to: 'static_pages#help'
get '/about', to: 'static_pages#about'
get '/contact', to: 'static_pages#contact'
get '/signup', to: 'users#new'
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
resources :users
end
----------------------------
Sessionsコントローラのテストで名前付きルートを使うようにする green
test/controllers/sessions_controller_test.rb
----------------------------
require 'test_helper'
class SessionsControllerTest < ActionDispatch::IntegrationTest
test "should get new" do
get login_path
assert_response :success
end
end
----------------------------
HTTPリクエスト URL 名前付きルート アクション名 用途
GET /login login_path new 新しいセッションのページ (ログイン)
POST /login login_path create 新しいセッションの作成 (ログイン)
DELETE /logout logout_path destroy セッションの削除 (ログアウト)
ルーティングを確認するときは
rails routes
ログ
----------------------------
gin) $ rails routes
Prefix Verb URI Pattern Controller#Action
root GET / static_pages#home
help GET /help(.:format) static_pages#help
about GET /about(.:format) static_pages#about
contact GET /contact(.:format) static_pages#contact
signup GET /signup(.:format) users#new
login GET /login(.:format) sessions#new
POST /login(.:format) sessions#create
logout DELETE /logout(.:format) sessions#destroy
users GET /users(.:format) users#index
POST /users(.:format) users#create
new_user GET /users/new(.:format) users#new
edit_user GET /users/:id/edit(.:format) users#edit
user GET /users/:id(.:format) users#show
PATCH /users/:id(.:format) users#update
PUT /users/:id(.:format) users#update
DELETE /users/:id(.:format) users#destroy
ec2-user:~/environment/sample_app (basic-login) $
----------------------------
・8 1 2 ログインフォーム
form_for(@user)
と書くと、フォームのactionは/usersというURLのPOSTであると判断するが、
セッションの場合は具体的に書く必要がある
form_for(:session, url: login_path)
ログインフォームのコード
app/views/sessions/new.html.erb
----------------------------
<% provide(:title, "Log in") %>
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(:session, url: login_path) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.submit "Log in", class: "btn btn-primary" %>
<% end %>
<p>New user? <%= link_to "Sign up now!", signup_path %></p>
</div>
</div>
----------------------------
以下のようにHTMLが生成される
----------------------------
<form accept-charset="UTF-8" action="/login" method="post">
<input name="utf8" type="hidden" value="✓" />
<input name="authenticity_token" type="hidden"
value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />
<label for="session_email">Email</label>
<input class="form-control" id="session_email"
name="session[email]" type="text" />
<label for="session_password">Password</label>
<input id="session_password" name="session[password]"
type="password" />
<input class="btn btn-primary" name="commit" type="submit"
value="Log in" />
</form>
----------------------------
params[:session][:email]
params[:session][:password]
にそれぞれ値が入る
・8 1 3 ユーザーの検索と認証
Sessionsコントローラのcreateアクション (暫定版)
app/controllers/sessions_controller.rb
----------------------------
class SessionsController < ApplicationController
def new
end
def create
render 'new'
end
def destroy
end
end
images/figures/initial_failed_login_3rd_edition
----------------------------
ハッシュは以下の通りになる
{ session: { password: "foobar", email: "user@example.com" } }
ユーザー認証に必要なあらゆる情報をparamsハッシュから簡単に取り出せる
Active Recordが提供する
User.find_byメソッドでデータベースからユーザーを探し
has_secure_passwordが提供する
authenticateメソッドでパスワードをチェックする
authenticateメソッドは認証に失敗したときにfalseを返す
それを利用して下記の通り
ユーザーをデータベースから見つけて検証する
app/controllers/sessions_controller.rb
----------------------------
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
# ユーザーログイン後にユーザー情報のページにリダイレクトする
else
# エラーメッセージを作成する
render 'new'
end
end
def destroy
end
end
----------------------------
・8 1 4 フラッシュメッセージを表示する
ログイン失敗時の処理を扱う (誤りあり)
app/controllers/sessions_controller.rb
----------------------------
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
# ユーザーログイン後にユーザー情報のページにリダイレクトする
else
flash[:danger] = 'Invalid email/password combination' # 本当は正しくない
render 'new'
end
end
def destroy
end
end
----------------------------
実はこのままだとエラーが残り続ける
・8 1 5 フラッシュのテスト
統合テストを作成する
rails generate integration_test users_login
以下の流れでテストコードを実装していく
ログイン用のパスを開く
新しいセッションのフォームが正しく表示されたことを確認する
わざと無効なparamsハッシュを使ってセッション用パスにPOSTする
新しいセッションのフォームが再度表示され、フラッシュメッセージが追加されることを確認する
別のページ(Homeページなど)にいったん移動する
移動先のページでフラッシュメッセージが表示されていないことを確認する
フラッシュメッセージの残留をキャッチするテストred
test/integration/users_login_test.rb
----------------------------
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
test "login with invalid information" do
get login_path
assert_template 'sessions/new'
post login_path, params: { session: { email: "", password: "" } }
assert_template 'sessions/new'
assert_not flash.empty?
get root_path
assert flash.empty?
end
end
----------------------------
rails testの引数にテストファイルを与えると、そのテキストファイルだけを実行することができる
rails test test/integration/users_login_test.rb
ログ
----------------------------
ec2-user:~/environment/sample_app (basic-login) $ rails test test/integration/users_login_test.rb
Running via Spring preloader in process 5381
Started with run options --seed 39292
FAIL["test_login_with_invalid_information", UsersLoginTest, 0.45197109099990485]
test_login_with_invalid_information#UsersLoginTest (0.45s)
Expected false to be truthy.
test/integration/users_login_test.rb:11:in `block in <class:UsersLoginTest>'
1/1: [========================================] 100% Time: 00:00:00, Time: 00:00:00
Finished in 0.45428s
1 tests, 4 assertions, 1 failures, 0 errors, 0 skips
ec2-user:~/environment/sample_app (basic-login) $
----------------------------
ログイン失敗時の正しい処理 green
app/controllers/sessions_controller.rb
----------------------------
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
# ユーザーログイン後にユーザー情報のページにリダイレクトする
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
end
end
----------------------------
ログ
----------------------------
ec2-user:~/environment/sample_app (basic-login) $ rails test test/integration/users_login_test.rb
Running via Spring preloader in process 5420
Started with run options --seed 33833
1/1: [========================================] 100% Time: 00:00:00, Time: 00:00:00
Finished in 0.45133s
1 tests, 4 assertions, 0 failures, 0 errors, 0 skips
ec2-user:~/environment/sample_app (basic-login) $ rails test
Running via Spring preloader in process 5436
Started with run options --seed 59391
23/23: [======================================] 100% Time: 00:00:00, Time: 00:00:00
Finished in 0.58676s
23 tests, 48 assertions, 0 failures, 0 errors, 0 skips
ec2-user:~/environment/sample_app (basic-login) $
----------------------------
・8 2 ログイン
cookiesを使った一時セッションでユーザーをログインできるようにする。
めっちゃ修正必要なはずだけど、railsなら以下をやるだけでおk
ApplicationコントローラにSessionヘルパーモジュールを読み込む
app/controllers/application_controller.rb
----------------------------
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
include SessionsHelper
end
----------------------------
・8 2 1 log_inメソッド
同じログイン手法を様々な場所で使いまわせるようにするために、
sessionsヘルパーにlog_inという名前のメソッドを定義
log_inメソッド
app/helpers/sessions_helper.rb
----------------------------
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
end
----------------------------
ユーザーにログインする
app/controllers/sessions_controller.rb
----------------------------
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
end
end
----------------------------
cookies、Expires
・8 2 2 現在のユーザー
rubyだと||=が使える
セッションに含まれる現在のユーザーを検索する
app/helpers/sessions_helper.rb
----------------------------
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
# 現在ログイン中のユーザーを返す (いる場合)
def current_user
if session[:user_id]
@current_user ||= User.find_by(id: session[:user_id])
end
end
end
----------------------------
・8 2 3 レイアウトリンクを変更する
logged_in?ヘルパーメソッド
app/helpers/sessions_helper.rb
----------------------------
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
# 現在ログイン中のユーザーを返す (いる場合)
def current_user
if session[:user_id]
@current_user ||= User.find_by(id: session[:user_id])
end
end
# ユーザーがログインしていればtrue、その他ならfalseを返す
def logged_in?
!current_user.nil?
end
end
----------------------------
ヘッダーを弄る
ログイン中のユーザー用のレイアウトのリンクを変更する
app/views/layouts/_header.html.erb
----------------------------
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<%= link_to "sample app", root_path, id: "logo" %>
<nav>
<ul class="nav navbar-nav navbar-right">
<li><%= link_to "Home", root_path %></li>
<li><%= link_to "Help", help_path %></li>
<% if logged_in? %>
<li><%= link_to "Users", '#' %></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Account <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><%= link_to "Profile", current_user %></li>
<li><%= link_to "Settings", '#' %></li>
<li class="divider"></li>
<li>
<%= link_to "Log out", logout_path, method: :delete %>
</li>
</ul>
</li>
<% else %>
<li><%= link_to "Log in", login_path %></li>
<% end %>
</ul>
</nav>
</div>
</header>
----------------------------
application.jsにBootstrapのJavaScriptライブラリを追加する
app/assets/javascripts/application.js
----------------------------
//= require rails-ujs
//= require jquery
//= require bootstrap
//= require turbolinks
//= require_tree .
----------------------------
・8 2 4 レイアウトの変更をテストする
以下の手順でテストを作成する
ログイン用のパスを書く
セッション用パスに有効な情報をpostする
ログイン用リンクが表示されなくなったことを確認する
ログアウト用リンクが表示されていることを確認する
プロフィール用リンクが表示されていることを確認する
登録済みユーザーとしてログインしておく必要がある
fixture向けのdigestメソッドを追加する
app/models/user.rb
----------------------------
class User < ApplicationRecord
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }
# 渡された文字列のハッシュ値を返す
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
end
----------------------------
ユーザーログインのテストで使うfixture
test/fixtures/users.yml
----------------------------
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
----------------------------
<%= User.digest('password') %>でテストユーザー用の有効なパスワードを作成できる
有効な情報を使ってユーザーログインをテストする green
test/integration/users_login_test.rb
----------------------------
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "login with valid information" do
get login_path
post login_path, params: { session: { email: @user.email,
password: 'password' } }
assert_redirected_to @user
follow_redirect!
assert_template 'users/show'
assert_select "a[href=?]", login_path, count: 0
assert_select "a[href=?]", logout_path
assert_select "a[href=?]", user_path(@user)
end
end
----------------------------
assert_redirected_to @user
はリダイレクト先が正しいかどうかをチェックしている。
テストで問題ないことを確認!
・8 2 5 ユーザー登録時にログイン
ユーザー登録時にログアウトしたままだと戸惑うので、
ログイン状態にしておく
ユーザー登録中にログインする
app/controllers/users_controller.rb
----------------------------
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
log_in @user
flash[:success] = "Welcome to the Sample App!"
redirect_to @user
else
render 'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
end
----------------------------
テスト中のログインステータスを論理値で返すメソッド
test/test_helper.rb
----------------------------
ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
fixtures :all
# テストユーザーがログイン中の場合にtrueを返す
def is_logged_in?
!session[:user_id].nil?
end
end
----------------------------
ユーザー登録後のログインのテスト green
test/integration/users_signup_test.rb
----------------------------
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
.
.
.
test "valid signup information" do
get signup_path
assert_difference 'User.count', 1 do
post users_path, params: { user: { name: "Example User",
email: "user@example.com",
password: "password",
password_confirmation: "password" } }
end
follow_redirect!
assert_template 'users/show'
assert is_logged_in?
end
end
----------------------------
テストOK
・8 3 ログアウト
log_outメソッド
app/helpers/sessions_helper.rb
----------------------------
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
.
.
.
# 現在のユーザーをログアウトする
def log_out
session.delete(:user_id)
@current_user = nil
end
end
----------------------------
セッションを破棄する (ユーザーのログアウト)
app/controllers/sessions_controller.rb
----------------------------
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
redirect_to user
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def destroy
log_out
redirect_to root_url
end
end
ログアウト
----------------------------
ユーザーログアウトのテスト green
test/integration/users_login_test.rb
----------------------------
require 'test_helper'
class UsersLoginTest < ActionDispatch::IntegrationTest
.
.
.
test "login with valid information followed by logout" do
get login_path
post login_path, params: { session: { email: @user.email,
password: 'password' } }
assert is_logged_in?
assert_redirected_to @user
follow_redirect!
assert_template 'users/show'
assert_select "a[href=?]", login_path, count: 0
assert_select "a[href=?]", logout_path
assert_select "a[href=?]", user_path(@user)
delete logout_path
assert_not is_logged_in?
assert_redirected_to root_url
follow_redirect!
assert_select "a[href=?]", login_path
assert_select "a[href=?]", logout_path, count: 0
assert_select "a[href=?]", user_path(@user), count: 0
end
end
----------------------------
ここでtestしてOKなことを確認!
・8 4 最後に
できた!
・8 4 1 本章のまとめ
Railsのsessionメソッドを使うと、あるページから別のページに移動するときの状態を保持できる。
一時的な状態の保存にはcookiesも使える
ログインフォームでは、ユーザーがログインするための新しいセッションが作成できる
flash.nowメソッドを使うと、描画済みのページにもフラッシュメッセージを表示できる
テスト駆動開発は、回帰バグを防ぐときに便利
sessionメソッドを使うと、ユーザーIDなどをブラウザに一時的に保存できる
ログインの状態に応じて、ページ内で表示するリンクを切り替えることができる
統合テストでは、ルーティング、データベースの更新、レイアウトの更新が正しく行われているかを確認できる
この記事が気に入ったらサポートをしてみませんか?