セッション / クッキー

認証機能とは?

認証機能とは、ユーザの情報を検証して、システムへのログインやユーザの有効性確認を行うための機能です。

そしてこの認証機能の中でも、特に使用頻度が高いのが、ログイン機能でしょう。 では、ユーザーがログインしている / していないを、システム的にはどのように判定しているのでしょうか。 これには、セッションと呼ばれる仕組みを利用しています。

セッションとは?

セッションとは、Webサイトにアクセスして行う一連の行動のことを指し、ページを跨いで情報を保持したい場合に利用される機能です。

webページ閲覧時に用いられるプロトコルHTTPプロトコルと呼ぶのですが、このHTTPプロトコルは、以前の通信を記憶しておく術を持ち合わせていません。(これを、ステートレスプロトコル と呼んだりします。) つまり、ログイン後、次のページを見たいという通信を行う際には、このHTTPプロトコルは、ユーザーがログインしていることを忘れてしまっているのです。

そこで、セッションの出番です。 セッションは、ページを遷移しても変わらず情報を保持することができるため、このセッションにログイン中のユーザーの情報を格納しておくことによって、ページを遷移しても、変わらずログイン状態を維持し続けることができるのです。

プロトコルとは、コンピューター同士が通信をする際の手順や規約などの約束事のことを指します。

webアプリケーションでは、アプリケーションサーバー側にセッションという仕組みを用意しており、1つのブラウザから連続して送られる一連のリクエスト間で状態を共有できます。セッションの情報はRails標準では、ブラウザ側のクッキーに保存されます。また、sessionは明示的に削除する(又は有効期限切れになる)まで消えません。

Railsアプリケーションにはユーザーごとにセッションが設定されます。前のリクエストの情報を次のリクエストでも利用するためにセッションに少量のデータが保存されます。あらゆるセッションは、暗号化されたセッション固有のID(セッションID)をcookieに保存します。Railsでセキュリティ上の理由からセッションIDをURLで渡すことは許可されていません。そのため、セッションIDは必ずcookieで渡すようにしているのです。

Action Controller の概要 - Railsガイド

cookieとは?

ブラウザとwebサーバー間でやりとりされる時に使われる仕組みです。cookieでは、webサーバーからブラウザへHTTPレスポンスを返す際に、何らかのcookie情報を含めて送ります。cookie情報はキーと値(value)のペアの集合です。ブラウザはこのcookieの情報をサーバーのドメインなどの情報に紐付けて保存し、次回以降、同じドメインのサーバーへのHTTPリクエスト時に、ブラウザで保管しておいたcookie情報をリクエストに添えて送ります。 そうすることにより、webサーバーはどのブラウザからのリクエストかを識別することが出来るのです。つまり、cookieは複数のリクエストの間で共有したい状態をブラウザ側に保存する仕組みです。

  • クッキーは平文

  • セッションIDは暗号化されたもの

f:id:Trial_and_error:20211016124525p:plain

基礎知識 Cookieの仕組み

Railsの場合、config/initializer/session_store.rbにて、セッション情報をどこに保存するのかが指定されています。デフォルトでは、

# セッション情報をどこに保存するのかを設定
# _○○_sessionというkeyで、cookieを用いてセッション情報を管理する
Rails.application.config.session_store :cookie_store, key: '_○○_session'

という設定がなされています。

cookieとは、webブラウザが保持する情報のことです。webでは毎回ページを読み込む度にブラウザとサーバーとの間で通信がなされており、このcookieもリクエスト、レスポンスの度に毎回やりとりがなされます。

つまり、セッション情報の保存先をcookieにするということは、

  1. ログイン時に、ユニークなセッション情報を発行
  2. それをサーバ : ブラウザ間の通信の度に、cookie内でやりとり
  3. cookieを受け取ったサーバは、cookie内のセッション情報を元に今リクエストを送ってきているユーザがログイン中か否かを判定

という流れを経ているのです。 但し、cookieに全てのセッション情報が載ってしまうということは、比較的簡単にセッション情報を確認できてしまうことも意味します。

Railsのセッション管理方法について

Rails セキュリティガイド - Railsガイド

Rails4系からはcookieに載るセッション情報が暗号化されたとは言え、秘密鍵を知っていれば誰でもログイン情報を復元できてしまう恐れがあるのです。ですので、よりセキュリティを強固にしたい場合、このセッションの管理先をredisなどのKVSデータベースに指定したりなどの方法を取る場合もあります。

今回はsorceryは上記で説明した、ログインに必要なことをほぼ自動的にやってくれます。

↓sessionの保存先をcookieからKVSに変更する理由

セッションを保存するとき、なぜ、Cookieではなくmemcachedやredisを使用するのでしょうか?

※KVSとは、キーバリューストア(Kye-Value Store)の略で、保存項目がkeyとvalueのみのDBのことを指します。

参照

Action Controller の概要 - Railsガイド

基礎知識 Cookieの仕組み

Rails セキュリティガイド - Railsガイド

セッションを保存するとき、なぜ、Cookieではなくmemcachedやredisを使用するのでしょうか?

sorceryって何??sorceryを使った、ユーザー登録、ログイン&ログアウト機能の実装からsorceryとはなんぞやを調べてみた。

sorceryとは?

sorceryとは、ユーザ認証機能を簡単に実装できるライブラリ(gem)です。同じように認証機能を提供してくれているものとしてdeviseなどが挙げられますが、sorceryの方がよりシンプルで、カスタマイズ性に富んでいるという特徴を持ちます。

ユーザの認証の基本的な機能であるパスワード認証を始め、

  • User Activation
  • Reset Password
  • Remember Me
  • Session Timeout
  • Brute Force Protection
  • Basic HTTP Authentication
  • Activity Logging

といった機能が揃っていて、必要な機能を選んで使うことができます。

間違えやすい点としては、Sorceryはログイン認証機能を実装する為のGemであり、ログイン認証機能自体はSorceryが提供するメソッドによって実現されます。 まとめると、SorceryというGemをRailsにインストールすると、Sorceryが提供する様々なログインに関するメソッドを使えるようになるということです。

[引用:【Rails】駆け出しVimmerがSorceryをざっくり説明してみた]

Sorceryをインストールして、ユーザー登録機能を実装する

RailsアプリケーションのGemfileにsorceryを追加します。

gem 'sorcery'

コマンドラインで、bundle installしコマンドを実行し、Gemfileに記載したSorceryGemをインストールします。

$ bundle install

3 **Sorceryによって追加されたジェネレーターを実行して、Sorcery Gemを使ったアプリの構築を開始します。

まず、Sorceryが提供するジェネレータbundle exec rails g sorcery:installを実行し、Userモデルとmigrationファイルを生成します。**

$ bundle exec rails g sorcery:install

bundle execって必要なの? - Qiita

上記のコマンド実行後、次のファイルが作成されます。↓

  • app/models/user.rb(userモデル)
  • config/initializers/sorcery.rb(initializersファイル)
  • db/migrate/yyyymmddhhmmss_sorcery_core.rb(migrationファイル)

4 rake rails db:migrateを実行して、usersテーブルを生成します。

$ rake db:migrate

5 以下のコマンドをターミナル上で実行して、Userモデルに対応したビューとコントローラを作成します。

同時に、userテーブルにstring型のemailカラムとstring型のcrypted_password(暗号化されたパスワード)カラムとstring型の※saltカラムを作成します。

$ rails g scaffold_controller user email:string crypted_password:string salt:string

仮想属性とは? 例えば今回のように、databaseのusersテーブルがあるとします。ユーザー情報を登録するusersテーブルにはpasswordというカラムはなく、password〇〇や、〇〇password といった暗号化されたパスワードを入れるようになっています。今回はcrypted_passwordカラムに暗号化されたパスワードが入るようになっています。 そして、crypted_passwordカラムにパスワードを暗号化して入れるためにpassword属性とpassword_confirmation属性という仮想属性を作っているのです。仮想属性にパスワードを入れてsaveすると、暗号化されたものが実際にcrypted_passwordカラムに入ります。 つまり、DBのテーブルにパスワードを保存するために仮想属性が必要になるということです。 実際には、DBに存在しない属性(カラム)であることから、仮想属性と呼ばれます。 params.permit(:password) するときに、password= というsetter methodがないとエラーになりますので atter_accessorを使って、getter、setterの仮属性定義が必要です。


password_confirmation(パスワードコンファーメイション)とは、データベースに保存されない仮想の属性で、パスワードの入力確認に使われます。

この属性を使用するとpasswordpassword_confirmationの双方が一致しているかのバリデーションが自動で追加されます。


現在パスワードの保管方法は、単にパスワードをハッシュ化して、暗号化したものを保管するのではなく、元のパスワードの文字列に別の文字列(ソルト)を付け加えることで文字列全体を長くしてから、繋げた文字列をまとめてハッシュ化したものをDBに格納しています。そうすることで、よりセキュリティを高めることができるのです。DBにはソルトとハッシュ値を保存しています。ハッシュ化(パスワード文字列+ソルト)=ハッシュ値

6 暗号化されたパスワードやソルトをユーザーに編集/閲覧させることはセキュリティ上、望ましくないため、ビューでこれらの属性を表示させないようにします。

ビューであるapp / views / users /のすべてのテンプレートからこれらの属性を削除します。

7 UsersControllerでStrong parameterを定義してフォームの特定の属性の値を取得できるように設定します。

class UsersController < ApplicationController
  # ...
  private

  def user_params
        # paramsオプジェクトとして受け取れる値は、userテーブルのemail属性、password属性、password_confirmation属性の値のみと設定。
        # ここで、password属性、password_confirmation属性の2つの属性は仮想属性であり、実際にはDBに存在しない属性です。passwordとpassword_confirmationの双方の値が一致しているか確認した後、暗号化されたものがDBに保存されます。
    params.require(:user).permit(:email, :password, :password_confirmation)
  end
end

8 暗号化されたパスワードがDBに保存される前に、ユーザーが入力したパスワードをフォームで受け取るために仮想フィールドをビュー(今回はビューテンプレート)に追加する必要があります。

# app/views/users/_form.html.erb
<div class="field">
   <%= form.label :password %><br />
   <%= form.password_field :password %>
</div>
<div class="field">
   <%= form.label :password_confirmation %><br />
   <%= form.password_field :password_confirmation %>
</div>

9 Userモデルに以下の記載をします。validates :password, confirmation: trueをUserモデルに設定することで、Userモデルにpassword属性とpassword_confirmation属性が定義されます。

(この2つはuserテーブルのcrypted_passwordカラムに紐づくUserモデルの仮想属性です。) この2つの仮想属性は暗号化されてから、userテーブルでcrypted_passwordカラムに変更されるようになっています。

# app/models/user.rb
class User < ActiveRecord::Base
  authenticates_with_sorcery!

  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }

  validates :email, uniqueness: true
end

10 最後に、ルーティングを定義します。

# config/routes.rb
root :to => 'users#index'
resources :users

アプリを実行し、http://localhost:3000/usersにアクセスすると、ユーザー新規登録ページに遷移し、そこで新しいユーザーを作成できます。Soceryを使用しているため、パスワードは自動的に暗号化され、ソルトも自動作成されます。

Sorceryを使って、ログイン・ログアウト機能を実装する

1 . ユーザーのログイン状態を維持し続けるために、以下のコマンドをターミナル上で実行します。このコマンドを実行することで、ユーザーのログインとログアウトに対応したコントローラとビューが作成されます。ログインとログアウト機能で、セッションを使用するので、コントローラー名をUserSessionsと設定しています。

$ rails g controller UserSessions new create destroy

2 . 上記で作成したコントローラーにログインとログアウト処理をメソッドとして記載します。

# app/controllers/user_sessions_controller.rb
class UserSessionsController < ApplicationController
    # ログイン処理
  def create
        # loginメソッドを使って、paramsで取得したemailによるUser検索、パスワードの検証を行い、正常に処理できるとセッションデータにUserレコードのid値を@userに格納する
    @user = login(params[:email], params[:password])

    if @user # @userがnullではない時、ログインできている時
            # ログインが成功すると、ログイン成功のフラッシュメッセージを表示するとともに、ユーザーが要求したページにリダイレクトする。
      redirect_back_or_to(:users, notice: 'Login successful')
    else # @userがnullの時、ログインできていない時
            # ログインに失敗すると、ログイン失敗フラッシュメッセージを表示して、新規登録ページに遷移する。
      flash.now[:alert] = 'Login failed'
      render action: 'new'
    end
  end

    # ログアウト処理
  def destroy
        # logoutメソッドでセッションをリセットする
    logout
    redirect_to(:users, notice: 'Logged out!')
  end
end

loginメソッドで、認証処理が行われる。 @user = login(params[:email], params[:password])でemailによるUser検索、パスワードの検証を行い、正常に処理できるとセッションデータにUserレコードのid値を格納する、という処理が行われている。


logoutメソッドで、セッションをリセットする。


redirect_back_or_toメソッド・・・保存されたURLがある場合そのURLに、ない場合は指定されたURLにリダイレクトします。ユーザーがリクエストしたURLがあるときはそのURLにリクエストを、リクエストURLがない場合は、指定されたURLにリダイレクトします。
例えば、掲示板ページにアクセスしようとしたユーザにログインを要求する場合、require_loginメソッドでユーザをログインページに誘導し、ログインが成功したら、最初に訪れようとしていた掲示板ページにリダイレクトさせるということが可能になる。

3 . ログインフォームを作成する

# app/views/user_sessions/new.html.erb
<h1>Login</h1>

<%= render 'form' %>

<%= link_to 'Back', users_path %>
# app/views/user_sessions/_form.html.erb
<%= form_with url: login_path, method: :post do |f| %>
  <div class="field">
    <%= f.label :email %><br />
    <%= f.text_field :email %>
  </div>
  <div class="field">
    <%= f.label :password %><br />
    <%= f.password_field :password %>
  </div>
  <div class="actions">
    <%= f.submit "Login" %>
  </div>
<% end %>

4 . ルーティングを定義する

# config/routes.rb
root :to => 'users#index'
resources :users

get 'login' => 'user_sessions#new', :as => :login
post 'login' => "user_sessions#create"
post 'logout' => 'user_sessions#destroy', :as => :logout

5. 現在は、ログアウトしているユーザーでもログインしているユーザーと同じような操作が出来てしまうので、操作の前にログイン認証を行うようにします。ただし、ログアウト時でもできる操作に関しては、Skip処理をすることで、ログイン認証を行わず、操作ができるように設定します。

# app/controllers/application_controller.rb
# 全てのアクション実行前にログイン認証を必須とする。
before_action :require_login 
# app/controllers/users_controller.rb
# index, new, createアクション実行時のみ、ログイン認証処理を必須としない。
skip_before_action :require_login, only: [:index, :new, :create]
# app/controllers/user_sessions_controller.rb
# new, createアクション実行時のみ、ログイン認証を必須としない
skip_before_action :require_login, only: [:new, :create]

6 . ログインしていないユーザーがログインユーザー専用のページにアクセスするのを防ぐため、以下のコードをapplication_controller.rbに記載します。そうすることで、もしログインしていないユーザーがログインユーザー専用のページにアクセスした際、ログインページに遷移します。

# app/controllers/application_controller.rb
private
def not_authenticated
  redirect_to login_path, alert: "Please login first"
end

sorceryを使うと使用できるようになるメソッド

require_login

ログインをしていないユーザーをアクション単位で弾く。アクセスしようとしたURLをセッションに格納し、not_authenticatedを実行するメソッド。以下のようにbefore_actionで指定する。アクションごとに変える場合は、only: :actionを付ける。アクション内の分岐など、もっと細かい単位で弾きたい場合は後述のlogged_in?を使う。

sorceryのモジュール内に定義されているメソッドで、sorcery gemをインストールすることによって、使えるメソッドになる。ApplicationController内に定義することで、全てのアクション実行前に使えるようになる。

before_action :require_login

before_actionの使い方とオプションについて

not_authenticated

先ほどのrequire_login内で、このメソッドも実行される。デフォルトではredirect_to root_path(自動的にルートに飛ばされる)と定義されているが、カスタマイズしたい場合はapplication_controllerで上書きをする。

class ApplicationController < ActionController::Base
  protected

  def not_authenticated
    redirect_to login_url, alert: 'ログインしてください'
  end
end

current_user

現在ログイン中のuserを返す。コントローラ、ビューで使える。

logged_in?

現在ログイン中かどうか、true or falseで返す。コントローラ、ビューで使える。ログインしているかどうかによって場合分けをしたいときに使うことが多い。

今回は、ヘッダーの表示をログインしているか、ログインしていないかで場合分けするために、logged_in?メソッドを使い、実装しました。

<% if logged_in? %>
  <%= link_to 'プロフィール', user_url(current_user) %>
<% else %>
  <%= link_to 'ログイン', login_url %>
<% end %>

sorceryを使わないで、ログインの有無による、表示分けを行う時は、以下を参照

【Rails】ログイン機能を実装する - Qiita

【Sorcery】Sorceryで使えるようになるメソッドとその活用例 - Qiita

第8章 ログイン、ログアウト - Railsチュートリアル

参考

Simple Password Authentication · Sorcery/sorcery Wiki

GitHub - Sorcery/sorcery: Magical Authentication

GitHub - heartcombo/devise: Flexible authentication solution for Rails with Warden.

Rails+Sorceryで認証処理を実装する

【Rails】駆け出しVimmerがSorceryをざっくり説明してみた

第16回 データベースからパスワードが盗まれても問題がなくなる方法

gem sorceryを使ってログイン機能を実装した話 - Qiita

【Rails】ログイン機能を実装 - Qiita

仮想属性を作成する理由が理解できません|teratail

【Sorcery】Sorceryで使えるようになるメソッドとその活用例 - Qiita

CSV形式のファイルのインポート/エクスポートについて

CSV形式とは?

1単語ずつカンマで区切られていて、1行ずつ情報が並んでいます。また、テキストデータとは文字のみのデータのことを指し、ワードやエクセルのように画像が入っていたり、文字サイズや色の装飾などがされていないプレーンな文字データのことを言います。通常のテキストデータの拡張子は「.txt」ですがCSVの場合は「.csv」となります。

# CSV形式の例
氏名,年代,性別,住所
山田 花子,30代,女性,東京都豊島区
田中 太郎,50代,男性,和歌山県有田郡

CSVは各ソフトウェアが共通で対応している形式のため、異なるソフトウェア間で簡単にデータの移行が可能になっています。

library csv

csv形式のデータを扱うには、csvライブラリが必要です。

そのため、ライブラリを取り込んで、使えるように設定が必要です。

require 'csv'
~省略~

読み込み

# CSVライブラリを読み込む
require "csv"

csv_text = <<~CSV_TEXT
  Ruby,1995
  Rust,2010
CSV_TEXT

IO.write "sample.csv", csv_text

# ファイルから一行ずつ
CSV.foreach("sample.csv") do |row|
  p row
end
# => ["Ruby", "1995"]
#    ["Rust", "2010"]

# ファイルから一度に
p CSV.read("sample.csv")
# => [["Ruby", "1995"], ["Rust", "2010"]]

# 文字列から一行ずつ
CSV.parse(csv_text) do |row|
  p row
end
# => ["Ruby", "1995"]
#    ["Rust", "2010"]

# 文字列から一度に
p CSV.parse(csv_text)
# => [["Ruby", "1995"], ["Rust", "2010"]]

書き込み

# CSVライブラリを読み込む
require 'csv'

# ファイルへ書き込み
CSV.open("path/to/file.csv", "wb") do |csv|
  csv << ["row", "of", "CSV", "data"]
  csv << ["another", "row"]
  # ...
end

# 文字列へ書き込み
csv_string = CSV.generate do |csv|
  csv << ["row", "of", "CSV", "data"]
  csv << ["another", "row"]
  # ...
end

1行変換

# CSVライブラリを読み込む
require 'csv'

csv_string = ["CSV", "data"].to_csv   # => "CSV,data"
csv_array  = "CSV,String".parse_csv   # => ["CSV", "String"]

現場Rails p312[csv出力する(エクスポート)]

app/models/task.rb

#タスクデータをCSVファイルとして出力する方法
class Task < ApplicationRecord

 # どの属性をどの順番で出力するかを定義してcsv_attributesメソッドで要素に属性名の文字列を持つ配列を作成します。
  def.csv_attributes
    ["name", "description", "created_at", "updated_at"]
  end

  # CSV形式でインスタンスの中身を出力できるようにします。
  def self.generate_csv
        # CSV.geterateでCSVデータの文字列を生成する
    CSV.generate(headers: true) do |csv|

      # headers: trueはデータベースの一番上の行(ヘッダー)をCSVのレコードのタイトルをとして指定します。
      # csvに上記で定義したcsv_attributesを一行目として代入します。
      csv << csv_attributes
      all.each do |task| # 省略前は、Task.all.each do |task|
        # Task.allでタスクを全件取得。その後、eachでtaskを一つ一つ取り出してtaskの値を代入していきます。
        csv << csv_attributes.map{ |attr| task.send(attr) }
      # mapはcsv_attributesの配列の中身全てに対して処理を行います。
            # この時、|attr|には["name", "description", "created_at", "updated_at"]が順番に入って処理に渡されます。
      # task.sendは引数に指定したメソッドを文字列で返します。
      # 文字列として返った属性名の中身をCSVに追加します。
      end

    end

  end

private
~省略~

end
  • <<は配列の末尾に要素を追加するメソッド

Array#<< (Ruby 3.0.0 リファレンスマニュアル)

  • map() メソッドは、与えられた関数を配列のすべての要素に対して呼び出し、指定した処理の結果を要素にした新しい配列を生成します。
# 配列の要素をすべて 3 倍にする→ その結果を新しい配列として生成する。
# 配列pの要素を順番に変数|n|に取り出してきて、そのnを使って、{ブロック内の後ろの処理を行う}。
# n * 3をした結果を新たな要素として配列を作成する。
p [1, 2, 3].map {|n| n * 3 }  # => [3, 6, 9]

Array#collect (Ruby 3.0.0 リファレンスマニュアル)

  • sendメソッドは、引数として渡されたメソッド(文字列、シンボル、変数として渡された)を実行し、その実行結果を返します。つまり、レシーバの持っているメソッドを呼び出してくれるメソッドです。 ブロック付きで呼ばれたときはブロックもそのまま引き渡します。
class User
  def name
    puts "taro"
  end
end

user = User.new
#定義したメソッドを呼び出す
user.name  # => taro

# sendを使った書き方
user.send(:name) # => taro
user.send("name")  # => taro

Object#send (Ruby 3.0.0 リファレンスマニュアル)

  • CSV.generateメソッドは、与えられた文字列をラップして CSV のオブジェクトとしてブロックに渡します。ブロック内で CSV オブジェクトに行を追加することができます。ブロックを評価した結果は文字列を返します。
generate(str = "CSVに変換したい文字列", options = Hash.new) {|csv| ... } -> String

CSV.generate (Ruby 3.0.0 リファレンスマニュアル)

app/controller/tasks_controller.rb

# コントローラに異なるフォーマットでの出力機能を追加する
def index
  ~ 略 ~
  respond_to do |format|
      # formatがHTMLとしてアクセスされた場合
    format.html 
        # formatがCSVでアクセスされた場合
    # send_dataメソッドを使って、レスポンスを送り出します。
        # モデルで定義したgererate_csvメソッドを使って、タスクデータをCSV形式にしたものをレスポンスとしています。
    format.csv { send_data @tasks.generate_csv, filename: "sample.csv"}
  end
end

参照

CSVファイルとは何?何のために使うの?

library csv

【Ruby】よく使う、CSVライブラリを使ったCSV操作 - Qiita

Array#<< (Ruby 3.0.0 リファレンスマニュアル)

Array#collect (Ruby 3.0.0 リファレンスマニュアル)

Object#send (Ruby 3.0.0 リファレンスマニュアル)

【Ruby on Rails】sendメソッドのいろんな書き方 - Qiita

Array.prototype.map() - JavaScript | MDN

CSV.generate (Ruby 3.0.0 リファレンスマニュアル)

ActiveStorageとは何か?

ActiveStorageとは?

Active Storageとは、ファイルアップロードを簡単に実装できるgemです。railsの標準のgemで、rails5.2から追加されました。

以前は、CarrierWaveというgemが使われていましたが、Rails5.2からはActiveStorageが使われることが多くなりました。

ユーザー側から、画像などのファイルをサーバーにアップロードして、サーバーにおいてあるファイルをモデルが取得するためにActiveStorageが使われます。また、サーバーではなく、Amazon S3, Google Cloud Storage, Microsoft Azure Storageなどのクラウドストレージサービスに対してファイルをアップロードすることもできます。

例)

SNSを使って、写真や画像を投稿する時、(=画像ファイルをアップロードする時)ActiveStorageを使って、画像ファイルを保存している。保存した場所(サーバーやクラウドストレージサービス)から、取り出してきて、見えるように表示している。

ActiveStorageの導入

#gemのインストールはRailsインストール時に行われているので、不要。
$ rails active_storage:install
#=>コマンド実行時に、active_storage_blobs とactive_storage_attachmentsという2つのテーブルを作成するマイグレーションファイルが生成されます。

#migrationファイルをDBに反映させる。
$ rails db:migrate

active_storage_blobsは実際にアップロードしたファイルが保存されるテーブル(実際はファイル名、ファイルの種類、バイト数、誤り検出符号などのファイルデータを保持するテーブル)で、active_storage_attachmentsは実際に操作を行うモデルとactive_storage_blobsを紐づける中間テーブルになります。

ファイルの保存場所を管理する設定の仕方

設定はRails .application .config .active _storage.serviceにファイルを保存する場所を名前で指定し、そのファイルの保存場所の名前に対応する設定をconfig/storage.ymlに定義することで行います。

#ファイルを保存する場所をlocalに設定
#デフォルトではdevelopment環境のファイルの管理場所はlocalとなっている
config.active_storage.service = :local
test: #testにおいてあるファイルの設定方法。ここで、testを定義している。
  service: Disk #testの時は使用するサービスをローカルディスクに設定
  root: <%= Rails.root.join("tmp/storage") %> #ファイルを格納する場所

local: #localにおいてあるファイルの設定方法(デフォルト)。ここで、localを定義している。
  service: Disk #localの時は使用するサービスをローカルディスクに設定
  root: <%= Rails.root.join("storage") %> #ファイルを格納する場所

# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
#   service: S3
#   access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
#   secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
#   region: us-east-1
#   bucket: your_own_bucket

# Remember not to checkin your GCS keyfile to a repository
# google:
#   service: GCS
#   project: your_project
#   credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
#   bucket: your_own_bucket

# Use rails credentials:edit to set the Azure Storage secret(as azure_storage:storage_access_key)
# microsoft:
#   service: AzureStorage
#   storage_account_name: your_account_name
#   storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
#   container: your_container_name

# mirror:
#   service: Mirror
#   primary: local
#   mirrors: [ amazon, google, microsoft ]

ここのファイルに書いてある情報(ルート、サービスなど)をもとに、モデルが実際のファイルを取得しに行きます。 Active Storageではローカルの他にも様々なサービスを指定することができます。 コメントアウトされている部分をみるとAWSのS3googleのGCSmicrosoftのAzureStorageも使うことができます。使う場合はこちらのコメントアウトを外し、Gemfileに対応したgemを追加する必要があります。

# Active Storageで指定できるservice
AWS    aws-sdk-s3 # 使用できるgem
google  google-cloud-storage # 使用できるgem
microsoft   azure-storage # 使用できるgem

モデルと添付ファイルを紐づける(1つの添付ファイルの場合)

class Task < ApplicationRecord
    # has_one_attachedを使って、Taskモデルに1つの画像を添付する
  has_one_attached :image #imageはファイルの呼び名のこと
end

:imageはファイルの呼び名で、:photo:avatar:hogeなど、ファイルの用途に合わせて好きなものを指定できます。以上のように指定することで、active storageが自動でBlobとAttachmentの2つのテーブルを使って、モデルからimageファイル(カラム)を取得できるようにしてくれます。

ここで、指定しているのは画像ファイル(:image)ですが、モデルに指定することで、まるでモデルのカラムであるかのように扱うことが出来ます。

つまり、imageというカラムで画像ファイルimageを保存しているイメージです。

モデルと添付ファイルを紐づける(複数の添付ファイルの場合)

# 複数の画像ファイルを一つのタスクに紐づける場合,has_one_attachedの代わりにhas_many_attachedを使う
class Task < ApplicationRecord
  has_many_attached :images
end

has_one_attachedとhas_many_attached

  • **has_one_attached** = レコードとファイルの間に1対1のマッピングを設定します。各レコードには1つのファイルを添付できます。

例)1つのメッセージに1つの画像ファイルを紐づける時に使用します。 - **has_many_attached** = レコードとファイルの間に1対多の関係を設定します。各レコードには、多数の添付ファイルをアタッチできます。

例)1つのメッセージごとに複数の画像ファイルを紐づける時に使用します。

保存したファイルを表示させるために、ビューを編集する

# カラムとして扱っている画像ファイルを表示する
<%= image_tag user.image %>

#画像の大きさを調節するためにvariantメソッドを使用する
# 画像の幅を統一したい場合はvariantメソッドを使用
# 200x200のサイズで画像が表示
<%= image_tag user.image.variant(resize:'200x200') %>

画像を削除する

# 画像を削除するにはpurgeメソッドを使う
user = User.find(params[:id])
user.image.purge

画像の大きさを調節する

デフォルトでは、画像は元のファイルのサイズのままであり、サイズを変更したい場合は、設定が必要になる。

# ImageMagickという画像変換ツールをインストール
$ brew install imagemagick
# ImageMagickをrailsで使えるようにするためmini_magickというgemをインストール
gem 'mini_magick'
#この後、bundle installコマンドをうつ
# 画像の幅を統一したい場合はvariantメソッドを使用
# 200x200のサイズで画像が表示
<%= image_tag user.image.variant(resize:'200x200') %>

ビューなどに画像ファイルを表示する記述を書いたとしても、StrongParametersを設定していた場合、アップロードしたファイルの情報を無視せず表示するために、新たに許可するパラメータのキーを設定し直す必要があります。

# 許可するパラメータのキーとして:imageを追加
def task params
    params.require(:task).permit(:name, description, :image)
end

ActiveStorageのメソッド

attachメソッド

レシーバーのオブジェクトに対して、指定のファイルを直接紐付けるメソッドです。

# 既存のメッセージに新しい画像を追加
@message.images.attach(params[:images])

attached?メソッド

レシーバのオブジェクトに指定のファイルが紐付いていればtrueを紐付いていなければ、falseを戻り値として返すメソッドです。

# あるメッセージに何らかの画像が添付されているかどうかを調べる
@message.images.attached?

variantメソッド

画像を変換する際に使われるメソッドです。このメソッドを使うためには、image_processing gemGemfileに追加する必要があります。

# ブラウザがバリアントURLにヒットすると、Active Storageは元のblobを指定のフォーマットに遅延変換し、新しいサービスのロケーションにリダイレクトします。
# 画像ファイルを100 * 100のサイズに変換する
<%= image_tag user.avatar.variant(resize: "100x100") %>

参照

【Rails 5.2】 Active Storageの使い方 - Qiita

CarrierWaveチュートリアル

Active Storageを使って画像をアップしよう!

Active Storage の概要 - Railsガイド