この記事は『Code Polaris Advent Calendar 2024』 の3日目の記事です。
この記事について
Rails8が11/8に正式リリースとなりました🎉
https://www.publickey1.jp/blog/24/ruby_on_rails_8sqlitedb6.html
なんとこの記事を書いている土日に珍しく夫が仕事休んでくれて子どもを見ててくれたのでPCがっつり開ける!ということでこないだ技術書典で購入したRails8入門を参考に触ってみました。
環境構築に関しては、本だとrails-newアプリとdevcontainerで環境構築しているのですが私はローカルで構築してます。理由としては今までの案件ではこの方法の方が多かったので、ひとまずはこっちの方法で試してみるかと思ったためです。
今度、rails-newアプリとdevcontainerのパターンでやるとどれだけ早いのか比較してみようと思います。(ちなみにローカル環境の構築は少しハマったのもあり2hくらいかかりました)
おそらく環境に依存せずサクッと構築するにはrails-newアプリとdevcontainerのパターンがお手軽なのかなと推測しています。
久しぶりに個人PCでrails newしたので備忘録も兼ねて環境構築部分からメモを残しています。
環境
M1 Mac(OS: Ventura)
ruby 3.3.6
PostgreSQL 14.14
環境構築
rubyのバージョン管理はasdfを使用しています。
https://qiita.com/murakami-mm/items/2d63177dc8ea002a847b
asdf install ruby 3.3.6の際に途中 BUILD FAILED (macOS 13.0.1 on arm64 using ruby-build 20241105)
が出たが、下記記事を参考に環境変数を~/.zshrc
にセットすることで解決
export RUBY_CONFIGURE_OPTS="--with-openssl-dir=$(brew --prefix openssl@1.1)"
export LDFLAGS="-L/opt/homebrew/opt/readline/lib"
export CPPFLAGS="-I/opt/homebrew/opt/readline/include"
export PKG_CONFIG_PATH="/opt/homebrew/opt/readline/lib/pkgconfig"
export optflags="-Wno-error=implicit-function-declaration"
export LDFLAGS="-L/opt/homebrew/opt/libffi/lib"
export CPPFLAGS="-I/opt/homebrew/opt/libffi/include"
export PKG_CONFIG_PATH="/opt/homebrew/opt/libffi/lib/pkgconfig"
export RUBY_CFLAGS="-w"
% mkdir rails8_test
% cd rails8_test
% asdf latest ruby
% asdf install ruby 3.3.6
% asdf local ruby 3.3.6
久しぶりなものでrails newからプロジェクト作る方法をググる・・・
https://qiita.com/yuitnnn/items/b45bba658d86eabdbb26
rails8_test % bundle init
Writing new Gemfile to /Users/xxxx/Jobs/rails8_test/Gemfile
# で、Gemfileのgem "rails"のコメントアウトを外す
# % cat ~/.bundle/config
# ---
# BUNDLE_PATH: "vendor/bundle"
# ↑の設定なのでオプションはつけてません
rails8_test % bundle install
きちんとrailsのバージョンが8でインストールされているのでよし
rails (8.0.0)
rails8_test % bundle exec rails new . -B -d postgresql --skip-test
exist
create README.md
create Rakefile
create .ruby-version
create config.ru
create .gitignore
create .gitattributes
conflict Gemfile
Overwrite /Users/sagawaai/Jobs/rails8_test/Gemfile? (enter "h" for help) # 先にGemfileいじらなくても良かった。上書き[Ynaqdhm] Y
force Gemfile
run git init -b main from "."
rails8_test % bin/rails s
Could not find gem 'propshaft' in locally installed gems.
Run `bundle install --gemfile /Users/sagawaai/Jobs/rails8_test/Gemfile` to install missing gems.
rails8_test % bundle install --gemfile /Users/sagawaai/Jobs/rails8_test/Gemfile
# この後psql関連のエラーが出てdb:createできなかったので下記を参考に解消
https://loveenglish.hatenablog.com/entry/2023/08/04/111831
rails8_test % bin/rails db:create
Created database 'rails8_test_development'
Created database 'rails8_test_test'
rails8_test % bin/rails s
🎉 (スクショのリンクがamazonaws・・・QiitaはAWSなんだ←どうでもいい)
Rails8の認証機能試してみる!
ここからはRails8の機能を色々試していきたいなというところなので、技術書典17で購入した下記の本を参考に進めていこうと思ってます。(第3章〜)
今回は認証機能に絞って色々確認してみました。
Ruby on Rails 8 入門
吉田智哉 著
https://x.com/tomoya_y0shida?lang=fa
https://techbookfest.org/product/41bEtMuRb2xADJH5E0WWVC?productVariantID=2hufyTuCnbtJu8bCHDG6pU
本ではジェネレーターで作成されるデフォルトの機能とそうでない機能も整理されていて、何ができて何ができないか(= 自前実装が必要か)の基準になるのでありがたかったです。
以下引用
注意点としては、現時点で認証機能は以下の機能のみサポートしています。
• ログイン (Authenticatable)
• ログアウト (Authenticatable)
• パスワードリセット (Recoverable)
• IP アドレスとログイン時刻の記録 (Trackable)
• ログイン回数の制限 (Lockable)サインアップや Devise では標準でサポートされている以下の機能はサポートされてい ません。
• メール認証 (Confirmable)
• ソーシャルログイン (Omniauthable)
• ログインの記憶 (Rememberable)
• IP のトラッキング (Trackable)
• バリデーション (Validatable)
• セッションの時間制限 (Timeoutable)
これらの機能の実装は自分で追加する必要があります。
1. 認証ジェネレータで作成される機能
ログイン機能作成
第3章ではジェネレーターを使った設定方法を説明してくれてます。
意外に作られるファイルが少ない。
テーブルもUserとSession管理用のみ
rails8_test % bin/rails generate authentication
invoke erb
create app/views/passwords/new.html.erb
create app/views/passwords/edit.html.erb
create app/views/sessions/new.html.erb
create app/models/session.rb
create app/models/user.rb
create app/models/current.rb
create app/controllers/sessions_controller.rb
create app/controllers/concerns/authentication.rb
create app/controllers/passwords_controller.rb
create app/channels/application_cable/connection.rb
create app/mailers/passwords_mailer.rb
create app/views/passwords_mailer/reset.html.erb
create app/views/passwords_mailer/reset.text.erb
create test/mailers/previews/passwords_mailer_preview.rb
insert app/controllers/application_controller.rb
route resources :passwords, param: :token
route resource :session
gsub Gemfile
bundle install --quiet
/Users/sagawaai/.asdf/installs/ruby/3.3.6/bin/ruby: No such file or directory -- bin/bundle (LoadError)
generate migration CreateUsers email_address:string!:uniq password_digest:string! --force
rails generate migration CreateUsers email_address:string!:uniq password_digest:string! --force
Could not find gem 'bcrypt (~> 3.1.7)' in locally installed gems.
Run `bundle install --gemfile /Users/sagawaai/Jobs/rails8_test/Gemfile` to install missing gems.
# パスワードをハッシュ化するためgem 'bcrypt(~> 3.1.7)'を有効にする(= Gemfileのコメントアウトを外す)
rails8_test % bundle install --gemfile /Users/sagawaai/Jobs/rails8_test/Gemfile
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...
Fetching bcrypt 3.1.20
Installing bcrypt 3.1.20 with native extensions
WARN: Unresolved or ambiguous specs during Gem::Specification.reset:
stringio (>= 0)
Available/installed versions of this gem:
- 3.1.2
- 3.1.1
WARN: Clearing out unresolved specs. Try 'gem cleanup <gem>'
Please report a bug if this causes problems.
Bundle complete! 20 Gemfile dependencies, 106 gems now installed.
Bundled gems are installed into `./vendor/bundle`
rails8_test % bin/rails generate authentication
invoke erb
identical app/views/passwords/new.html.erb
identical app/views/passwords/edit.html.erb
identical app/views/sessions/new.html.erb
identical app/models/session.rb
identical app/models/user.rb
identical app/models/current.rb
identical app/controllers/sessions_controller.rb
identical app/controllers/concerns/authentication.rb
identical app/controllers/passwords_controller.rb
identical app/channels/application_cable/connection.rb
identical app/mailers/passwords_mailer.rb
identical app/views/passwords_mailer/reset.html.erb
identical app/views/passwords_mailer/reset.text.erb
identical test/mailers/previews/passwords_mailer_preview.rb
unchanged app/controllers/application_controller.rb
route resources :passwords, param: :token
route resource :session
gsub Gemfile
bundle install --quiet
/Users/sagawaai/.asdf/installs/ruby/3.3.6/bin/ruby: No such file or directory -- bin/bundle (LoadError)
generate migration CreateUsers email_address:string!:uniq password_digest:string! --force
rails generate migration CreateUsers email_address:string!:uniq password_digest:string! --force
invoke active_record
create db/migrate/20241116053429_create_users.rb
generate migration CreateSessions user:references ip_address:string user_agent:string --force
rails generate migration CreateSessions user:references ip_address:string user_agent:string --force
invoke active_record
create db/migrate/20241116053431_create_sessions.rb
rails8_test % bin/rails db:migrate
# Userテーブルを作成している
== 20241116053429 CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0081s
-- add_index(:users, :email_address, {:unique=>true})
-> 0.0163s
== 20241116053429 CreateUsers: migrated (0.0246s) =============================
# セッション管理用テーブルを作成している
== 20241116053431 CreateSessions: migrating ===================================
-- create_table(:sessions)
-> 0.0088s
== 20241116053431 CreateSessions: migrated (0.0089s) ==========================
rails8_test % bin/rails g controller home index
create app/controllers/home_controller.rb
route get "home/index"
invoke erb
create app/views/home
create app/views/home/index.html.erb
invoke helper
create app/helpers/home_helper.rb
rails8_test % bin/rails db:seed
作成されたテーブル
create_table "sessions", force: :cascade do |t|
t.bigint "user_id", null: false
t.string "ip_address"
t.string "user_agent"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_sessions_on_user_id"
end
create_table "users", force: :cascade do |t|
t.string "email_address", null: false
t.string "password_digest", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "confirmation_status", default: 1, null: false
t.string "confirmation_token", limit: 64
t.datetime "expiration_date"
t.index ["email_address"], name: "index_users_on_email_address", unique: true
end
とりあえず下記のファイルが重要そうかなと思いました
module Authentication
extend ActiveSupport::Concern
included do
before_action :require_authentication
helper_method :authenticated?
end
class_methods do
def allow_unauthenticated_access(**options)
skip_before_action :require_authentication, **options
end
end
private
def authenticated?
resume_session
end
def require_authentication
resume_session || request_authentication
end
def resume_session
Current.session ||= find_session_by_cookie
end
def find_session_by_cookie
Session.find_by(id: cookies.signed[:session_id])
end
def request_authentication
session[:return_to_after_authenticating] = request.url
redirect_to new_session_path
end
def after_authentication_url
session.delete(:return_to_after_authenticating) || root_url
end
def start_new_session_for(user)
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
Current.session = session
cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
end
end
def terminate_session
Current.session.destroy
cookies.delete(:session_id)
end
end
ポイント
パスワー ドは平文ではなく、ハッシュ化して保存するために password_digest というカラムを追 加します。xxx_digest というカラム名にしておくことで、この後に追加する has_secu re_password メソッドを使ってパスワードをハッシュ化して保存することができます。
class User < ApplicationRecord
has_secure_password
normalizes :email_address, with: -> e { e.strip.downcase }
end
その他としては・・・
- CurrentAttributes でリクエストごとの情報を管理している。現在ログイン中のユーザー情報を取得は、
Current.user
で取得できる - sessionを作成して、Current.session にセットしている(start_new_session_forの処理・ログインの度にsession情報新規作成)
- 作成したsessionのidをsigned署名付きのcookieに保存している(start_new_session_forの処理)
ログアウト機能
terminate_session
でログアウトしてる
def terminate_session
Current.session.destroy
cookies.delete(:session_id)
end
def destroy
terminate_session # ApplicationControllerでinclude Authenticationされている
redirect_to new_session_path
end
パスワードリセット
パスワードリセットメール用プレビューの処理がデフォであるのはいいなと思った
実際の送信確認はletter_openerとか利用した方がいいのだろうけど、ひとまずメール文面の見栄えとかを確認したい際にはいいかな
# Preview all emails at http://localhost:3000/rails/mailers/passwords_mailer
class PasswordsMailerPreview < ActionMailer::Preview
# Preview this email at http://localhost:3000/rails/mailers/passwords_mailer/reset
def reset
PasswordsMailer.reset(User.take)
end
end
IP アドレスとログイン時刻の記録 (Trackable)
IP アドレスとログイン時刻の記録 (Trackable)は下記のようにsession情報から取得可能
ログイン/ログアウト時にも触れていたがログインするとsession情報が作成されて、ログアウトすると削除される動きだった。
そして、またログインすると新しいレコードができる。
複数sessionなる場合ってどんな時だろ?(ログアウト機能作ってなくて何回もログインした時は複数作られてたけど、基本的にはそんなことはないだろうし。)
[4] pry(main)> user.sessions
Session Load (0.5ms) SELECT "sessions".* FROM "sessions" WHERE "sessions"."user_id" = 1 /* loading for pp */ LIMIT 11 /*application='Rails8Test'*/
=> [#<Session:0x000000011f879d88
id: 4,
user_id: 1,
ip_address: "xxxx",
user_agent: "xxxxx",
created_at: "2024-11-16 07:07:23.701156000 +0000",
updated_at: "2024-11-16 07:07:23.701156000 +0000">]
ソースで言うとこの辺り
def start_new_session_for(user)
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
Current.session = session
cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
end
end
ログイン回数の制限 (Lockable)
認証ジェネレータはレートリミット機能を使ってログイン回数の制限をかけてい ます。デフォルトでは 3 分間に 10 回までログインを試行できます。
とのこと。
この辺りの処理でレートリミットの詳細は下記記事がわかりやすかった
これログインに限らず、カート購入処理の回数制限等々いろんな箇所で使えるのでは?と思った
https://techracho.bpsinc.jp/hachi8833/2024_02_20/139497
class SessionsController < ApplicationController
allow_unauthenticated_access only: %i[ new create ]
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." }
2. 認証ジェネレーターで作成されない機能
まさかのサインアップがないんだなー
サインアップ機能
第7章で紹介してくれてました。
ポイントは認証前(= 未ログイン)でもページを表示できるように、RegistrationsControllerでallow_unauthenticated_access
を設定しているとこ
登録時のメール認証機能
deviseだと:confirmable
を設定すればいいのだけど、今回はデフォルトでは備わっていないので簡易的な認証メール機能を追加してみた。Lintで指摘されているのはスルーでお願いします・・・
- トークンとステータス(メール認証した・してない)、トークン有効期限のカラム追加
class AddConfirmationToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :confirmation_status, :integer, default: 1, null: false
add_column :users, :confirmation_token, :string, limit: 64
add_column :users, :expiration_date, :datetime
end
end
rails8_test % bin/rails g migration add_confirmation_to_users
rails8_test % bin/rails db:migrate
== 20241117071624 AddConfirmationToUsers: migrating ===========================
-- add_column(:users, :confirmation_status, :integer, {:default=>1, :null=>false})
-> 0.0068s
-- add_column(:users, :confirmation_token, :string, {:limit=>64})
-> 0.0013s
-- add_column(:users, :expiration_date, :datetime)
-> 0.0013s
== 20241117071624 AddConfirmationToUsers: migrated (0.0095s) ==================
rails8_test % bin/rails g mailer User
create app/mailers/user_mailer.rb
invoke erb
create app/views/user_mailer
- ユーザー登録時に確認トークンを生成
class User < ApplicationRecord
EMAIL_CONFIRMATION_LIMIT = 5.minutes # 実案件では各案件ルールに従った定数の設定が良い
before_create :generate_confirmation_token
enum :confirmation_status, { confirmed: 0, unconfirmed: 1 }
private
# トークンとトークン期限の設定
def generate_confirmation_token
self.confirmation_token = SecureRandom.urlsafe_base64(47)
self.expiration_date = Time.zone.now + EMAIL_CONFIRMATION_LIMIT
end
- 確認メールを送信
def create
@user = User.new(params.permit(:email_address, :password))
if @user.save
UserMailer.send_email_confirmation(@user).deliver_later
redirect_to root_path, notice: '登録したメールアドレスに確認メールを送信しました。'
else
flash.now[:alert] = "Failed to sign up"
render :new
end
end
- トークン付きのURLでメール認証
# メール認証用アクション
def confirm_email
@user = User.find_by(confirmation_token: params[:token])
# 未確認状態かつトークン有効期間内
if @user.unconfirmed? && !@user.expired?
@user.confirm!
redirect_to root_path, notice: "メールアドレスの確認が完了しました"
else
redirect_to root_path, alert: "無効なトークンです"
end
end
以下のリンクをクリックしてメールアドレスの認証を行ってください。
<%= link_to "メールアドレスを確認する", confirm_email_registration_url(token: @user.confirmation_token) %>
他にもデフォルトで備わっていない機能はあるが今回はこの辺で。
まとめ
DHH氏が
プログラマは認証についてなにが起きているのかを知らなければならないとして、Rails 8では認証機能をコードの生成によって提供する
と言っていたとのことなので、今までDeviseでブラックボックスになっていた部分がシンプルなコードで確認できるようになったのはいいなと思いました。
ただ、機能の豊富さはDevise等gemの方がまだ多いので要件によっては従来のgemで対応していくこともまだまだあるかなと思いました。
その他 機能
今回は認証機能のみに絞って触ってみましたが他にも色々と機能追加がされたようなので、また後日確認できたらなーと
- Kamal2
- Thurster
- SprocketsがPropshaftに置き換わった
- Solidツール群:RedisとかSidekiqが不要になるのかな?
あとEC案件を多く扱ってきた身としては次のRails8.1のActive Record Searchは気になります。
https://www.publickey1.jp/blog/24/dhhrails_8redissqliterails_world_2024.html
デフォルトでGitHub CIの設定が入っている
これはGitHubにpushしたときにたまたま気づいた
name: CI
on:
pull_request:
push:
branches: [ main ]
jobs:
scan_ruby:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- name: Scan for common Rails security vulnerabilities using static analysis
run: bin/brakeman --no-pager
scan_js:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- name: Scan for security vulnerabilities in JavaScript dependencies
run: bin/importmap audit
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- name: Lint code for consistent style
run: bin/rubocop -f github
不要な人は--skip-ci
すれば作成されない
rails8_test % rails new --help
-T, [--skip-test] # Skip test files
[--skip-system-test] # Skip system test files
[--skip-bootsnap] # Skip bootsnap gem
[--skip-dev-gems] # Skip development gems (e.g., web-console)
[--skip-thruster] # Skip Thruster setup
[--skip-rubocop] # Skip RuboCop setup
[--skip-brakeman] # Skip brakeman setup
[--skip-ci] # Skip GitHub CI files
余談
当初turboが効いておらず<%= link_to "Logout", session_path, data: { turbo_method: :delete } %>
でsession#showに飛んでしまいエラーとなっていた
簡易的なテストシステムなのでimportmapを利用
https://www.airteams.net/media/articles/2394
必要なgem
gem "turbo-rails"
gem "importmap-rails"
必要なファイル構成
app/
javascript/
application.js # Turboのインポート
config/
importmap.rb # JavaScriptのマッピング
vendor/
javascript/ # Turboのライブラリファイル
application.html.erbで読み込み
<%= javascript_importmap_tags %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
</head>
app/javascript/application.js でimport
import "@hotwired/turbo-rails"
rails8_test % bin/rails importmap:install
apply /Users/sagawaai/Jobs/rails8_test/vendor/bundle/ruby/3.3.0/gems/importmap-rails-2.0.3/lib/install/install.rb
Add Importmap include tags in application layout
unchanged app/views/layouts/application.html.erb
Create application.js module as entrypoint
create app/javascript/application.js
Use vendor/javascript for downloaded pins
create vendor/javascript
create vendor/javascript/.keep
Configure importmap paths in config/importmap.rb
create config/importmap.rb
Copying binstub
create bin/importmap
run bundle install --quiet
他参考
https://zenn.dev/webuilder240/scraps/1038b4d469ed7d
https://techracho.bpsinc.jp/hachi8833/2024_10_21/145343