しおブログ言うてますけど

技術ブログ言うてますけど技術以外のことも発信すると思われ。RubyとかRuby on Railsとか生き方とかジャンルにこだわらず自分の好きなように。初心者なのでお手柔らかに。

ログイン機能の実装(digestを保存するまで)①

Rsilsチュートリアルや現場Railsでも解説されているログイン機能の実装は理解が追いつかなく、なにがなんだかわからない状態だったので、現場Railsを参考にログイン機能の実装をアウトプットしていきたいと思う。
まずWEBアプリケーションでログイン機能を実装する理由は、例えばTODOアプリを作ったとして、やることリストを投稿したとする。自分一人なら、なんら問題は無いが、第三者に記事を投稿されたり削除されてはいけない。
そういった問題を解決するためにログイン機能があり、ログインしたユーザーのみが自分自身のタスクしか扱えないようにするのが目的である。(少々雑かったらすまそん...)
ログイン機能の実装はかなり長くて、1つの記事にしようとすると多分自分でも後で読む気にならないと思うので、いくつかの記事にしてアウトプットする。

前提に必要な知識

Cookie

HTTPはステートレスなプロトコルであるため、WEBブラウザとWEBサーバーの一連のやりとりにおいて、状態を保持し管理する仕組みがない。そのためにショッピングサイトなどで状態を保持し管理する必要がある場合には、Cookieと呼ばれるデータが用いられる。いわば複数のリクエストの間で共有したい「状態」をブラウザ側に保存する仕組みである。

セッション

セッションとは一連の関連性のある処理の流れのことである。例えばショッピングサイトで商品を買う場合の「商品を選ぶ」「商品を買い物かごに入れる」「買い物かごの中身を確認する」「商品を購入する」といった流れがセッションになる。WEBブラウザからに処理を関連性のある一連の処理(=セッション)として扱いたい場合はCookieを用いてセッションを管理できる。

Railsではコントローラーからsessionというメソッドを呼び出すことで、セッションにアクセスできる。sessionはハッシュのように扱うことができる。

 session[:user_id] = @user.id

値を取り出すには下記のように参照する。

 @user_id = session[:user_id]

Railsではセッションの仕組みの一部がCookieによって実現されている。なので直接Cookieを操作することはあまりない。

(ユーザーを表すUser)モデルの作成

ここではアプリケーションを利用するユーザーを表すUserモデルを作成する。Userクラスのデータ構造は下記のように設計する。

意味 属性名 テータ型
名前 name string(文字列)
メールアドレス email string(文字列)
パスワード password_digest string(文字列)

パスワードの属性名がpassword_digestとなっているのは意味があり、これはRailsに標準で付いているhas_secure_passwordという機能を使った命名ルールに沿った属性名である。
has_secure_passwordを使うことによってセキュアにハッシュ化したパスワードを、usersテーブル内のpassword_digestという属性に保存できるようになる。 下記のコマンドでUserモデルを作成する。

$ rails g model User name:string email:string password_digest:string

するとマイグレーションファイルが作成される。
rails db:migrateする前にマイグレーションを編集する。

class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.string :email, null: false
      t.string :password_digest, null: false

      t.timestamps
      t.index :email, unique: true
    end
  end
end

名前、メールアドレス、パスワードには必ず文字列が入らないといけないのでNot Null制約をつけている。 編集したらrails db:migrateでデータベースにusersテーブルを作成する。

digestを保存する

上記で説明したhas_secure_passwordを使ってパスワードをセキュアにハッシュ化する。
そのためにbcryptというgemを使う。 Gemfileにコメントアウトされているgem 'bcrypt'のコメントアウトを外してbundle install

gem 'bcrypt', '~> 3.1.7'
$ bundle install

has_secure_passwordの使いかたは簡単でモデルの中にhas_secure_passwordと記述するだけでパスワードをハッシュ化してくれる。

class User < ApplicationRecord
  has_secure_password
end

モデルの中にhas_secure_passwordを記述すると下記2つのデータベースのカラムには対応しない属性が追加される。

  • password
  • password_confirmation

password属性はユーザーが入力したパスワードを一時的に格納するための属性。
password_confirmation属性はpassword属性で入力した値の確認用の属性。
つまりpassword属性の値とpassword_confirmation属性の値が一致してなければ検証に失敗する。
password属性の値とpassword_confirmation属性の値が一致していればpassword_digestにハッシュ化したパスワードの値が保存される。

実際にrails cを使って本当にハッシュ化されるのか検証してみたいと思う。

> user = User.new(name: "テスト", email: "test@test.com", password: "test", password_confirmation: "test")
=> #<User id: nil, name: "テスト", email: "test@test.com", password_digest: "$2a$12$RyxUd9HvQgnA4ur8UXJYpeXRykmYHKKV3ll3nieLMIu...", created_at: nil, updated_at: nil>

password_digestの部分が特定できないハッシュ化されたパスワードになっている。 これをsaveすると、

> user.save
   (0.1ms)  BEGIN
  User Create (0.5ms)  INSERT INTO "users" ("name", "email", "password_digest", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["name", "テスト"], ["email", "test@test.com"], ["password_digest", "$2a$12$RyxUd9HvQgnA4ur8UXJYpeXRykmYHKKV3ll3nieLMIumQw167pkri"], ["created_at", "2021-01-10 07:41:28.956293"], ["updated_at", "2021-01-10 07:41:28.956293"]]
   (4.9ms)  COMMIT
=> true

trueが返ってきて無事に保存されていることがわかる。
ちなみにpassword属性とpassword_confirmation属性の値が一致せずに保存しようとすると、

> test = User.new(name: "テスト", email: "test@test.com", password: "test", password_confirmation: "foobar")
=> #<User id: nil, name: "テスト", email: "test@test.com", password_digest: "$2a$12$U1fzoPdhbC9seU.Ku0kDUeGWSLzxSm5773n/t1KecCy...", created_at: nil, updated_at: nil>
> test.save
   (0.2ms)  BEGIN
   (0.2ms)  ROLLBACK
=> false

falseが返ってきて保存されないということがわかる。

ここまでのまとめ

  • ログイン機能を実装する目的はWEBアプリケーションにて、自分以外のユーザーが他のユーザーの情報を操作できないようにするためである。
  • ユーザー(User)モデルを作成する際、属性には必ず必要なname, email, password_digestとする。
  • password_digestにする理由はRails標準のhas_secure_passwordという機能を使うためである。
  • モデルにhas_secure_passwordと記述すると、passwordpassword_confirmationの2つのデータベースのカラムには対応しない属性が追加される。
  • password属性の値とpassword_confirmation属性の値が一致しているとusersテーブルのpassword_digestカラムにハッシュ化されたパスワードが保存される。
続きます

少し長いので別の記事に続きます。

マイグレーションの様々な機能

まずテーブルを作成する前にカラムを追加できる型の種類を知っておく必要があるのではじめに主に使われるデータ型をまとめてみる。

データ型

データ型 説明
:boolean 真偽値
:integer 符号付き整数
:float 浮動少数点数
:string 文字列(短い文字列に利用する)
:text 文字列(長い文字列に利用する)
:date 日付
:datetime 日時

他にもたくさんあるが主に使われるデータ型をまとめた。

次にマイグレーションの代表的な個々のパターンについてまとめる。

新規テーブルを作成する

一番一般的なやり方だと思うが、モデルを作成することによって自動的にテーブルを作成する。
ここでは例としてFoodモデル(name属性を持つ)を作成してみる。

$ rails g model Food name:string

この結果

class CreateFoods < ActiveRecord::Migration[5.2]
  def change
    create_table :foods do |t|
      t.string :name

      t.timestamps
    end
  end
end

Foodモデルを作成したことによってfoodsテーブルが作成され、カラムとしてname(string文字列)を追加するとを表している。

既存のテーブルにカラムを追加する

上記で作成したFoodモデル(foodsテーブル)に新しいカラムを追加します。
ここでは例としてdescription(string文字列)カラムを追加します。
すでにFoodモデル(foodsテーブル)を作成しているので、マイグレーションファイルだけrails g コマンドで作成する。

$ rails g migration add_description_to_foods description:string

上記のコマンドではrails g コマンドでマイグレーションを作成し、ファイル名は「addカラム名to_テーブル名」とし、そのあと追加するカラムを指定している。

この結果、

class AddDescriptionToFoods < ActiveRecord::Migration[5.2]
  def change
    add_column :foods, :description, :string
  end
end

add_columnメソッドを使用してfoodsテーブルにdescriptionカラムが追加されていることがわかる。

既存のテーブルのカラム属性を変更する

descriptionカラムを追加したが複数行の文字列に対応させたいとします。現在の属性タイプではstringで複数行の文字列にはふさわしくないのでtextタイプに変更しなければいけない。

このような場合に変更するための適切な名前のルールはない。ここではchange_datatype_description_of_foodsといいう名前のマイグレーションファイルを作成する。

$ rails g migration change_datatype_description_of_foods

ここではオプションを設定していないので下記のようなマイグレーションが作成されていることがわかる。

class ChangeDatatypeDescriptionOfFoods < ActiveRecord::Migration[5.2]
  def change
  end
end

このマイグレーションファイルにカラムを変更するための記述を行う。
カラムの変更を行うにはchange_columnメソッドを使用する。 change_columnの後にモデル名、テーブル名、変更する属性とする。

class ChangeDatatypeDescriptionOfFoods < ActiveRecord::Migration[5.2]
  def change
    change_column :foods, :description, :text
  end
end

これでfoodsテーブルのdescription属性タイプをtextに変更することができる。

他にも色々な方法でマイグレーションの設定ができるが基本的なテーブルの作成、カラムの追加、カラム属性の変更ができるようになった。

マイグレーションのメソッド

上記でテーブルの作成、カラムの追加、カラム属性の変更ができるようになったがもっと柔軟にマイグレーションが扱えるようにマイグレーションのテーブルを操作するメソッドやテーブル操作の中で使用できるメソッドをまとめてみる。

マイグレーションのテーブル操作メソッド

メソッド 役割
change テーブルの作成、カラムの追加、削除を行う場合に使用する。最も利用されるメソッド
up rollbackが働くよう、スキーマに対する変更を記述する。up(変更記述) / down(戻し記述)
down rollbackが働くよう、upメソッドによって追加されたスキーマの変更を「元に戻す」方法を記述する。up / downは対になって利用される
reversible changeメソッドだけで簡単にマイグレーションロールバックを判定できないような場合に、up / downを使用して「変更」と「戻し」の処理をreversibleの中に取り込み、どちら向きの変更処理でも常に同じ方向の処理ができるようにする
revert 実行済みのマイグレーションの一部のみをロールバック(元に戻す)場合に使用する。戻したい実行済みのマイグレーション記述をコピーし、revertブロック野中に組み込むことでロールバックを実現する

テーブル操作メソッドの中で使用できるメソッド

メソッド 構文 役割
create_table create_table :テーブル名 [, オプション] テーブルを作成する
drop_table drop_table :テーブル名 [, オプション] テーブルを削除する
rename_table rename_table :現在のテーブル名, :新しいテーブル名 テーブル名を変更する
change_table change_table :テーブル名 [, オプション] テーブル設定を変更する
add_column add_column :テーブル名 :カラム名, :データ型 [, オプション] カラムを追加する
add_reference add_reference :テーブル名, :リファレンス名 [, オプション] 外部キーリファレンスを追加する(親子のアソシエーションで使用可能)
add_timestamps add_timestamps :テーブル名 タイムスタンプ(登録日、更新日を追加する)
rename_column rename_column :テーブル名, :変更するカラム名, :新しいカラム名 カラム名を変更する
change_column change_column :テーブル名, カラム名, :データ型 [, オプション] カラム設定を変更する
remove_column remove_column :テーブル名, :カラム名 [, :データ型, オプション] カラムを除去する
remove_reference remove_reference :テーブル名, リファレンス名 [, オプション] 外部リファレンスキーを除去する
remove_timestamps remove_timestamps :テーブル名 タイムスタンプ(登録日、更新日)を除去する
add_index add_index :テーブル名, :インデックスを付与するカラム名 [, オプション] インデックスを追加する
rename_index rename_index :テーブル名, :旧インデックス名, :新インデックス名 インデックス名を変更する
remove_index remove_index :テーブル名 [, オプション] インデックスを削除する

まとめ

モデルの作成は簡単だが、マイグレーションファイルのみ作成し、自分でカラムを追加したり削除したりすることはあまりやってなかったのでこれで理解できるようになった。
すべてのメソッドを使いこなせるにはまだまだ時間がかかりそうだが、柔軟にマイグレーションファイルを使いこなせるようにしていきたい。

現場Railsで唐突に出てきたメソッドの理解

Railsを勉強している人なら誰しもがわかる技術書「現場で使えるRuby on Rails速習実践ガイド(以下、現場Rails)」にて勉強していたらRailsチュートリアルでも見たことのないメソッドが出てきて、 「現場で使われるということはこういうことか...」とか勝手に思った。

どのようなメソッドだったのか。
simple_formatメソッドである。(そんなん知ってるわ、って思っていたらすみません) 現場RailsのChapter3-3で出てくる。現場Railsによると、

simple_formatはデフォルトエリアを

タグで囲い、テキストに含まれる一部の危険なHTMLタグを取り除いてくれます(=sanitizeオプション)。

しかし今回は一部のタグを取り除くのではなく、すべてのタグを安全な形で表示することにします。〜以下略

と書いてある。しかしよくわからない。
テキストに含まれる一部の危険なHTML?! これは調べないとわからない。。Railsドキュメントでググってみよう。

説明

  • 文字列を<p>で括る。
  • 改行は <br/> を付与。
  • 連続した改行は、</p> <p>を付与。

使い方

simple_format(文字列 [, オプション or HTML属性 or イベント属性])

オプション

オプション 説明
:sanitize サニタイズ
:wrapper_tag 文字列を囲むタグ

イベント属性

イベント属性 説明
:onclick クリックされた時
:ondblclick ダブルクリックされた時
:onmousedown マウスのボタンが押し下げられた時
:onmouseup マウスのボタンが離された時
:onmouseover カーソルが重なった時
:onmousemove カーソルが移動した時
:onmouseout カーソルが離れた時
:onkeypress キーが押されて離された時
:onkeydown キーが押し下げられた時
:onkeyup キーが離された時

<% text = <<EOL
テキスト

テキストテキスト
EOL %>
<%= simple_format(text) %>
# <p>テキスト</p>
# <p>テキストテキスト</p>

simple_formatの概要は理解できた。では一体なぜこのようなメソッドがあるのか。
冒頭で?になっていたテキストに含まれる一部の危険なHTMLタグを取り除くということがこのメソッドの特徴である。

sanitize(サニタイズ)とは

サニタイズ(Sanitize)とは、危険なコードやデータを変換または除去して無力化する処理です。 たとえば、Webサイトに設置された入力フォームなどから、悪意のあるコードが入力され、その文字列が実行されることで様々な被害に遭う可能性があります。この入力値に対しサニタイズを行い、悪意のあるコードを無力化(単なる文字列として扱う)することで、被害を阻止することができます。 サニタイズ(Sanitize) | セコムトラストシステムズのBCP(事業継続計画)用語辞典より

では度々危険なHTMLタグや危険なコードと言っているが危険な〜とはどういうことなのか。
クロスサイトスクリプティングXSSという、攻撃対象のウェブサイトに、脆弱性がある掲示板のようなウェブアプリケーションが掲載されている場合に、悪意のある第三者がそこへ罠を仕掛け、サイト訪問者の個人情報を盗むなどの被害をもたらす攻撃が危険ということがわかった。

www.kagoya.jp

クロスサイトスクリプティングXSS)については上記の記事でわかりやすく図を使って説明されているので掲載しておく。
また→XSS攻撃体験サイトでは実際にクロスサイトスクリプティングXSS)が体験できるサイトなのでどういうものなのか体感できます。

まとめ

初めてsimple_formatの説明を現場Railsで読んだときは危険ななんとかかんとかってなんやねん!って思ってましたが、クロスサイトスクリプティングXSS)という攻撃があり、これがどのような危険があるかということを知ることができ、セキュリティ面で非常に活躍するメソッドなんだなということがわかった。WEBアプリケーションでセキュリティは一番大事といっても過言では無いと思うので、simple_formatメソッドを使いこなせるようにしていきたい。

モデルを作ってrails db:migrateしようとしたらエラーに遭遇した件

タイトルのままなんですが、、、 一応開発環境とかも説明しておく

開発環境

Rails 5.2.4
Ruby 2.5.0

マイグレーションファイル

モデル名 Task
テーブル名 tasks
カラム name:string
description:text

どのようなエラーが出た?

rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:
PG::DuplicateTable: ERROR: relation "tasks" already exists

英語が苦手というわけではないが、すらすら読めるわけではないので便利な「Deel」という翻訳アプリを使って翻訳してみた。

レールは中止されました !

エラーが発生し、これ以降のすべての移行がキャンセルされました。

"tasks" リレーションが既に存在する。

うーん。なるほど。多分めんどくさい。

マイグレーションファイルに問題がありそうなので、
rails db:migrate:statusコマンドでマイグレーションファイルの状態を確認してみよう。

database: taskleaf_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20201228122412  ********** NO FILE **********
   up     20201230060553  ********** NO FILE **********
   up     20201230061636  ********** NO FILE **********
   up     20201230094313  ********** NO FILE **********
   up     20201230095352  ********** NO FILE **********
   up     20201230150314  ********** NO FILE **********
  down    20210106145728  Create tasks

!?!?!?!?!?明らかにおかしい... まずこのNO FILEのマイグレーションファイルを全部削除して再度rails db:migrationを実行することにしようか。

qiita.com

上記の記事を参考にして、一度マイグレーションファイルをすべて削除し、綺麗な状態に戻そう。

再度 rails db:migrate:statusコマンドを叩き、

database: taskleaf_development

 Status   Migration ID    Migration Name
--------------------------------------------------

この状態にしたらrails g model Task name:string description:text を実行し、rails db:migrateを実行してみる。

んんん???エラーが治らん... マイグレーションをリセットするのを忘れてしまっていた。
rails db:migrate:resetを叩き、 rails db:migrate:statusを見てみると、

database: taskleaf_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20210106154208  Create tasks

しっかり動きました。

まとめ

マイグレーションファイルやデータベースはRailsと別の世界にあるので、データベースの世界にカラムが残ってたり、マイグレーションファイルを消しても内部で NO FILE みたいに残っていることがある。 焦らず、rails db:migrate:statusで、マイグレーションの状態を確認し、不要なファイルがあれば、削除。再度リセットする。ということを頭に入れて置くことが大事だと感じた。

〜完〜

Rails国際化(i18n)を簡単に導入する

Railsにはよくあるエラーメッセージがerrorsメソッドで呼び出される。 デフォルトの状態では英語での出力となっているため、日本語に対応させる必要がある。 簡単に日本語対応するには2つのステップで対応させることができる。(他にもやり方は色々あるが、自分がやりやすいやり方で紹介する)

ステップ1

$ wget https://raw.githubusercontent.com/svenfuchs/rails-i18n/master/rails/locale/ja.yml -P config/locales

ターミナルでこのコマンドを叩く。
コマンドのオプションに関してはLinuxの範囲になるので、詳しくは割愛するが、 wgetコマンド + URLを指定することによってそのURLのファイルをダウンロードすることができる。
実際にhttps://raw.githubusercontent.com/svenfuchs/rails-i18n/master/rails/locale/ja.ymlにアクセスしてみるとわかるが、i18nの日本語ymlファイルがある。
また、-Pオプションで保存先となるディレクトリを指定することができる。つまり、configディレクトリ内のlocalesディレクトリにja.yml ファイルがダウンロードされている。

---
ja:
  activerecord:
    errors:
      messages:
        record_invalid: 'バリデーションに失敗しました: %{errors}'
        restrict_dependent_destroy:
          has_one: "%{record}が存在しているので削除できません"
          has_many: "%{record}が存在しているので削除できません"
  date:
    abbr_day_names:
    - 日
    - 月
    - 火
    - 水
    - 木
    - 金
    - 土
    abbr_month_names:
    - 
    - 1月
    - 2月
    - 3月
    - 4月
    - 5月
    - 6月
    - 7月
    - 8月
    - 9月
    - 10月
    - 11月
    - 12月
    day_names:
    - 日曜日
    - 月曜日
    - 火曜日
    - 水曜日
    - 木曜日
    - 金曜日
    - 土曜日
and more...

詳しくは↓のページに色々書いてるので参考に。 www.atmarkit.co.jp

ステップ2

あとはデフォルトで日本語のコンテンツを使うようにアプリケーションの設定を変更する。 config/initializersディレクトリにlocale.rbというファイルを作って

Rails.application.config.i18n.default_locale = :ja

とすることで、Railsアプリケーションのエラーメッセージなどを日本語化することができる。