Rails Tutorial 5 Basic login
Rails Tutorial 5 Basic login
今回は Chapter 8/ Basic login | Ruby on Rails Tutorial (Rails 5) | Softcover.ioに沿って認証システムを導入し、ユーザーがログインとログアウトをできるように作り変えていく。
Sessions
トピックブランチで作業を進める。
$ git checkout -b basic-login
ログインとログアウトの要素を、Sessionsコントローラの特定のRESTアクションにそれぞれ対応付ける。
- new アクションでログインのフォームを処理
- create アクションへの POST リクエストでログイン
- destroy アクションへの DELETE リクエストでログアウト
これらを実装していく。
まずは Sessions コントローラとnew アクションを生成する。
$ rails generate controller Sessions new Running via Spring preloader in process 498610 create app/controllers/sessions_controller.rb route get 'sessions/new' invoke erb create app/views/sessions create app/views/sessions/new.html.erb invoke test_unit create test/controllers/sessions_controller_test.rb invoke helper create app/helpers/sessions_helper.rb invoke test_unit invoke assets invoke coffee create app/assets/javascripts/sessions.coffee invoke scss create app/assets/stylesheets/sessions.scss
なお、rails generateでnewアクションを生成すると、それに対応するビューも生成されます。createやdestroyには対応するビューが必要ないので、無駄なビューを作成しないためにここではnewだけを指定しています。
とある通り、new ではログインのフォームを表示するビューが必要なため上記で自動生成し、createとdestroyはビューが不要なため指定から外している。
次に名前付きルーティングを設定する。
config/routes.rb
に以下を追記する。
get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy'
上記に合わせ、自動生成されたテスト test/controllers/sessions_controller_test.rb
を下記のように書き換える。
・ ・ ・ test "should get new" do get login_path assert_response :success end ・ ・ ・
全ルーティングは下記の通り。
$ 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
Login form
ここからログインフォームを作成していく。
フォーム自体はユーザ登録フォームと大差ないが、 - 表示するフィールドが異なる - セッションは Active Record オブジェクトではないのでエラーは今回は flash で表示する
と言った違いがある。
- モデルが無い場合のform_forヘルパーの使用方法
- form から送信されたメールアドレス、パスワードと言ったユーザ認証に必要な情報を取り出す方法
- flash は表示後リダイレクトで消すか再描画だけで消したいかで .now との使い分けが必要
- “エラーをキャッチするテストを先に書いて、そのエラーが解決するようにコードを書く” の実践
と進んでいく。
Logging in
Rails で定義されている session メソッドを使って、ブラウザを終了すると期限が切れる一時 cookies での単純なログインを実装していく。
Sessions コントローラを生成した際に自動生成されているセッション用ヘルパーを、全コントローラで使用できるようにベースクラスにインクルードする。
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base protect_from_forgery with: :exception include SessionsHelper end
次に Sessions コントローラに log_in 関数を定義する。
app/helpers/sessions_helper.rb
module SessionsHelper # Logs in the given user. def log_in(user) session[:user_id] = user.id end end
sessionメソッドで作成した一時cookiesは自動的に暗号化され、リスト 8.14のコードは保護されます。そしてここが重要なのですが、攻撃者がたとえこの情報をcookiesから盗み出すことができたとしても、それを使って本物のユーザーとしてログインすることはできないのです。
ふむふむ
ただし今述べたことは、sessionメソッドで作成した「一時セッション」にしか該当しません。cookiesメソッドで作成した「永続的セッション」ではそこまで断言はできません。永続的なcookiesには、セッションハイジャックという攻撃を受ける可能性が常につきまといます。
ふむふむ? ここについては次の章で、だそうな。
このヘルパーメソッドを使用してユーザーログインを行ってセッションのcreateアクションを完了し、ユーザーのプロフィールページにリダイレクトする。
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の情報を調べると。。? 後でやってみよう。
ログイン済みのユーザ名を表示するためにヘルパーにメソッドを追加する。
app/helpers/sessions_helper.rb
module SessionsHelper # Logs in the given user. def log_in(user) session[:user_id] = user.id end # Returns the current logged-in user (if any). def current_user @current_user ||= User.find_by(id: session[:user_id]) end end
Changing the layout links
ログイン中とそうでないときでレイアウトを変更する。
ログイン中には以下のリンクを表示するようにする。 - ログアウト - ユーザー設定 - ユーザー一覧 - プロフィール表示
統合テストを先に書くことが望ましいかもしれないがここではまた新しいことを学ぶので先に実装してしまいましょう、だそうな。臨機応変、臨機応変。
まずログイン中かどうかを返すメソッドをヘルパーに追加する。
app/helpers/sessions_helper.rb
# Returns true if the user is logged in, false otherwise. def logged_in? !current_user.nil? 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>
ドロップダウンメニューが使用されている。
これらのドロップダウンの機能を有効にするため、Railsのapplication.jsファイルを通して、アセットパイプラインにBootstrapのカスタムJavaScriptライブラリをインクルードするように指示します
ほうほう。
app/assets/javascripts/application.js
//= require jquery //= require jquery_ujs //= require bootstrap //= require turbolinks //= require_tree .
これでログイン中とそうでないときでレイアウトを変更する実装が(まだ途中だが)できたのでテストを作成していく。
ログイン時の動作を確認するためには有効なユーザが登録されている必要があるので fixture を使う。
まず app/models/user.rb
に fixture 向け digest メソッドを追加する。
# Returns the hash digest of the given string. def User.digest(string) cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost BCrypt::Password.create(string, cost: cost) end
上記を使った fixture を作成する。
test/fixtures/users.yml
michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %>
これでテストは GREEN になる。
Login upon signup
ユーザ登録が終わったユーザにログインさせては手間なので、ユーザ登録完了時に自動でログインするよう実装を変更する。
と言ってもUsersコントローラのcreateアクションにlog_inを足すだけだそうな。
app/controllers/users_controller.rb
def create @user = User.new(user_params) if @user.save log_in @user flash[:success] = "Welcome to the Sample App!" redirect_to @user else
この動作を確認するようテストを変更する。
ヘルパーメソッドはテストから呼び出しできないのでsessionメソッドを使ってテストヘルパーを作成すると良いそうな。
test/test_helper.rb
# Returns true if a test user is logged in. def is_logged_in? !session[:user_id].nil? end
これで、ユーザ登録完了時にログイン状態になっているかどうかを以下のようにテストできるようになる。
test/integration/users_signup_test.rb
assert is_logged_in?
Logging out
ログアウト機能を実装する。
app/helpers/sessions_helper.rb
に log_out メソッドを定義する。
# Logs out the current user. def log_out session.delete(:user_id) @current_user = nil end
これを Sessions コントローラの destroy アクションから呼び出す。
app/controllers/sessions_controller.rb
def destroy log_out redirect_to root_url end
これで実装は完了。テストを修正していく。
test/integration/users_login_test.rb
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
自動テストが走り、結果全テストスイートが GREEN となっていることが確認できる。
[1] guard(main)> 22:51:51 - INFO - Run all 22:51:51 - INFO - Running: all tests Running via Spring preloader in process 198087 Started with run options --seed 64342 22/22: [=========================================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.97809s 22 tests, 58 assertions, 0 failures, 0 errors, 0 skips
あとはいつも通り master にマージしてリモートにプッシュ。Heroku へのデプロイも行う。
furue:~/workspace/sample_app (basic-login) $ git add -A kfurue:~/workspace/sample_app (basic-login) $ git commit -m "Implement basic login" [basic-login 55d4b22] Implement basic login 16 files changed, 160 insertions(+), 11 deletions(-) create mode 100644 app/assets/javascripts/sessions.coffee create mode 100644 app/assets/stylesheets/sessions.scss create mode 100644 app/controllers/sessions_controller.rb create mode 100644 app/helpers/sessions_helper.rb create mode 100644 app/views/sessions/new.html.erb create mode 100644 test/controllers/sessions_controller_test.rb create mode 100644 test/integration/users_login_test.rb kfurue:~/workspace/sample_app (basic-login) $ git checkout master Switched to branch 'master' Your branch is up-to-date with 'origin/master'. kfurue:~/workspace/sample_app (master) $ git merge basic-login Updating 11c9daa..55d4b22 Fast-forward app/assets/javascripts/application.js | 1 + app/assets/javascripts/sessions.coffee | 3 +++ app/assets/stylesheets/sessions.scss | 3 +++ app/controllers/application_controller.rb | 5 +---- app/controllers/sessions_controller.rb | 21 +++++++++++++++++++++ app/controllers/users_controller.rb | 5 +++-- app/helpers/sessions_helper.rb | 23 +++++++++++++++++++++++ app/models/user.rb | 7 +++++++ app/views/layouts/_header.html.erb | 23 ++++++++++++++++++++--- app/views/sessions/new.html.erb | 19 +++++++++++++++++++ config/routes.rb | 3 +++ test/controllers/sessions_controller_test.rb | 9 +++++++++ test/fixtures/users.yml | 5 ++++- test/integration/users_login_test.rb | 38 ++++++++++++++++++++++++++++++++++++++ test/integration/users_signup_test.rb | 1 + test/test_helper.rb | 5 ++++- 16 files changed, 160 insertions(+), 11 deletions(-) create mode 100644 app/assets/javascripts/sessions.coffee create mode 100644 app/assets/stylesheets/sessions.scss create mode 100644 app/controllers/sessions_controller.rb create mode 100644 app/helpers/sessions_helper.rb create mode 100644 app/views/sessions/new.html.erb create mode 100644 test/controllers/sessions_controller_test.rb create mode 100644 test/integration/users_login_test.rb kfurue:~/workspace/sample_app (master) $ rails test Running via Spring preloader in process 198402 Started with run options --seed 40205 22/22: [=========================================================================================] 100% Time: 00:00:01, Time: 00:00:01 Finished in 1.05401s 22 tests, 58 assertions, 0 failures, 0 errors, 0 skips kfurue:~/workspace/sample_app (master) $ git push Warning: Permanently added 'bitbucket.org,104.192.143.2' (RSA) to the list of known hosts. Counting objects: 32, done. Delta compression using up to 8 threads. Compressing objects: 100% (31/31), done. Writing objects: 100% (32/32), 4.64 KiB | 0 bytes/s, done. Total 32 (delta 17), reused 0 (delta 0) To git@bitbucket.org:kfurue/sample_app.git 11c9daa..55d4b22 master -> master kfurue:~/workspace/sample_app (master) $ git push heroku Counting objects: 32, done. Delta compression using up to 8 threads. Compressing objects: 100% (31/31), done. Writing objects: 100% (32/32), 4.64 KiB | 0 bytes/s, done. Total 32 (delta 17), reused 0 (delta 0) remote: Compressing source files... done. remote: Building source: remote: remote: -----> Ruby app detected remote: -----> Compiling Ruby/Rails remote: Your app was upgraded to bundler 1.13.7. remote: Previously you had a successful deploy with bundler 1.13.6. remote: remote: If you see problems related to the bundler version please refer to: remote: https://devcenter.heroku.com/articles/bundler-version remote: -----> Using Ruby version: ruby-2.2.4 remote: -----> Installing dependencies using bundler 1.13.7 remote: Running: bundle install --without development:test --path vendor/bundle --binstubs vendor/bundle/bin -j4 --deployment remote: Fetching gem metadata from https://rubygems.org/......... remote: Fetching version metadata from https://rubygems.org/.. remote: Fetching dependency metadata from https://rubygems.org/. remote: Using rake 11.2.2 remote: Using i18n 0.7.0 remote: Using minitest 5.9.0 remote: Using concurrent-ruby 1.0.2 remote: Using thread_safe 0.3.5 remote: Using builder 3.2.2 remote: Using erubis 2.7.0 remote: Using mini_portile2 2.1.0 remote: Using pkg-config 1.1.7 remote: Using rack 2.0.1 remote: Using nio4r 1.2.1 remote: Using websocket-extensions 0.1.2 remote: Using mime-types-data 3.2016.0521 remote: Using arel 7.1.1 remote: Using execjs 2.7.0 remote: Using bcrypt 3.1.11 remote: Using sass 3.4.22 remote: Using coffee-script-source 1.10.0 remote: Using method_source 0.8.2 remote: Using thor 0.19.1 remote: Using multi_json 1.12.1 remote: Using pg 0.18.4 remote: Using puma 3.4.0 remote: Using bundler 1.13.7 remote: Using tilt 2.0.5 remote: Using turbolinks-source 5.0.0 remote: Using tzinfo 1.2.2 remote: Using rack-test 0.6.3 remote: Using sprockets 3.7.0 remote: Using websocket-driver 0.6.4 remote: Using mime-types 3.1 remote: Using autoprefixer-rails 6.5.0.1 remote: Using uglifier 3.0.0 remote: Using nokogiri 1.6.8 remote: Using turbolinks 5.0.1 remote: Using activesupport 5.0.0.1 remote: Using mail 2.6.4 remote: Using bootstrap-sass 3.3.6 remote: Using coffee-script 2.4.1 remote: Using loofah 2.0.3 remote: Using rails-dom-testing 2.0.1 remote: Using globalid 0.3.7 remote: Using activemodel 5.0.0.1 remote: Using jbuilder 2.4.1 remote: Using rails-html-sanitizer 1.0.3 remote: Using activejob 5.0.0.1 remote: Using activerecord 5.0.0.1 remote: Using actionview 5.0.0.1 remote: Using actionpack 5.0.0.1 remote: Using actioncable 5.0.0.1 remote: Using actionmailer 5.0.0.1 remote: Using railties 5.0.0.1 remote: Using sprockets-rails 3.2.0 remote: Using coffee-rails 4.2.1 remote: Using jquery-rails 4.1.1 remote: Using rails 5.0.0.1 remote: Using sass-rails 5.0.6 remote: Bundle complete! 22 Gemfile dependencies, 57 gems now installed. remote: Gems in the groups development and test were not installed. remote: Bundled gems are installed into ./vendor/bundle. remote: Bundle completed (4.11s) remote: Cleaning up the bundler cache. remote: Removing bundler (1.13.6) remote: -----> Detecting rake tasks remote: -----> Preparing app for Rails asset pipeline remote: Running: rake assets:precompile remote: I, [2017-01-26T22:56:19.431707 #422] INFO -- : Writing /tmp/build_506d95587b2555e8da043d094b950b40/public/assets/application-0818f0786393846d9323fc0d27121442c638ab69ec67b3ebe5e0050e00c18c01.js remote: I, [2017-01-26T22:56:19.468053 #422] INFO -- : Writing /tmp/build_506d95587b2555e8da043d094b950b40/public/assets/application-0818f0786393846d9323fc0d27121442c638ab69ec67b3ebe5e0050e00c18c01.js.gz remote: Asset precompilation completed (11.56s) remote: Cleaning assets remote: Running: rake assets:clean remote: I, [2017-01-26T22:56:25.901020 #433] INFO -- : Removed application-db5fed084311c4f37b7088efa1db9925ddf5cd86e135ad019fea6c0bc4abebae.js remote: remote: ###### WARNING: remote: You have not declared a Ruby version in your Gemfile. remote: To set your Ruby version add this line to your Gemfile: remote: ruby '2.2.4' remote: # See https://devcenter.heroku.com/articles/ruby-versions for more information. remote: remote: -----> Discovering process types remote: Procfile declares types -> web remote: Default types for buildpack -> console, rake, worker remote: remote: -----> Compressing... remote: Done: 30.2M remote: -----> Launching... remote: Released v11 remote: https://fierce-wave-40771.herokuapp.com/ deployed to Heroku remote: remote: Verifying deploy... done. To https://git.heroku.com/fierce-wave-40771.git 11c9daa..55d4b22 master -> master