- 投稿日:2020-07-21T23:51:01+09:00
【Rails】renderメソッドの引数のカッコの有無について
- 投稿日:2020-07-21T22:39:25+09:00
rails c でkey errorが出た時
以前FactoryBotの確認でコンソールをいじっている時にそれは起こった。
irb(main):001:0> FactoryBot.create(:user) Traceback (most recent call last): 1: from (irb):1 KeyError (Factory not registered: "user")FactoryBotの中身を確認してみる。。
FactoryBot.define do factory :user do name {Faker::Name.last_name} email {Faker::Internet.free_email} password = Faker::Internet.password(min_length: 8) password {password} password_confirmation {password} end endFavtoryBotのエラーについて調べたが()で閉じることでエラーが出ることが多いみたい。でも{}で中身入れてるし問題ない。
調べていくうちにrailsコマンドが正常に作動しない時はspringを止めたら動き始めるとのことが発覚。
springはrailsが持っている昨日で入力したrailsコマンドの起動を早くしてくれるらしいがたまに固まってしまうらしい。
spring stopこれで無事動き始めました。
安心してください、、、またrailsコマンドを使うと自動的にspringは起き上がります!
- 投稿日:2020-07-21T22:10:53+09:00
マークダウン記法でDB設計【残り55日】
使用したマークダウン記法
[#]
見出しとして使用
数を増やすとfont-sizeが小さくなっていく
HTMLでいうh1~h6タグ[|文字列|]
表の作成として使用
コロンを文字列の左、左右、右につけることで左寄せ、中央揃え、右寄せにできる[-]
箇条書きとして使用
ハイフンの後に半角スペースを入れることテーブル設計
users テーブル
Column Type Option name string null: false string null: false password string null: false Association
- has_many :room_users
- has_many :rooms, through :room_users
- has_many :messages
カラム、アソシエーションが一目瞭然なのでアプリケーション開発には必須の作業。
- 投稿日:2020-07-21T21:18:18+09:00
[Rails]Administrateでメインアプリで使っていたヘルパーメソッドを動作させる方法
困ったこと、やりたいこと
\\\メインアプリのヘルパーメソッドが使えない///
Railsアプリで管理画面を作成するために、Administrateというgemを使用した時のことです。
メインアプリのビューで使うためにhelperファイルに定義したhelperメソッドを、admin以下のビューでも使おうとしたところ、下記のエラーが…
image_present?
は、メインアプリのビューで使用しているヘルパーメソッド(自分でhelpers/shops_helper.rb
に定義したもの)です。
メインアプリではNoMethodErrorにならないのに…どうすればいいの???環境
- Ruby 2.5.1
- Rails 5.0.7.2
- Administrate 0.14.0
結論
1.
config/application.rb
ファイルに、下記の設定を記述する。
2. アプリケーション(サーバー)を再起動する以上。簡単でした笑
config/application.rbmodule Hoge class Application < Rails::Application # 他のconfigが書かれてるかも # ここから config.to_prepare do Administrate::ApplicationController.helper Hoge::Application.helpers end # ここまで end end※
Hoge
はアプリ名です。通常ならば、1行目のmodule
の後ろにrails newした時のアプリ名が記載されているはずです。
Administrate::ApplicationController.helper Hoge::Application.helpers
の
Hoge
の部分を、ご自身のアプリ名に置き換えてください。しっかりGitHubのIssuesに挙げられていて、解決済みだった
Best way to include main app helper modules in administrate? #334
余談
参考記事に挙げたGitHubのIssuesを見ていると、他の方法もあるよう。
こちらの方法は、
1.helpers
ディレクトリの下にadministrate
ディレクトリを作成
2. そこにapplication_helper.rb
ファイルを作成
3. そこにヘルパーメソッドを再度定義する
という方法のようです。試しにやってみた
helpers/administrate/application_helper.rbmodule Administrate::ApplicationHelper def image_present?(shop) if shop.shop_images.present? image_tag "#{shop.shop_images[0].shop_image}", class: 'top-cover-img shop-img' else image_tag asset_path('no-image.png'), class: 'top-cover-img shop-img' end end endポイントは、
module
の宣言の際に、クラスの継承?(Administrate::ApplicationHelper
)を記述することだそうです。
これがないとRouting Errorになります。
実現できるが、DRY原則に反する
一応上記の方法でも実現できましたが、同じヘルパーメソッドを2回定義することになるので、DRY原則に反するなと思いました。
無駄なファイルも作成しなくていいので、冒頭の方法をおすすめします。
- 投稿日:2020-07-21T20:48:54+09:00
Railsで、チャット機能を実現する
Railsで、チャット機能を実現する
実現したいこと
- UserとProducer間で、チャットができるようにしたい。
- 基本的にはUserからしかチャットを持ちかけることはできない。
- 「User」としてログインした際に、商品の詳細ページから、「生産者にDMする」ボタンを押すと、チャットページに遷移し、商品を出品している生産者と利用でDMができる。
- 「Producer」としてログインした際は、「DMリスト一覧」ボタンを押すと、Producerとチャットしたことのあるユーザー一覧が表示される。そこから各ユーザーとのチャット画面に遷移することができる。
前提
2つのUser間でチャットが行われる前提なので、2つのモデル(UserモデルとProducerモデルなど)をdeviseで作成してあることが前提となります。
モデル作成
$ rails g model room user_id:integer producer_id:integer $ rails g model message room_id:integer is_user:boolean content:text $ rails db:migrateアソシエーション設定
user.rbhas_many :roomsproducer.rbhas_many :roomsroom.rbhas_many :messages belongs_to :producer belongs_to :userコントローラー作成
$ rails g controller rooms index show $ rails g controller messagesルーティング修正
routes.rbRails.application.routes.draw do root to: 'home#index' devise_for :producers devise_for :users resources :products do resources :users, only: [:show] resources :rooms, only: [:index,:show, :create] do resources :messages, only: [:create] end endroomsの中にmessagesをネストすることによって、messageのcreateアクションのURLは
/rooms/:room_id/messages(.:format)
になります。roomsコントローラー記述
rooms_controller.rbclass RoomsController < ApplicationController def index #producerとしてログインした際の、「DMリスト」画面 # チャットページへの遷移処理追加 if producer_signed_in? # producer がサインインしていたら @users = User.all #全てのuserを取得 rooms = current_producer.rooms #現在ログインしているproducerが所持している全ての rooms を取得 @user_ids = [] #配列を用意 rooms.each do |r| #「現在ログインしている Producer が所持している全てのrooms」を1つずつ取り出す @user_ids << r.user_id #ログインしている Producer のチャット相手、つまり user の user_id を @user_ids に格納 end #これにより、 user(チャット相手)の user_id があるかどうかview側で判断して、「チャットしたことある相手なのかどうか」を判断している end end def show #トークルーム画面 @room = Room.find(params[:id]) #ルーム情報の取得( user_id, producer_id が入っているかどうかを調べる) @message = Message.new #新規メッセージ作成 @messages = @room.messages if user_signed_in? #もし、ユーザーがサインインしていて if @room.user_id == current_user.id #かつ、roomに入っている user_id が、 現在ログインしているユーザーのidだったら @producer = @room.producer # @producerに、現在 room に入っている producer の情報を全て格納する else redirect_to "/" end elsif producer_signed_in? #もし、producerがログインしていて if @room.producer_id == current_producer.id #かつ、 roomに入っている producer_id が、 現在ログインしている producer のidだったら @user = User.find_by(id: @room.user_id) # @userに、現在 room に入っている user の情報を全て格納する else redirect_to "/" end end end def create if user_signed_in? #もしユーザーがサインインしていたら @room = Room.new(room_producer_params) @room.user_id = current_user.id # @room の user_id に、 現在ログインしているユーザーのidを代入する。 else redirect_to "/" end if @room.save redirect_to action: "show", id: @room.id else redirect_to "/" end end private def room_producer_params params.require(:room).permit(:producer_id) end endcreateアクションについて説明していきます。
これは、商品詳細ページから、userとproducerのチャットルームが作られる時の挙動を表しています。
チャットルームが作られる際の条件としては、 user_id と producer_id が必要です。
Room.new(room_producer_params)で、html側のhidden_fieldでproducer_idを送信しています。
@room.user_id には、現在ログインしているユーザーのidを代入しています。
これで2つ揃ったので、チャットルームが作成されます。
if @room.save
redirect_to action: "show", id: @room.id
else
で、チャットルームが作成されたら、チャット画面に遷移する。どのチャット画面に遷移すればいいのかわからないので、@room.idを引数に渡しています。messagesコントローラー記述
messages_controllerclass MessagesController < ApplicationController def create @room = Room.find(params[:room_id]) # Roomモデルから、 該当のroomを取ってくる @message = Message.new(message_params) #新規メッセージ作成 #メッセージがuserによるものだったらis_user=true, producerによるものだったらis_user=false if user_signed_in? #もしユーザーがサインインしていたら @message.is_user = true # is_userをtrueにして、 userの発言としている elsif producer_signed_in? #もし producer がサインインしていたら @message.is_user = false # is_userをtrueにして、 producerの発言としている end @message.room_id = @room.id #messageのroom_idに、現在のルームのidを代入する。 これで、room_id と content が揃うので、message を save 出来る if @message.save # メッセージを save できたら redirect_to room_url(@room) #現在のroomに遷移する else redirect_to room_url(@room) end end private def message_params params.require(:message).permit(:content) end endhtml記述(商品詳細ページ)
show.html.erb<h2>商品詳細</h2> <% if producer_signed_in? && @product.producer == current_producer %> <%= link_to "編集", edit_product_path(@product) %> <% end %> <p>商品名:<%= @product.name %></p> <p>内容:<%= @product.content %></p> <p>値段:<%= @product.price %></p> <p>内容:<%= @product.description %></p> <% if @product.image.attached? %> <p><%= image_tag @product.image %></p> <% end %> <p><%= @product.producer.prefecture.name %></p> <p><%= @product.producer.name %></p> <% if @product.producer.image.attached? %> <p><%= image_tag @product.producer.image %></p> <% end %> <% if user_signed_in? %> <% if @producer_ids.include?(@producer.id) %> <%#ユーザーとしてログインした時に、相手のidが存在するかどうかをチェックしている %> <br> <%= link_to "チャットへ", room_path(current_user.rooms.find_by(producer_id: @producer.id)) %> <% else %> <%= form_for Room.new do |f| %> <%= f.hidden_field :producer_id, :value => @producer.id %> <%= f.submit %> <% end %> <% end %> <% end %>html記述(チャットルームページ)
show.html.erb<h1>チャットルーム</h1> <% if user_signed_in? %> <h3><%= @producer.name %>とのチャットルーム</h3> <div class="chat-field"> <% @messages.each do |m| %> <!-- メッセージがUserによるものだったら --> <% if m.is_user %> <!-- メッセージを右に寄せる --> <div class="right-message"> <%= m.content %> </div> <!-- メッセージがproducerによるものだったら --> <% else %> <!-- メッセージを左に寄せる --> <div class="left-message"> <%= m.content %> </div> <% end %> <% end %> </div> <% elsif producer_signed_in?%> <h3><%= @user.name %>とのチャットルーム</h3> <div class="chat-field"> <% @messages.each do |m| %> <!-- メッセージがUserによるものだったら --> <% if m.is_user %> <!-- メッセージを左に寄せる --> <div class="left-message"> <%= m.content %> </div> <!-- メッセージがproducerによるものだったら --> <% else %> <!-- メッセージを右に寄せる --> <div class="right-message"> <%= m.content %> </div> <% end %> <% end %> </div> <% end %> <%= form_for [@room, @message] do |f| %> <%= f.text_field :content %> <%= f.submit "メッセージを送る"%> <% end %>is_userがtrueかfalseかで、CSSで表示を分けています。
html記述(DM一覧ページ)※Producerとしてログインした場合にしか表示されない
index.html.erb<h1>rooms#index</h1> <% @users.each do |user| %> <% if user.rooms.present? %> <%= user.name %> <% if @user_ids.include?(user.id) %> <br> <%= link_to "チャットへ", room_path(current_producer.rooms.find_by(user_id: user.id)) %><br> <% end %> <% end %> <% end %>
<% if user.rooms.present? %>
に関して、コントローラー側で@users=User.all
で全てのユーザーを取ってきています。
ユーザーが所持しているroomsの値は存在するか? = Producerとチャットしたことがあるユーザーだけをフィルタリングしています。参考資料
Railsでモデルを複数使用しチャット(DM)機能を作成する - Qiita
の記事を参考にさせていただきました。ありがとうございました。
- 投稿日:2020-07-21T18:47:56+09:00
【Vue.js】【CRUD】Vue.js(Nuxt.js)とRailsでユーザー新規登録・ログイン・退会・ログアウト・編集を実装してみる
はじめに
Vue.js(Nuxt.js)とRailsで新規開発を行っており、JWTを用いてユーザー新規登録・ログイン・ログアウト等の認証周りを担当したのでここに記しておきます。備忘録。
今回はauthmoduleとdevise_token_authを使用して実装しました。
別々で解説している記事はあったのですが、全体の流れがわかる記事が少なかったので、執筆しました
この記事が見知らぬ誰かの糧となれば良いなあと思っております〜〜
authmodule公式
devise_token_auth公式環境
- MacOS
- yarn 1.21.1
- node 12.0.0
- vue 2.6.1
- rails 5.2.4
- ruby 2.6.3
新規登録の流れ
Nuxt側: APIにEメールとパスワードをのせて、HTTPリクエストを送信する。
Rails側: HTTPリクエストを受け取り、devise_token_authでパスワードとEメールで認証をする。
Nuxt側: 認証されると、APIから認証TOKENを送り返す。
Nuxt側: レスポンスヘッダー情報をlocalStrageに保存する
Nuxt側: ユーザをログイン画面からホーム画面へリダイレクトする
Nuxt側: ログイン済みであればログインしていないとアクセスできないページへのアクセスを許可する。ログイン済みでなければ、ログインページにリダイレクトする。プラグインの導入
Axiosを導入
Nuxt側にaxiosを追加します。
axios公式ターミナル$ yarn add @nuxtjs/axiosAuth Moduleを導入
Nuxt側にAuth Moduleも追加します。
ターミナル$ yarn add @nuxtjs/authVuex用にindex.jsを作成
Auth moduleはVuexを使用して、ユーザの認証情報を管理します。
そこで、Vuex用のindex.jsファイルを用意しておく必要があります。
nuxtアプリのルートディレクトリにstoreというファイルが作成されるので、そこにindex.jsという名前のファイルを作成しておきます。index.jsの中はなにも記載しなくて大丈夫です。
Nuxt
nuxt.config.jsにプラグインを記載
追加したaxiosとauthをnuxtアプリに読み込ませます。
nuxt.config.jsmodules: [ '@nuxtjs/axios', '@nuxtjs/auth' ]nuxt.config.jsにauthオプションを記載
authで用意されているオプションを記載していきます。
nuxt.config.jsauth: { redirect: { login: '/users/login', logout: '/', callback: false, home: '/users/profile', }, strategies: { local: { endpoints: { login: { url: '/api/v1/auth/login', method: 'post', propertyName: 'token' }, logout: { url: '/api/v1/auth/logout', method: 'post' }, user: false, }, } } }redirect
ユーザーのアクションに応じたリダイレクト先のURL設定。
- login:未ログイン時にリダイレクトされる先のURL
- logout:ログアウトした後にリダイレクトされる先のURL
- callback:コールバック用のURL。Oauth認証(SNS認証)等に使われる。
- home:ログイン後にリダイレクトされる先のURL
strategies
Auth Moduleの認証ロジックの設定です。JWTとCookieを使うlocalと、OAuthを使うsocialの2種類の設定ができます。今回はOauth認証は使わないので、socialは記載していません。
- endpoint:どのメソッドが呼ばれた際に、APIのどのエンドポイントに飛ばすかを設定します。例えば、loginメソッドが呼び出されたら、APIにpost: /api/auth/loginにHTTPリクエストを送信します。
ログイン
ログイン画面の実装です。
vuetifyというUIフレームワークを使用しています。template部分は適宜変えてください。users/login<template> <v-container> <v-card width="400px" class="mx-auto mt-5"> <v-card-title> <h1 class="display-1"> ログイン </h1> </v-card-title> <v-card-text> <v-form ref="form" lazy-validation> <v-text-field v-model="email" prepend-icon="mdi-email" label="メールアドレス" /> <v-text-field v-model="password" prepend-icon="mdi-lock" append-icon="mdi-eye-off" label="パスワード" /> <v-card-actions> <v-btn color="light-green darken-1" class="white--text" @click="loginWithAuthModule" > ログイン </v-btn> </v-card-actions> </v-form> </v-card-text> </v-card> </v-container> </template> <script> export default { name: 'App', auth: false, data() { return { password: '', email: '', } }, methods: { // loginメソッドの呼び出し async loginWithAuthModule() { await this.$auth .loginWith('local', { // emailとpasswordの情報を送信 data: { email: this.email, password: this.password, }, }) .then( (response) => { // レスポンスで返ってきた、認証に必要な情報をlocalStorageに保存 localStorage.setItem('access-token', response.headers['access-token']) localStorage.setItem('client', response.headers.client) localStorage.setItem('uid', response.headers.uid) localStorage.setItem('token-type', response.headers['token-type']) return response }, (error) => { return error } ) }, }, } </script>methodの部分をざっくり説明すると、
1.入力されたemailとpasswordの情報をloginメソッドを用いて、APIに送信する。
2.RailsAPI側からのレスポンスから認証機能に必要な情報(access-token・client・uid・token-type)をlocalStorageに保存するということをやっています。他にもやり方はありますし、仕様によっても異なるので、あくまで参考程度に、、、
ただ、APIに入力された情報を送信して、認証機能に必要な情報を保持するというのは基本的に同じです。
この仕組みをある程度理解しておくと、応用できると思います。新規登録
新規登録画面の実装です。
users/signup<template> <v-container> <v-card width="400px" class="mx-auto mt-5"> <v-card-title> <h1 class="display-1"> 新規登録 </h1> </v-card-title> <v-card-text> <v-form ref="form" lazy-validation> <v-text-field v-model="user.email" prepend-icon="mdi-email" label="メールアドレス" /> <v-text-field v-model="user.password" prepend-icon="mdi-lock" append-icon="mdi-eye-off" label="パスワード" /> <v-text-field v-model="user.password_confirmation" prepend-icon="mdi-lock" append-icon="mdi-eye-off" label="パスワード確認" /> <v-card-actions> <v-btn color="light-green darken-1" class="white--text" @click="registerUser" > 新規登録 </v-btn> </v-card-actions> </v-form> </v-card-text> </v-card> </v-container> </template> <script> export default { name: 'App', auth: false, data() { return { user: { password: '', email: '', password_confirmation: '', }, } }, methods: { registerUser() { this.$axios.post('/api/v1/auth', this.user).then((response) => { window.location.href = '/users/comfirmation' }) }, }, } </script>auth moduleには新規登録のヘルパーメソッドはないので、axiosを使ってメソッドを自作します。
といっても、registerUserが発火されたら新規登録のエンドポイントにメールアドレスやパスワードの必要情報を送信するだけです。
見た目は各自適当に弄ってください!!
ログアウト・退会
users/account<template> <v-app> <v-container> <v-row> <v-spacer></v-spacer> <v-col cols="12" lg="4"> <v-row> <v-col cols="12" lg="7" class="grey--text text--darken-3 font-weight-bold pa-2 text-h6" > <p> アカウント設定 </p> </v-col> </v-row> <v-row class="my-5"> <v-col cols="12" lg="7" class="pa-2"> <a href="/" class="grey--text text--darken-3 mb-1" @click="$auth.logout()" > ログアウト </a> </v-col> <v-col cols="12" lg="5" class="pa-2 text-right"> <font-awesome-icon icon="angle-right" /> </v-col> </v-row> <v-divider></v-divider> <v-row class="my-5"> <v-col cols="12" lg="7" class="pa-2"> <a href="#" class="red--text text--darken-3 mb-1" @click="deleteUser" > 退会 </a> </v-col> <v-col cols="12" lg="5" class="pa-2 text-right"> <font-awesome-icon icon="angle-right" /> </v-col> </v-row> </v-col> <v-spacer></v-spacer> </v-row> </v-container> </v-app> </template> <script> export default { name: 'App', data: () => ({}), methods: { deleteUser() { this.$axios .delete('api/v1/auth', { headers: { 'access-token': localStorage.getItem('access-token'), uid: localStorage.getItem('uid'), client: localStorage.getItem('client'), }, }) .then((response) => { this.$auth.logout() window.location.href = '/' }) }, }, } </script>ログアウトはauth moduleにヘルパーメソッドがあるので利用しましょう。
クリックと同時に$auth.logout()を発火すれば、完了です。
header内に保存されていた認証情報がすべて削除され、未ログイン状態になります。退会は新規登録と同様にメソッドを作ります。
axiosでエンドポイントに認証情報を付与して送信します。httpメソッドはdeleteですね。
その後、ログアウトメソッドを実行しとけばエラーにならず、安心です。
簡単ですね。編集
users/edit<template> <v-app> <v-container> <v-card width="400px" class="mx-auto mt-5"> <v-card-title> <h1 class="display-1"> メールアドレス変更 </h1> </v-card-title> <v-card-text> <v-form ref="form" lazy-validation> <v-text-field v-model="user.email" prepend-icon="mdi-email" label="新しいメールアドレス" /> <v-text-field v-model="user.password" prepend-icon="mdi-lock" append-icon="mdi-eye-off" label="パスワード" /> <v-card-actions> <v-btn color="light-green darken-1" class="white--text" @click="editEmail" > 保存する </v-btn> </v-card-actions> </v-form> </v-card-text> </v-card> </v-container> </v-app> </template> <script> export default { name: 'App', data() { return { user: { password: '', email: '', }, } }, methods: { editEmail() { this.$axios .put('api/v1/auth', this.user, { headers: { 'access-token': localStorage.getItem('access-token'), uid: localStorage.getItem('uid'), client: localStorage.getItem('client'), }, }) .then((response) => { localStorage.setItem('access-token', response.headers['access-token']) localStorage.setItem('client', response.headers.client) localStorage.setItem('uid', response.headers.uid) localStorage.setItem('token-type', response.headers['token-type']) window.location.href = '/' }) }, }, } </script>メールアドレスの編集ページです。見た目はほとんど新規登録・ログイン画面と同じです。
編集は認証情報を渡して、受け取らなければなりません。
リクエストで認証情報を付与して送信する。成功したら受け取った認証情報をlocalStrageに保存する。
理由はアカウント編集すると認証情報が変わるからです。
これで編集もできました。auth module補足
一連の流れを見ていただいた方ならおわかりかと思いますが、auth moduleはそこまで機能がもりもりではありません。
すべてのメソッドを作ってくれているわけではなく、ある程度は自力で頑張る必要があります。
認証機能の補助輪くらいに考えておいてください!!
ただ、リダイレクト先の指定やloginメソッドなど優秀な機能もありますので、Nuxtで認証機能を実装する方は是非使ってみてください。
他に認証機能で良いプラグインがあれば教えて下さいRails
フロント側(Nuxt側)ばかりやってきましたが、APIが飛ばないとお話になりませんので、バックエンド側(Rails側)もやっていきましょう!!
devise_token_authを導入
Gemfileにdevise_token_authとrack-corsを記載し、bundle installをします。
Gemfile# ログイン機能 gem 'devise' gem 'devise_token_auth' # CORS設定 gem 'rack-cors'ターミナル$ rails g devise:install $ rails g devise_token_auth:install User authこれで色々ファイルが生成されると思います。
※すでにUserモデルがある方は設定方法が少し変わるので、対処法は公式を読んでください。DB作成
db/migrate/~_devise_token_auth_create_users.rbclass DeviseTokenAuthCreateUsers < ActiveRecord::Migration[5.2] def change create_table(:users) do |t| ## Required t.string :provider, :null => false, :default => "email" t.string :uid, :null => false, :default => "" ## Database authenticatable t.string :encrypted_password, :null => false, :default => "" ## Recoverable t.string :reset_password_token t.datetime :reset_password_sent_at t.boolean :allow_password_change, :default => false ## Rememberable t.datetime :remember_created_at # ここを追記 -------------------------------------------- ## Trackable t.integer :sign_in_count, default: 0, null: false t.datetime :current_sign_in_at t.datetime :last_sign_in_at t.string :current_sign_in_ip t.string :last_sign_in_ip # ----------------------------------------------------- ## Confirmable t.string :confirmation_token t.datetime :confirmed_at t.datetime :confirmation_sent_at t.string :unconfirmed_email # Only if using reconfirmable ## Lockable # t.integer :failed_attempts, :default => 0, :null => false # Only if lock strategy is :failed_attempts # t.string :unlock_token # Only if unlock strategy is :email or :both # t.datetime :locked_at ## User Info t.string :name t.string :nickname t.string :image t.string :email ## Tokens t.text :tokens t.timestamps end add_index :users, :email, unique: true add_index :users, [:uid, :provider], unique: true add_index :users, :reset_password_token, unique: true add_index :users, :confirmation_token, unique: true # add_index :users, :unlock_token, unique: true end endmigrateしてDBを作成します。
devise_token_authの設定
config/initializers/devise_token_auth.rbDeviseTokenAuth.setup do |config| # リクエストごとにトークンを更新するか config.change_headers_on_each_request = false # トークンの有効期間 config.token_lifespan = 2.weeks # headersの名前対応 config.headers_names = {:'access-token' => 'access-token', :'client' => 'client', :'uid' => 'uid', :'token-type' => 'token-type' } endここで諸々の設定をします。
トークンの有効期限やheadersの送信名もここで設定します。controllers/application_controller.rbclass ApplicationController < ActionController::Base include DeviseTokenAuth::Concerns::SetUserByToken skip_before_action :verify_authenticity_token, if: :devise_controller? endAPIではCSRFチェックをしないようにapplication_controller.rbをすこしいじったら、
準備完了です。ルーティング設定
routes.rbRails.application.routes.draw do devise_for :users namespace :api do scope :v1 do mount_devise_token_auth_for 'User', at: 'auth' end end end公式通りだとこんな感じですね。
リクエストテスト
では実際にリクエストを投げてみましょう。
今回はログイン機能で試してみます。
※email:example@example.com,password:passwordというユーザーが登録されている想定です。
ログイン機能のエンドポイントである/api/v1/auth/sign_inにpostでリクエストを送信します。
Content-Typeはapplication/jsonを選択しておきましょう。
BODYにemailとpasswordの情報をjson形式で付与します。これでリクエストを投げます。
うまく行けばステータスコード200でレスポンスが返ってきます。
こんな感じですね。
新規登録やログアウトも同様です。
エンドポイント、HTTPメッソド、付与情報を変更して、リクエストを送れば、それに応じたレスポンスが返ってくるはずです。devise_token_auth補足
devise_token_authではdeviseと同じように
before_action :authenticate_user!
やcurrent_user
も使えます。
フロント側で保持しておくべき情報も基本的にはレスポンスで返してくれるので、それを取り出してlocalStrage等で保存しておきましょう。詳しくは公式を読んでみてください。
devise_token_auth公式完成!!!
これで完成です!!!長かったですね、、、
フロント側(Nuxt側)もバックエンド側(Rails側)もバッチリだと思います。
もう一度、新規登録を例にとって流れを振り返ります。Nuxt側: APIにEメールとパスワードをのせて、HTTPリクエストを送信する。
Rails側: HTTPリクエストを受け取り、devise_token_authでパスワードとEメールで認証をする。
Nuxt側: 認証されると、APIから認証TOKENを送り返す。
Nuxt側: レスポンスヘッダー情報をlocalStrageに保存する
Nuxt側: ユーザをログイン画面からホーム画面へリダイレクトする
Nuxt側: ログイン済みであればログインしていないとアクセスできないページへのアクセスを許可する。ログイン済みでなければ、ログインページにリダイレクトする。これができているはずです。
ログインや退会等のその他の機能も流れは基本的に変わりません!!最後に
auth moduleとdevise_token_auth別々の解説記事はあったのですが、まとまった記事(流れがわかるような記事)がなかったため、今回執筆しました。
長かった、、、かなり、、、
もしわからない点があれば気軽にコメントしてくださいね〜
- 投稿日:2020-07-21T18:35:58+09:00
Railsで日時を日本語表記に変更
はじめに
テーブルのカラム
created_at
を使えば、データを作成した日時を取り出せます。
ところが、そのままの表記では使いづらいです。今回は、
2020/7/21 9:05:50
の様な表記で取り出せるようにしたいと思います。作成日時を表示する
まず
posts
テーブルのデータ@post
のcreated_at
をそのまま取り出してみましょう。posts/show.html.erb<p>イベント: <%= @post.event %></p> <p>場所: <%= @post.place %></p> <p>作成日時: <%= @post.created_at %></p>
place event created_at 東京 花火大会 2020-07-21 00:05:50 UTC 目標としている
2020/7/21 9:05:50
の表記と大いに違うことが分かると思います。具体的には以下の3点が異なります。
UTC
(世界標準時) になっている/
のところが-
になっている7
の表記が07
になっている順番に修正していきたいと思います。
1.世界標準時から日本時間に変更する
プログラミングは世界中で使われている為、そのままだと
世界標準時
で出力されてしまいます。
その為、日本時間
に変更しましょう。config/application.rbclass Application < Rails::Application # ***** 次の1行を追加 ***** config.time_zone = "Asia/Tokyo" endこの1行を入れることで以下のように、
世界標準時
を日本時間
に変更することができました。
place event created_at 東京 花火大会 2020-07-21 09:05:50 +0900
+0900
がついていますが、これは、日本時間と世界標準時との時差が+9時間であることを表しています。2.フォーマットを変更する
日本時間にはなりましたが、フォーマットが
2020-07-21 09:05:50 +0900
では分かりにくいです。変更していきましょう。まず、多言語に対応する為の gem をインストールし、日本語に設定します。
Gemfile# Rails6 の場合 gem 'rails-i18n', '~> 6.0' # Rails5 の場合 gem 'rails-i18n', '~> 5.1'ターミナルbundle install
config/application.rbclass Application < Rails::Application # 略 config.time_zone = "Asia/Tokyo" # ***** 次の1行を追加 ***** config.i18n.default_locale = :ja end次に、
rails-i18n
に用意されているl
メソッドを使用します。posts/show.html.erb<p>イベント: <%= @post.event %></p> <p>場所: <%= @post.place %></p> <!-- ***** l を追加 ***** --> <p>作成日時: <%= l @post.created_at %></p>さらに、config/locales/ja.ymlというファイルを作成し、フォーマットを設定しましょう。
config/locales/ja.ymlja: time: formats: default: "%Y/%m/%d %H:%M:%S"これにより、表記が以下のように変わり、+0900も消えます。
place event created_at 東京 花火大会 2020/07/21 09:05:50 フォーマットは自由に変更することができ、例えば、
秒
が不要なら:%S
を省くことで対応出来ます。config/locales/ja.ymldefault: "%Y/%m/%d %H:%M"
place event created_at 東京 花火大会 2020/07/21 09:05
記号 意味 %Y 年(YEAR) %m 月(MONTH) %d 日(DAY) %H 時(HOUR) %M 分(MINUTE) %S 秒(SECOND) [参考ページ] Ruby 2.7.0 リファレンスマニュアル(strftime)
3.ゼロ(0)埋めをなくす
例えば、
7月
が07
と表記されるのは不自然なので0
を削りましょう。config/locales/ja.ymlja: time: formats: # ***** %の後ろに-を追加 ***** default: "%Y/%-m/%-d %-H:%M:%S"
place event created_at 東京 花火大会 2020/7/21 9:05:50
-
を入れることで以上の様に、0埋め
をなくすことが出来ました。
- 投稿日:2020-07-21T17:42:00+09:00
DockerでRailsのセットアップ
① develop > docker > app名(test) を作成
② test内に、'dockerfile'、'docker-compose.yaml'を作成
③ 'dockerfile'編集
dockerfileFROM ruby:2.6.6-stretchDocker は Dockerfile から命令を読み込んで、自動的にイメージをビルドします。 Dockerfile はテキストファイルであり、イメージを作り上げるために実行するコマンドラインコマンドを、すべてこのファイルに含めることができます。 docker build を実行すると、順次コマンドライン命令を自動化した処理が行われて、ビルド結果となるイメージが得られます。
④ 'docker-compose.yaml'編集
docker-compose.yamlversion: '3' services: app: build: . volumes: - ".:/app" ports: - "3000:3000" tty: true⑤ ターミナル
docker-compose up # コンテナの起動・構築6 コンテナを作成して起動からログインしてbashで操作
docker exec -it SAMPLE_APP_1 /bin/bash
Dockerでよく使うコマンドまとめ
- 投稿日:2020-07-21T16:09:22+09:00
[Rails API + SPA] ログイン方法とCSRF対策例
記事の趣旨
モダンなWebアプリ開発の学習として、Vue.jsによるフロント、Rails APIによるバックエンドで実装するSPAを作りました。
その中で、SPAの様々なログイン実装方法や、それに必要な CSRF 対策を学びました。
最終的に
session + cookie
を利用するログインを実装した過程を記録したいと思います。ログイン実装方法の検討
SPAのログイン実装方法
下記の記事等を読んで、主に
JWT
を使う方法と、session
を使う方法があることが分かりました。こちらの記事では、様々な方法の良し悪しが網羅的にまとめられていました。
こちらの記事では、JWTとCookieが端的に比較されていました。
こちらの記事では、
JWT
をLocal Storage
に保存することの危険性が説かれています。重要なデータを保存する必要があるなら、常にサーバーサイドセッションを使うべきです。
Cookie + session認証
いずれの記事でも、
JWT
を単純にLocal Storage
に保存する方法は避けるべきだと言われています。
そこで、下記の記事の実装例をベースに、session
をCookie
に保存する方法を採用することにしました。SPA + Rails API 構成におけるcookie + session認証
この方法は、普通のRailsアプリと同じように、
session
やcookies
を使うことができるシンプルさが良いと思いました。この場合、 CSRF 対策を自分で実装する必要があり、それについては後半に扱います。
ログイン機能実装
環境
ruby 2.7.1
rails 6.0.3
Rails APIモードでの立ち上げ
bundle exec rails new アプリ名 --api -TC -d mysql
--api
オプションが重要です。その他は、
-T
テストなし(RSpecを使う場合等)、-C
ActionCableなし、-d mysql
DBを指定、というオプションです。バックエンド側のログイン実装
バックエンド側のログイン機構は、Railsチュートリアルにあるような基本的な方法を使いました。
auth_controller.rb# フロントから { email: 'メールアドレス', password: 'パスワード' }のようなparamsが送られるものとする def create user = User.find_by(email: params[:email]) if user&.authenticate(params[:password]) session[:user_id] = user.id payload = { message: 'ログインしました。', name: user.name } else payload = { errors: ['メールアドレスまたはパスワードが正しくありません。'] } end render json: payload end認証に成功したら、
session
にuser_id
を保存しています。(フロントのページに表示するためにユーザー名も返しています。)ただし、この
session[:user_id] =
というメソッドをAPIモードのRailsで使うには、下記の設定が必要です。
config/application.rb
に下記の3行を追記参考: Rails の API モードでセッションを有効にする
application.rbmodule Myapp class Application < Rails::Application … config.middleware.use ActionDispatch::Cookies config.middleware.use ActionDispatch::Session::CookieStore config.middleware.use ActionDispatch::ContentSecurityPolicy::Middleware end endまた、「ログイン状態を保持する」がチェックされた際に、以下のように署名付きCookieに保存するようにしました。
cookies.permanent.signed[:user_id] = user.id cookies.permanent[:remember_token] = user.remember_token
cookies
を使う時には、下記のようにActionController::Cookies
をコントローラーにincludeする必要がありました。application_controller.rbclass ApplicationController < ActionController::API include ActionController::Cookies # 追加 … endこれで、普通のRailsアプリと同じように、安全なCookieに情報が保存され、リクエストで自動的にCookie情報が送られるようになります。
ログインが必要なアクションでは、下記のような
before_action
で、@current_user
をセットするようにしました。application_controller.rbclass ApplicationController < ActionController::API before_action :require_login private def require_login @current_user = User.find_by(id: session[:user_id]) return if @current_user render json: { error: 'unauthorized' }, status: :unauthorized end endCORSの設定
バックエンドとフロントエンドのオリジンが異なる場合は、axiosなどによるフロントからのリクエストを受け取るために、
CORS
の設定が必要です。例えば開発環境で、バックエンドを
localhost:3000
、フロントをlocalhost:8080
で動かしている場合などです。バックエンド側
gem 'rack-cors'
を導入して、config/initializers/cors.rb
に以下のように記述します。cors.rbRails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins '*' resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head], credentials: true end end最後の
credentials: true
という部分は、Cookieを使えるようにするために必要です。フロントエンド側
Cookieを使えるようにするため、axiosなどでリクエストする際に、
withCredentials: true
というオプションを含めます。axios({ method: 'HTTPメソッド', url: '送信先URL', withCredentials: true, // ここが必要 headers: { ヘッダー }, data: { 送信データ }, })ログイン認証に必要な設定等は以上ですが、このままでは CSRF の攻撃が筒抜けになってしまうので、以降で対策します。
CSRF対策
CSRF(クロスサイトリクエストフォージェリ)
CSRFについては、こちらの記事内の説明が特に分かりやすかったです。
Cookie の性質を利用した攻撃と Same Site Cookie の効果
- ユーザーが、あるサイト(ターゲット)にログインしたままにしている
- 別の罠サイト内に、そのターゲットサイトへのリクエスト(何かを変更するなど)を偽装して送る仕掛けがある
- ユーザーが罠サイトを訪れ、偽装リクエストが送られる
- ユーザーはターゲットサイトにログインしているため、そのサイトは、偽装リクエストを正常なリクエストとして受け取ってしまう
CSRFとCookie
CSRFは、ターゲットサイトに紐づくCookieにログイン状態が保存されていて、 リクエストがどこから送られたかに関係なく、Cookieが自動的に送信されてしまう ことが利用されています。
なぜ、自サイト内からのリクエストでのみ、Cookieが送信されるようになっていないのでしょうか?
もしそうだった場合、別のサイトからのリンクでターゲットサイトに訪れた時、ログインしていても、Cookieが送られないため、ログインしていないことになってしまいます。そしてリロードすると、自サイト内からのリクエストとしてCookieが送られ、ログインした状態になるという不自然な挙動になります。
RailsのCSRF対策
サーバーサイドレンダリングをするRailsアプリでは、デフォルトでフォームにトークンが仕込まれ、送られてきたリクエストのトークンをチェックすることでCSRF対策をしています。
参考: Rails セキュリティガイド
APIモードでは、フォームにトークンを仕込むことはできないので、別の方法で対策する必要があります。
実験
実際に、上で紹介したログイン機構を持つタスク管理アプリをサーバーに配置し、CSRFの実験をしてみました。
- 下記のような偽装フォームを持つhtmlを用意し、ローカルサーバーを立ち上げます。
偽装フォーム<form action="アプリURL" method="post"> <input type="hidden" name="task[content]" value="偽装タスク"> <input type="submit" value="送る"> </form>
- 実際のアプリにログインします。
- 同じブラウザで、偽装フォームのページを開き、「送る」を押します。
- アプリは
params[:task][:content]
を受け取って、「偽装タスク」という内容のタスクを作成してしまいました!axiosなどを使ってJSでリクエストを送る場合は、CORS設定で許可したオリジン以外は拒否されるのですが、この例のように、formからリクエストを送ると、簡単にCSRFができてしまうことが分かりました。
対策方法
これで完璧!今さら振り返る CSRF 対策と同一オリジンポリシーの基礎
こちらの記事で、主に3つの対策方法が紹介されていて、大変参考になりました。
今回はその中から、 固有の HTTP ヘッダを検証する方法 を用いて対策することにしました。
固有の HTTP ヘッダでCSRF対策
リクエストに
X-Requested-With: XMLHttpRequest
というヘッダを持たせて、バックエンド側では、そのヘッダを含まないリクエストを拒否するという方法です。この
X-Requested-With: XMLHttpRequest
は一般的に使われているというだけで、ヘッダ自体には意味がなく、 固有のHTTPヘッダ をつけることに意味があります。それは、HTML フォーム送信に関しては,一切の余分な HTTP ヘッダの付与が許可されていない。
という制約があるため、上で実験したようにフォームを偽装しても、
X-Requested-With
ヘッダをリクエストに含めることができないからです。したがって、このヘッダを持たない偽装リクエストはバックエンドで弾かれるようになります。
バックエンド側の実装
application_controller
に、ヘッダをチェックするbefore_action
を追加します。X-Requested-With
ヘッダの有無をチェックするrequest.xhr?
というメソッドを利用します。application_controller.rbclass ApplicationController < ActionController::API before_action :check_xhr_header private def check_xhr_header return if request.xhr? render json: { error: 'forbidden' }, status: :forbidden end endフロントエンド側の実装
axiosなどでリクエストする際に、
'X-Requested-With': 'XMLHttpRequest'
のヘッダを含めます。axios({ method: 'HTTPメソッド', url: '送信先URL', withCredentials: true, headers: { 'X-Requested-With': 'XMLHttpRequest' }, // 追加 data: { 送信データ }, })以上の実装で、上で行ったCSRF実験で、リクエストを送っても
forbidden
が返ってくるようになりました。まとめ
今回実装したログイン方法は、普通のRailsアプリと同じようにsessionを使えるので、初めてのWeb API開発としては、とっつきやすい方法だと思いました。
CSRFの対策は、必要最低限のレベルだと思います。実際のアプリ開発で、もっとセキュリティを高めるために、Cookieの
SameSite
属性を組み合わせるなどの方法を学びたいです。参考記事
SPAのログイン認証のベストプラクティスがわからなかったのでわりと網羅的に研究してみた〜JWT or Session どっち?〜
SPA + Rails API 構成におけるcookie + session認証
Cookie の性質を利用した攻撃と Same Site Cookie の効果
- 投稿日:2020-07-21T16:09:22+09:00
【Rails API + SPA】ログイン方法とCSRF対策例
記事の趣旨
モダンなWebアプリ開発の学習として、Vue.jsによるフロント、Rails APIによるバックエンドで実装するSPAを作りました。
その中で、SPAの様々なログイン実装方法や、それに必要な CSRF 対策を学びました。
最終的に
session + cookie
を利用するログインを実装した過程を記録したいと思います。ログイン実装方法の検討
SPAのログイン実装方法
下記の記事等を読んで、主に
JWT
を使う方法と、session
を使う方法があることが分かりました。こちらの記事では、様々な方法の良し悪しが網羅的にまとめられていました。
こちらの記事では、JWTとCookieが端的に比較されていました。
こちらの記事では、
JWT
をLocal Storage
に保存することの危険性が説かれています。重要なデータを保存する必要があるなら、常にサーバーサイドセッションを使うべきです。
Cookie + session認証
いずれの記事でも、
JWT
を単純にLocal Storage
に保存する方法は避けるべきだと言われています。
そこで、下記の記事の実装例を参考にさせていただき、session
をCookie
に保存する方法を採用することにしました。SPA + Rails API 構成におけるcookie + session認証
この方法は、普通のRailsアプリと同じように、
session
やcookies
を使うことができるシンプルさが良いと思いました。この場合、 CSRF 対策を自分で実装する必要があり、それについては後半に扱います。
ログイン機能実装
環境
ruby 2.7.1
rails 6.0.3
Rails APIモードでの立ち上げ
bundle exec rails new アプリ名 --api -TC -d mysql不要なファイルを省くためのオプションを色々付けていますが、
--api
が重要です。その他は、
-T
テストなし(RSpecを使う場合等)、-C
ActionCableなし、-d mysql
DBを指定、というオプションです。バックエンド側のログイン実装
バックエンド側のログイン機構は、Railsチュートリアルにあるような基本的な方法を使いました。
auth_controller.rb# フロントから { email: 'メールアドレス', password: 'パスワード' }のようなparamsが送られるものとする def create user = User.find_by(email: params[:email]) if user&.authenticate(params[:password]) session[:user_id] = user.id payload = { message: 'ログインしました。', name: user.name } else payload = { errors: ['メールアドレスまたはパスワードが正しくありません。'] } end render json: payload end認証に成功したら、
session
にuser_id
を保存しています。(フロントのページに表示するためにユーザー名も返しています。)ただし、この
session[:user_id] =
というメソッドは、APIモードのRailsでは無効になっているので、下記の設定が必要です。
config/application.rb
に下記の3行を追記参考: Rails の API モードでセッションを有効にする
application.rbmodule Myapp class Application < Rails::Application … config.middleware.use ActionDispatch::Cookies config.middleware.use ActionDispatch::Session::CookieStore config.middleware.use ActionDispatch::ContentSecurityPolicy::Middleware end endまた、「ログイン状態を保持する」がチェックされた際に、以下のように署名付きCookieに保存するようにしました。
cookies.permanent.signed[:user_id] = user.id cookies.permanent[:remember_token] = user.remember_token
cookies
を使う時には、下記のようにActionController::Cookies
をコントローラーにincludeする必要がありました。application_controller.rbclass ApplicationController < ActionController::API include ActionController::Cookies # 追加 … endこれで、普通のRailsアプリと同じように、安全なCookieに情報が保存され、リクエストで自動的にCookie情報が送られるようになります。
ログインが必要なアクションでは、下記のような
before_action
で、@current_user
をセットするようにしました。application_controller.rbclass ApplicationController < ActionController::API before_action :require_login private def require_login @current_user = User.find_by(id: session[:user_id]) return if @current_user render json: { error: 'unauthorized' }, status: :unauthorized end endCORSの設定
バックエンドとフロントエンドのオリジンが異なる場合は、axiosなどによるフロントからのリクエストを受け取るために、
CORS
の設定が必要です。例えば開発環境で、バックエンドを
localhost:3000
、フロントをlocalhost:8080
で動かしている場合などです。バックエンド側
gem 'rack-cors'
を導入して、config/initializers/cors.rb
に以下のように記述します。cors.rbRails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins '*' resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head], credentials: true end end最後の
credentials: true
という部分は、Cookieを使えるようにするために必要です。フロントエンド側
Cookieを使えるようにするため、axiosなどでリクエストする際に、
withCredentials: true
というオプションを含めます。axios({ method: 'HTTPメソッド', url: '送信先URL', withCredentials: true, // ここが必要 headers: { ヘッダー }, data: { 送信データ }, })ログイン認証に必要な設定等は以上ですが、このままでは CSRF の攻撃が筒抜けになってしまうので、以降で対策します。
CSRF対策
CSRF(クロスサイトリクエストフォージェリ)
CSRFについては、こちらの記事内の説明が特に分かりやすかったです。
Cookie の性質を利用した攻撃と Same Site Cookie の効果
- ユーザーが、あるサイト(ターゲット)にログインしたままにしている
- 別の罠サイト内に、そのターゲットサイトへのリクエスト(何かを変更するなど)を偽装して送る仕掛けがある
- ユーザーが罠サイトを訪れ、偽装リクエストが送られる
- ユーザーはターゲットサイトにログインしているため、そのサイトは、偽装リクエストを正常なリクエストとして受け取ってしまう
CSRFとCookie
CSRFは、ターゲットサイトに紐づくCookieにログイン状態が保存されていて、 リクエストがどこから送られたかに関係なく、Cookieが自動的に送信されてしまう ことが利用されています。
なぜ、自サイト内からのリクエストでのみ、Cookieが送信されるようになっていないのでしょうか?
もしそうだった場合、別のサイトからのリンクでターゲットサイトに訪れた時、ログインしていても、Cookieが送られないため、ログインしていないことになってしまいます。そしてリロードすると、自サイト内からのリクエストとしてCookieが送られ、ログインした状態になるという不自然な挙動になります。
RailsのCSRF対策
サーバーサイドレンダリングをするRailsアプリでは、デフォルトでフォームにトークンが仕込まれ、送られてきたリクエストのトークンをチェックすることでCSRF対策をしています。
参考: Rails セキュリティガイド
APIモードでは、フォームにトークンを仕込むことはできないので、別の方法で対策する必要があります。
実験
実際に、上で紹介したログイン機構を持つタスク管理アプリをサーバーに配置し、CSRFの実験をしてみました。
- 下記のような偽装フォームを持つhtmlを用意し、ローカルサーバーを立ち上げます。
偽装フォーム<form action="アプリURL" method="post"> <input type="hidden" name="task[content]" value="偽装タスク"> <input type="submit" value="送る"> </form>
- 実際のアプリにログインします。
- 同じブラウザで、偽装フォームのページを開き、「送る」を押します。
- アプリは
params[:task][:content]
を受け取って、「偽装タスク」という内容のタスクを作成してしまいました!axiosなどを使ってJSでリクエストを送る場合は、CORS設定で許可したオリジン以外は拒否されるのですが、この例のように、formからリクエストを送ると、簡単にCSRFができてしまうことが分かりました。
対策方法
これで完璧!今さら振り返る CSRF 対策と同一オリジンポリシーの基礎
こちらの記事で、主に3つの対策方法が紹介されていて、大変参考になりました。
今回はその中から、 固有の HTTP ヘッダを検証する方法 を用いて対策することにしました。
固有の HTTP ヘッダでCSRF対策
リクエストに
X-Requested-With: XMLHttpRequest
というヘッダを持たせて、バックエンド側では、そのヘッダを含まないリクエストを拒否するという方法です。この
X-Requested-With: XMLHttpRequest
は一般的に使われているというだけで、ヘッダ自体には意味がなく、 固有のHTTPヘッダ をつけることに意味があります。それは、HTML フォーム送信に関しては,一切の余分な HTTP ヘッダの付与が許可されていない。
という制約があるため、上で実験したようにフォームを偽装しても、
X-Requested-With
ヘッダをリクエストに含めることができないからです。したがって、このヘッダを持たない偽装リクエストはバックエンドで弾かれるようになります。
バックエンド側の実装
application_controller
に、ヘッダをチェックするbefore_action
を追加します。X-Requested-With
ヘッダの有無をチェックするrequest.xhr?
というメソッドを利用します。application_controller.rbclass ApplicationController < ActionController::API before_action :check_xhr_header private def check_xhr_header return if request.xhr? render json: { error: 'forbidden' }, status: :forbidden end endフロントエンド側の実装
axiosなどでリクエストする際に、
'X-Requested-With': 'XMLHttpRequest'
のヘッダを含めます。axios({ method: 'HTTPメソッド', url: '送信先URL', withCredentials: true, headers: { 'X-Requested-With': 'XMLHttpRequest' }, // 追加 data: { 送信データ }, })以上の実装で、上で行ったCSRF実験で、リクエストを送っても
forbidden
が返ってくるようになりました。まとめ
今回実装したログイン方法は、普通のRailsアプリと同じようにsessionを使えるので、初めてのWeb API開発としては、とっつきやすい方法だと思いました。
CSRFの対策は、必要最低限のレベルだと思います。実際のアプリ開発で、もっとセキュリティを高めるために、Cookieの
SameSite
属性を組み合わせるなどの方法を学びたいです。参考記事
SPAのログイン認証のベストプラクティスがわからなかったのでわりと網羅的に研究してみた〜JWT or Session どっち?〜
SPA + Rails API 構成におけるcookie + session認証
Cookie の性質を利用した攻撃と Same Site Cookie の効果
- 投稿日:2020-07-21T15:13:47+09:00
【備忘録】Missing host to link to! Please provide the :host parameterが出た
自作アプリRailsチュートリアル方式でログイン機構とパスワードリセット機構を実装した時の話です。
11章のaccount activation機能を無視して12章を進めていました。
具体的にリスト12.10まで進めたところで、試しに再設定用ページからメール送信ボタンをクリックしたところ、
タイトルのエラーが出てしまいました。Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true「ホストへのリンクがないよー」といっているのはわかりますが、そのホストとは何のことを指しているのか全くわからなかったためググったところ、テストメールに使うドメインホストが抜けてるとのことでした。
config/environments/test.rbconfig.action_mailer.default_url_options = { host: 'example.com' }(※実際のホスト名は自作アプリ向けに変えてます)
でいけました!ここ11章できちんと説明されているのに飛ばしたからハマってしまいました...。今回に限らず別のところでもよくハマるのですが、railsでアプリを作成するのに何が足りないとかなぜそのエラーがでてしまうのか、そのエラーは何を意味しているのか理解するのって難しいですね。
- 投稿日:2020-07-21T14:58:06+09:00
railsのmigration fileが外部キーでdown出来ない時の対処法
ruby 2.6.6
rails 6.0.3
catalina 10.15.5railsのmigration fileが外部キーでdown出来ない時の対処法がググってもあまり情報が出て来なかったので、後学のために。
要らないmigration fileを削除したく以下コマンド叩く。$ rails db:migrate:down VERTION=migration_file_idすると下記エラー
Mysql2::Error: Cannot drop table 'stores' referenced by a foreign key constraintエラーメッセージに外部キーと書いてあるので、まずは無効化。の前にdatabase選択
以下コマンド後、少し解説mysql> show databases;mysql> use database名;# 外部キー無効化 mysql> set foreign_key_checks = 0;mysql> show tables;mysql> select * from schema_migrations;mysql> select * from schema_migrations where version = 'migration_file_id';mysql> delete from schema_migrations where version = 'migration_file_id';mysql> select * from schema_migrations;$ rails db:migrate:status$ rails db:rollbackここまでコマンドのみ
以下解説
mysql> select * from schema_migrations; +----------------+ | version | +----------------+ | 20200714085643 | | 20200715061816 | +----------------+ 2 rows in set (0.00 sec)消したいのはこれ → 20200714085643
mysql> select * from schema_migrations where version = '20200714085643'; +----------------+ | version | +----------------+ | 20200714085643 | +----------------+ 1 row in set (0.00 sec)mysql> delete from schema_migrations where version = '20200714085643'; Query OK, 1 row affected (0.00 sec)mysql> select * from schema_migrations; +----------------+ | version | +----------------+ | 20200715061816 | +----------------+ 1 row in set (0.01 sec) # 20200714085643が消えた# 外部キー有効化 mysql> set foreign_key_checks = 1;# 最新のマイグレーションファイルのバージョンが指定データベースのschema_migrationsテーブルから削除される $ rails db:rollbackdownしたいファイルが複数ある場合は下記コマンド
# 5つのマイグレーションファイルをロールバックしたい時 $ rails db:rollback STEP=5これでrails db:migrate:statusすればdownになっているはず。
- 投稿日:2020-07-21T14:19:23+09:00
動画の再生時間(HH:MM:SS)を秒数にしたいし、その逆もしたい
- 投稿日:2020-07-21T14:01:14+09:00
docker-composeでアプリのコンテナが立ち上がらず困惑しました!というお話
ある日、開発途中のrailsアプリを立ち上げようと
docker-compose up
しようとしたら、
Cannot start service webpacker: OCI runtime create failed: container_linux.go:349: starting container process caused "exec: \"./bin/webpack-dev-server\": stat ./bin/webpack-dev-server: no such file or directory": unknownというエラーでて、
アプリのコンテナが、extend with 0
で落ちました。その後、エラー文を調べると以下記事を見つけ解決にいたりました!
docker-compose upしようとしたら「Cannot start service」 - Qiita結果からいうと
以前、gitignoreを編集しているときに
bin/以下ファイルを何を思ったか追加してて、ディレクトリごと削除していたんです。で、
docker-composeファイル内のcommandで指定している"bin/webpack-dev-server"
コマンドを実行しようとしても、そもそもdocker-composeでマウントするディレクトリにないですけど!
と注意を受けていた訳でした。原因がわかったので、解決
git log
でbinファイルを削除してしまったコミットIDを調べる。
git revert [コミットID]
コミットを取り消す
docker-compose up
無事起動を確認慢心ですね。
gitignoreの扱いには注意。
- 投稿日:2020-07-21T08:30:41+09:00
fields_forで子テーブルのデータを一気に作成する(テストも書いてます)[Rails][Rspec]
前提
あるショップのデータを作成するときに、そのショップへのコメントも一気に作成したいと思います。前提となる環境は以下の通りです。
- Rails 5.2.4.2
- rspec-rails 4.0.1
テーブルについて
テーブルの構造はこんな感じです。
親テーブル(shops)
shops id name 子テーブル(comments)
comments id shop_id content モデル
親モデル、子モデルのmodelファイルはそれぞれこのようになっています。
親モデル(Shop)
models/shop.rbclass Shop < ApplicationRecord has_many :comments accepts_nested_attributes_for :comments end親モデルにて
accepts_nested_attributes_for :comments
を指定し、子モデルのデータを受け入れるようにしておくことがポイントです。子モデル(Comment)
子モデルの記載には特に変わったことはありません。
models/comment.rbclass Comment < ApplicationRecord belongs_to :shop endコントローラー
親モデル側のコントローラーに、工夫すべき点がいくつかあります。
controllers/shops_controller.rbclass ShopsController < ApplicationController def new @shop = Shop.new @shop.comments.build # ←一つ目のポイント end def create @shop = Shop.create(shop_params) if @shop.save # 成功したときの処理 else # 成功しなかったときの処理 end end private def shop_params # ↓2つ目のポイント params.require(:shop).permit(:name, comments_attributes: [:content]) end end一つ目のポイントは、
new
アクションのところで、@shop.comments.build
と記載し、子テーブルであるcomments
のインスタンスをbuild
までしておくことです。こうすることで、親テーブルに紐づくcomments
を作成する準備ができます。二つ目のポイントは、
shop_params
のところで、comments_attributes: [:content]
と記載し、子テーブルの要素もpermitしておくことです。要素が二つ以上あるときには、comments_attributes: [:content, :attr2, :attr3]
のように記載します。ビュー
ビューには、このように記載します。
views/shops/new.rb= form_with model: @shop, local: true do |f| = f.label :name, "店名" = f.text_field :name = f.fields_for :comments, local: true do |comments_form| = comments_form.label :content, "ショップへのコメント" = comments_form.text_field :content = f.submit "送信"これで、子テーブルのデータも一気に作成されるはずです。
余談ですが、以前f.fields_for
のところをfields_for
と書いてしまい、子テーブルの要素がshop_params
として送られずはまったことがありました。。。
f.fields_for
と記載することで、親テーブルのフォームオブジェクトに紐づけることができます。テスト
テストもそれなりにコツが必要だったので記載します。ここでは、リクエストspecのみを書きます。
factory
親(shop)
spec/factories/shops.rbFactoryBot.define do factory :shop do name { "テストのお店" } trait :with_nested_instances do after( :create ) do |shop| create :comment, id: shop.id end end end end
trait
といのは、FactoryBotで、状況により少しだけ違うデータを用いたい場合の設定方法です。
after(:create)
はFactoryBotのコールバックです。この辺りはこちらの記事が参考になりました。子(comment)
spec/factories/comments.rbFactoryBot.define do factory :comment do content { "コメント" } end end子モデルのfactoryに特に変わった点はありません。
リクエストspec
spec/requests/shops_request_spec.rbRSpec.describe "Shops", type: :request do describe "POST /shops" do context "依存関係のあるテーブルのparamsが送信されているとき" do before do @comment_params = { comments_attributes: { "0": FactoryBot.attributes_for(:comment) } } @params_nested = { shop: FactoryBot.attributes_for(:shop).merge( @comment_params ) } end it 'リクエストが成功すること' do post shops_url, params: @params_nested expect(response.status).to eq 302 end it "ショップ情報とコメントが新規作成されること" do expect do post shops_url, params: @params_nested end.to change(Shop, :count).by(1) and change(Comment, :count).by(1) end end end end
"0"
あたりがちょっと特殊な書き方に見えますが、これは、実際のデータ送信時に送られるparams
に合わせた形になります。長かったですが、テストまで頑張ったおかげで、依存関係のあるデータの作成と大分仲良くなれました。参考記事
ここまでに参考にした記事はこちらです。
RSpecにおけるFactoryGirlの使い方まとめ
【Rails】複数のレコードを作成する。modelの関係性によって異なるform_for / fields_forの使い方
↓特にこの記事にはお世話になりました!!
[Rails]accepts_nested_attributes_forの使い方追記(発展学習?)
結局は自分の凡ミスだったのですが、同様にネストしたテーブルについて悩んていたときにスクールの同窓生コミュニティに投稿したら、以下のことを教えていただきました。
▼よくわからないがフォームがすっきり書けるらしい(そんな理解ですみません)
【Rails】FormObjectを使ってほしい▼次のフォームには使ってみたいと思っているgem, 1体多の構造を持つテーブルで、多数の子テーブルデータが一気に作成できるやつ
fields_forを使った子モデルへの複数レコード保存【cocoonが便利】では、今日もこれから仕事、頑張ります。アドバイスくださった皆様、ありがとうございました!!
- 投稿日:2020-07-21T00:59:44+09:00
HerokuでMongoDBのアドオンのmLabが終了するので、別のMongoBD無料サービスにマイグレーションした時のメモ
概要
Herokuで無料で利用可能なMongoDBのアドオンであるmLabのサービス終了に伴い、後継の無料利用可能なMongoDBサービス(MongoDB Atlas)に移行した際の方法のメモです。
画面操作手順が多く、一部の手順等を省略しておりますが、移行時の参考になればと思います。
背景
Heroku上で無料プランのみで、Railsで作成した趣味のWebサイト(※)を構築しており、
その中でMongoDBが無料利用可能なmLabアドオンを使用していました。
無料プランですと容量等、色々と制限がありますが、小規模で使用する分には問題ない形で使用できていました。ですが、7/11頃に下記のようなメールが届いていました。
要約すると、2020年の11月10日にmLabのMongoDBのアドオンは削除されて使用できなくなるという事となります。
詳細は下記に記載されていますが、mLab自体がMongoDB本体の会社(MongoDB Inc)に買収されたことに伴い、そちらのクラウドサービス(MongoDB Atlas)に統合されるためのようです。
参考幸いにもサービス停止に伴い、MongoDB Atlas側へ無料利用可能なプラン(512MBまで利用可能)へのマイグレーション方法が案内されており、今回はそちらのプランへのマイグレーションを実施した際の記録になります。
手順
MongoDBの対応バージョンの確認
mLabで動いていたMongoDBのバージョンは3.6ですが、MongoDB Atlasは4.2となるため、使用しているmongoidやrubyのバージョンに注意が必要です。
今回私の場合には4.2に対応しているバージョンを使用しており、幸いにも対応は不要でした。
バージョン等の対応に関しては下記が参考になります。
参考アカウント作成
https://www.mongodb.com/cloud/atlas
にアクセスして、Atlasアカウントを作成します。
(詳細は割愛しますが、メールアドレスやパスワードを入力し、メールのアクティベーションを実施してください。)
Clusterを作成
ログイン後、クラスターを作成します。
Clusters → Build a Clusterを選択します。Freeプランを選択します。
AWS, GCP, Azureやリージョン等の選択ができますのでお好みで選択してください。
Clusterが作成できれば完了です。
mLabアカウントとの接続
画面左上の歯車アイコンを選択します。
Connect to mLabといった項目があるので右側のボタンを選択します。
なお、mLabのアカウントが不明な場合には一度Herokuにログインして、mLabを使用しているアプリ内のAddonsのmLabを選択するとログインできるのでその後、上記のログイン画面でリロードするとログインできます。
ログイン完了後に、ダイアログが表示されるのでAutorizeで連携を許可します。
Atrasの画面に戻って、mLab Accountの画面が表示されれば連携が完了です。
マイグレーション設定
mLab Accountの画面でActions → ... → Configure Migrationを選択します。
Create or Select a projectを選択します。
プロジェクトを選択して Confirm ~ を選択します。
(デフォルトであればProject 0)
mLabから引き継いだアカウント情報などが表示されるので確認及び、importにチェックがついていることを確認して、 Import ~ を選択します。
Allow ~ のチェックボックスにチェックを入れて、右下のボタンを選択します。
作成したClusterを選択して、右下のボタンを選択します。
マイグレーション実行
Migrate DeploymentのConfirmを選択します。
Confirm And Continueを選択します。
チェックボックスにチェックを入れずに、Begin Test Runを選択します。
マイグレーションが無事に成功したら、Confirm Connectivityを選択します。
チェックボックスにチェックを入れて、Begin Migrationを選択します。
(下記では、アプリケーションを事前に止めるように注意しているのでheroku ps:scale web=0等のコマンドで予めアプリを止めた方が無難です。)
マイグレーションが始まるので待ちます。
しばらくすると完了するので、Start Using Atlasを選択します。MongoDBの接続URLの情報の雛形が表示されるので、コピーした上で
username及びpasswordに関してはherokuで使用していた値に置き換えて使用します。
herokuの環境変数を使用している場合には、下記のように環境変数を新しい値に書き換えます。
heroku config:set 環境変数名="上記の接続URL"アプリの起動
Herokuのアプリを止めている場合には、再立ち上げを行います。
MongoDBを使用している機能が動くこと確認してください。後始末
HerokuのmLabのアドオンを消します。
下記等を参考に予めバックアップを取ることが推奨されているので任意で実施してください。
https://docs.mlab.com/backups/#retention-policiesまた、mLabを削除すると自動で追加されたMONGODB_URI"や"MONGOLAB_URI等の環境変が削除されるとの事ですので、
heroku config等で環境変数の値を出力してバックアップしておく事が推奨されています。Herokuにログインを行い、Configure Add-onsを選択します。
mLabのDelete Add onを選択します。
確認ダイアログが表示されるのでアプリ名を入力して削除します。
以上でHeroku側のmLabの削除が完了しました。
念のためMongoDBを使用した機能が動くか確認してみましょう。まとめ
Heroku上で動くMongoDBのアドオンのmLabの終了に伴い、代替サービスとなるMongoDB Atlasへ移行時の方法のメモを紹介しました。
手順が多く大変でしたが、無事マイグレーションできたので一安心しました。
- 投稿日:2020-07-21T00:29:04+09:00
Vue.js Rails ページ遷移
前提
Rails 5.2
Ruby 2.5.1package.json{ "name": "app", "private": true, "dependencies": { "@rails/webpacker": "5.1.1", "vue": "^2.6.11", "vue-loader": "^15.9.3", "vue-template-compiler": "^2.6.11" }, "devDependencies": { "webpack-dev-server": "^3.11.0" } }ファイル構成
app |--javascript |--packs |--hello_vue.js |--app.vue本題
かなり初歩的ですが、とても躓いたため記述しました。
・VueRouterを導入
$ npm install vue-router・app.vueにを追記
app.vue<template> <div id="app"> <!-- 追記 --> <RouterView /> <p>{{ message }}</p> </div> </template> <script> export default { data: function() { return { message: "Hello Vue!", }; } }; </script>・app/javascript/packsの中にpagesフォルダー作成
・作成したpagesフォルダーのなかに、ファイルを2つ作成。pages/Hoge.vue<template> <div> <router-link to="/">Test</router-link> </div> </template>pages/test.vue<template> <div> <router-link to="/hoge">Hoge</router-link> <p>login</p> </div> </template>・packsフォルダーにrouter.jsを作成
router.jsimport Vue from "vue"; import VueRouter from "vue-router"; import Test from "./pages/Test.vue"; import Hoge from "./pages/Hoge.vue"; Vue.use(VueRouter); const routes = [ { path: "/", component: Test, }, { path: "/Hoge", component: Hoge, }, ]; const router = new VueRouter({ mode: "history", routes, }); export default router;・hello_vue.jsにvue-routerを追記
hello_vue.jsimport Vue from "vue"; import router from "./router"; import App from "../app.vue"; document.addEventListener("DOMContentLoaded", () => { const app = new Vue({ router, components: { App }, template: "<App />", render: (h) => h(App), }).$mount(); document.body.appendChild(app.$el); });ビルド後にページリロードして動作確認。
# bin/webpack-dev-server