- 投稿日:2020-05-16T23:28:17+09:00
dockerで作ったRailsプロジェクトをロリポップ!マネージドクラウドにあげるのに手こずった話
前置き
dockerを使ってRailsの開発を趣味でやっていた。
ひと段落ついたので、サーバーに公開しようと思ったがエラーが出てなかなかうまくいかなかった話です。エラーの内容
consoleArgumentError: Missing `secret_key_base` for 'production' environment, set this string with `rails credentials:edit`
解決法
consoleEDITOR=vim rails credentials:edit --environment production
dockerに入って上のコマンドを実行していたが、Vimが入っていなかったので、一見できてるようでできていなかった。
編集画面が出てこないのでおかしいとは思っていたが、ファイルはしっかり生成されているし大丈夫だと思っていたのが罠だった。あとはマネクラのプロジェクト管理画面で環境変数の設定をしておく。
ここに入れる値の内容は、/config/credentials/production.key
の内容をコピーして貼り付ける。その後以下のファイルを作った。
/config/deploy.rbappend :linked_files, 'config/credentials/production.key'これは
Railsがproduction.keyを参照するためのシンボリックリンクを貼る記述を追加する。
という感じらしい。
最後にSSHでマネクラにアクセスして以下のファイルを作っておいてからPushするとOK
consolevim user_command.sh
user_command.shtest -f ~/shared/.env && ln -s ~/shared/.env ./.env || true export RAILS_ENV=production bundle install --deployment --without development,test --path vendor/bundle bin/rails db:create bin/rails db:migrate最後に
なかなか解決せずに6時間くらい悩みました。
30分休憩してから再度取り掛かったらすぐに解決しました。
煮詰まったら一回休むってほんと大事お世話になった記事
Rails 6でCapistranoでデプロイする際のCredentials関連エラーに対処する
[Ruby on Rails]ロリポップ!マネージドクラウドでRailsアプリを稼働させるまで
- 投稿日:2020-05-16T23:07:00+09:00
rails 1対多 保存の方法 紛らわしいやつまとめ(メモ)
1対多のリレーションでも保存の方法の違いが紛らしいな
1番基本
- 1つ1つリレーションを作るパターン
## リレーション User 1 - * Post ## user.rb has_many :posts ## post.rb belongs_to :user ## controller def new @post = Post.new def create @post = Post.new(post_params) private def post_params params.require(:post).permit(:name, :body).merge(user_id: current_user.id) ## view = form_for @post do |form| = form.label :title = form.text_field :title = form.label :body = form.text_field :body = form.submit '送信' ## 要点 1つ1つ userとpostの繋がりを作っていくパターン複数一気に作るパターン
パターン1(子要素のデータがある場合)
- xx_idsを使う
## リレーション User 1 - * Skill ## user.rb has_many :skills ## skill.rb belongs_to :user ## 子要素のデータ存在する Skill id1 name: "筋トレ" id2 name: "イケメン" id3 name: "話術" ## controller def new @user = User.new private def post_params params.require(:post).permit(:name, skill_ids: []) ## view = form_for @user do |f| = f.label :name = f.text_field :name = f.fields_for :skill_ids do |skill| = skill.collection_select :name, Skill.all, :id, :name = f.submit "送信"パターン2(子要素のデータがない場合)
- accepts_nested_attributes_for使う
## リレーション User 1 - * Address ## user.rb has_many :addresses accepts_nested_attributes_for :addresses,allow_destroy: true ## address.rb - state - city belongs_to :user ## controller def new @user = User.new @user.addresses.build private def user_params params.require(:user).permit(:name, addresses_attributes: [:id, :state, :city]) end ## view = form_for @user do |form| .field = form.label :name = form.text_field :name .fidle = form.fields_for :addresses do |address| = address.label :state = address.text_field :state = address.label :city = address.text_field :city .fidle = form.fields_for :addresses do |address| = address.label :state = address.text_field :state = address.label :city = address.text_field :city
- 投稿日:2020-05-16T22:01:29+09:00
rails リレーション色々
- 1対多
ツイート機能
class User has_many :twees end class Twitter belongs_to user end ## schema users name:string tweets body:string user:refrences
- 自己参照型
like機能
class User < ApplicationRecord has_many :likers,foreign_key: "liker_id",class_name: "Like" has_many :likeds,foreign_key: "liked_id",class_name: "Like" end class Like < ApplicationRecord belongs_to :user, foreign_key: "liker_id", class_name: "User" belongs_to :user, foreign_key: "liked_id" , class_name: "User" end ## schema users name:string likes liker_id:integer liked_id:integer
- 多対多 && accepted_nested_for
投稿記事とカテゴリー
class Post has_many :post_categoris has_many :categories, through: :post_categoris accepts_nested_attributes_for :post_categoris, allow_destroy: true, reject_if: :all_blank end class Category has_many :post_categoris has_many :posts, through: :post_categoris end class PostCategory belongs_to :post belongs_to :category end ## schema posts - body categoris - name post_categories - post_id - category_id ## 実装イメージ どちらからも画面を作るときに、多対多になるのかも => カテゴリー主体の機能がなければ 1 - *でいいのではないか 投稿一覧機能 投稿1 カテゴリー1 カテゴリー2 投稿2 カテゴリー3 カテゴリー4 カテゴリー一覧機能 カテゴリー1 投稿数 カテゴリー2 投稿数
- ポリモーフィック(中々体に染み込まない)
Userが複数のタグをもつ
Postが複数のタグを持つ## 図 User 1 - * Tag(user.tags user.tags.first.tagable == user) Post 1 - * Tag(post.tags post.tags.first.tagable == post) class User has_many :tags ,as: :tagable end class Post has_many :tags ,as: :tagable end class Tag belongs_to :tagable, polymorphic: true end ## schema "posts" t.string "name" t.string "desc" "tags" t.string "name" t.string "tagable_type" t.integer "tagable_id" "users" t.string "name" t.integer "age"
- 投稿日:2020-05-16T21:47:36+09:00
Railsの基礎知識の備忘録
個人的に忘れやすいことをメモ的な意味で書きます。
ActiveRecordクラス
テーブルから情報を取得するため位必要なメソッドを兼ね備えたクラス。
メソッド一例メソッド 用途
●全てのデータを取得する
使用例)postテーブルに保存されているデータの全てを取得して@postsにインスタンス変数し、ビューファイルに使用できるようになる。
@post = Posts.all
↓複数のデータを表示しようとするとエラーが出る。
<h1>トップページ</h1>
<%= @posts.content %>
<%= @posts.created_at %>
↓eachメソッドを使って順番に処理させることで解決。
<h1>トップページ</h1>
<% @posts.each do |post| %>
<%= post.content %>
<%= post.created_at %>
<% end %>●テーブルのレコードの内、ある一つのデータを取得する
使用例)1番目のレコードのみ取得
@post = Post.find(1)
●クラスのインスタンス(レコード)を生成する
post.new
●クラスのインスタンス(レコード)を保存する
post.save
ヘルパーメソッド
ViewでHTMLタグを出現させたりテキストを加工するために予めソッドが用意されている。
●投稿ページなどにおけるフォームの実装
<%= form_tag('/posts', method: :post) do %>
<input type="text" name="content">
<input type="submit" value="投稿する">
<% end %>●その他のパーツ
type="text" 1行テキストボックス
type="password" パスワード入力ボックス
type="checkbox" 複数選択可
type="radio" ラジオボタン(一つだけ選択)
type="submit" 送信ボタン●リンクの実装
aタグの代わりに使用できる、リンクを仕込むためのヘルパーメソッド
<%= link_to '新規投稿', '/posts/new' %>
ストロングパラメーターとプライベートメソッド
Railsではセキュリティ上の概念などから、ストロングパラメーターという技術を用いてデータを保存する。
●ストロングパラメーターとは
指定したキーを持つパラメーターのみを受け取るようにするもの。以下に記述することで特定のキーしか受け取れないようにする仕組みを構築する。
def post_params
params.permit(:キー名, :キー名) # 受け取りたいキーを指定する
end●プライベートメソッド
クラス外から呼び出すことのできないメソッドで、ストロングパラメーターを記述したメソッドはprivate以下に記載して、プライベートメソッドとして扱う。
private
def post_params
params.permit(:content)
end★プライベートメソッドのメリット
1.classの外部から呼ばれたら困るメソッドの隔離
メソッドの中には、classの外部から呼び出されてしまうとエラーを起こすメソッドも存在します。そのような事態を事前に防ぐ事ができる。
2.可読性
classの外部から呼び出されるメソッドを探すときに、private以下の部分は目を通さなくて良くなります。また、繰り返し使用するメソッドもprivate以下に集約する事で、コードをシンプルにできる。
- 投稿日:2020-05-16T20:44:45+09:00
Rails セキュリティー
前提
本日学んだセキュリティーについて書いていきます。
本題
リダイレクトとファイル
セキュリティ上の脆弱性として検討したいのは、Webアプリケーションにおける「リダイレクトとファイル」。
リダイレクト
Webアプリケーションにおけるリダイレクトは、過小評価されがちなクラッキングツール。
攻撃者はこれを使ってユーザーを危険なWebサイトに送り込んだり、Webサイト自体に罠を仕掛けたりすることもできる。リダイレクト用のURL (の一部) を渡すことをユーザーに許すと、潜在的な脆弱性となる。
最もあからさまな攻撃方法としては、ユーザーを本物そっくりの偽Webサイトにリダイレクトすることが考えられる。
これは俗に「フィッシング(phishing)」や「釣り」などと呼ばれる攻撃手法。
具体的には、無害を装ったリンクを含むメールをユーザーに送りつけ、XSSを使ってそのリンクをWebアプリケーションに注入するか、リンクを外部サイトに配置する。
このリンクの冒頭部分はそのWebアプリケーションのURLなので、一見無害に見える。ファイルアップロード
ファイルがアップロードされたときに重要なファイルが上書きされることのないようにする。
また、メディアファイルの処理は非同期で行なう。多くのWebアプリケーションでは、ユーザーがファイルをアップロードできるようになっている。
ユーザーが選択/入力できるファイル名 (またはその一部) は必ずフィルタする。
攻撃者が危険なファイル名をわざと使ってサーバーのファイルを上書きしようとする可能性があるため。
ファイルが /var/www/uploads ディレクトリにアップロードされ、そのときにファイル名が「../../../etc/passwd」と入力されていると、重要なファイルが上書きされてしまう可能性がある。
言うまでもなく、Rubyインタプリタにそれだけの実行権限が与えられていなければ、そのような上書きは実行できない。
Webサーバー、データベースサーバーなどのプログラムは、比較的低い権限を持つUnixユーザーとして実行されているのが普通。さらにもう一つ注意。
ユーザーが入力したファイル名をフィルタするときに、ファイル名から危険な部分を取り除くアプローチを使わないこと。
Webアプリケーションがファイル名から「../」という文字を取り除くことができるとしても、今度は攻撃者が「....//」のようなその裏をかくパターンを使えば、やはり「../」という相対パスが通ってしまい、きりがない。
最も良いのは「ホワイトリスト」によるアプローチ。
これはファイル名が有効であるかどうか (指定された文字だけが使われているかどうか) をチェックするもの。
これは「ブラックリスト」アプローチと逆の手法であり、利用が許されてない文字を除去する。
ファイル名が無効の場合は、拒否するか、無効な文字を置き換えますが、取り除くわけではない。ファイルアップロードで実行可能なコードを送り込む
アップロードされたファイルに含まれるソースコードが特定のディレクトリに置かれると、ソースコードが実行可能になってしまう可能性がある。
Railsの/publicディレクトリがApacheのホームディレクトリになっている場合は、ここにアップロードファイルを置いてはいけない。広く使われているApache WebサーバーにはDocumentRootというオプションがある。
これはWebサイトのホームディレクトリであり、このディレクトリツリーに置かれているものはすべてWebサーバーによって取り扱われる。
そこに置かれているファイルの名前に特定の拡張子が与えられていると、それに対してリクエストが送信された時に実行されてしまうことがある。
実行される可能性のある拡張子は、たとえばPHPやCGIなど。
攻撃者が「file.cgi」というファイルをアップロードし、その中に危険なコードが仕込まれているとする。
このファイルを誰かがダウンロードすると、このコードが実行される。ApacheのDocumentRootがRailsの/publicディレクトリを指している場合、アップロードファイルをここに置かない。
少なくとも1階層上に保存する必要がある。ファイルのダウンロード
ユーザーが任意のファイルをダウンロードできる状態を作らないこと。
ファイルアップロード時にファイル名のフィルタが必要になるのと同様、ファイルのダウンロード時にもファイル名をフィルタする必要がある。
以下のsend_file()メソッドは、サーバーからクライアントにファイルを送信します。フィルタ処理されていないファイル名を使うと、ユーザーが任意のファイルをダウンロードできるようになってしまう。send_file('/var/www/uploads/' + params[:filename])「../../../etc/passwd」のようなファイル名を渡せば、サーバーのログイン情報をダウンロードできてしまう。
これに対するシンプルな対応策は、リクエストされたファイル名が、想定されているディレクトリの下にあるかどうかをチェックすること。その他に、ファイル名をデータベースに保存しておき、データベースのidをサーバーのディスク上に置く実際のファイル名の代りに使う方法も併用できる。
この方法も、アップロードファイルが実行される可能性を回避する方法として優れている。
attachment_fuプラグインでも同様の手法が採用されている。イントラネットAdminのセキュリティ
イントラネットおよび管理画面インターフェイスは、強い権限が許されているため、何かと攻撃の目標にされがち。
イントラネットおよび管理画面インターフェイスには、他よりも手厚いセキュリティ対策が必要ですが、現実には逆にむしろこれらの方がセキュリティ対策が薄いということがしばしばある。イントラネットや管理アプリケーションにとって最も脅威なのはXSSとCSRF。
XSS: 悪意のあるユーザーがイントラネットの外から入力したデータがWebアプリケーションで再表示されると、WebアプリケーションがXSS攻撃に対して脆弱になる。
ユーザー名、コメント、スパムレポート、注文フォームの住所のような情報すらXSS攻撃に使われることがある。管理画面やイントラネットで1箇所でもサニタイズ漏れがあれば、アプリケーション全体が脆弱になる。
想定される攻撃としては、管理者のcookieの盗み出し、管理者パスワードを盗み出すためのiframe注入、管理者権限奪取のためにブラウザのセキュリティホールを経由して邪悪なソフトウェアをインストールする、などが考えられる。CSRF: クロスサイトリクエストフォージェリ (Cross-Site Request Forgery) はクロスサイトリファレンスフォージェリ (XSRF: Cross-Site Reference Forgery) とも呼ばれ、非常に強力な攻撃手法。
この攻撃を受けると、管理者やイントラネットユーザーができることをすべて行えるようになってしまう。RailsのURLはかなり構造が素直であるため、オープンソースの管理画面を使っていると構造を容易に推測できてしまう。
攻撃者は、ありそうなIDとパスワードの組み合わせを総当りで試す危険なImageタグを送り込むだけで、数千件ものまぐれ当たりを獲得することもある。その他予防策
管理画面は、多くの場合次のような作りになっている。www.example.com/admin のようなURLに置かれ、Userモデルのadminフラグがセットされている場合に限り、ここにアクセスできる。
ユーザー入力が管理画面で再表示されると、管理者の権限でどんなデータでも削除/追加/編集できてしまう。常に最悪の事態を想定することは極めて重要。
「誰かが自分のcookieやユーザー情報を盗み出すことができたらどうなるか」。
管理画面にロール (role)を導入することで、攻撃者が行える操作の範囲を狭めることができる。
1人の管理者に全権を与えるのではなく、権限を複数管理者で分散する方法や、管理画面用に特別なログイン情報を別途設置するという方法もある。
一般ユーザーが登録されているUserモデルに管理者も登録し、管理者フラグで分類していると攻撃されやすいことから、これを避けるため。
極めて重要な操作では別途特殊なパスワードを要求する方法もある。管理者は、必ずしも世界中どこからでもそのWebアプリケーションにアクセスできる必要性はないはず。
送信元IPアドレスを一定の範囲に制限するという方法。request.remote_ipメソッドを使えばユーザーのIPアドレスをチェックできる。
この方法は攻撃に対する直接の防御にはならないが、検問としては非常に有効。
ただし、プロキシを用いて送信元IPアドレスを偽る方法がある。管理画面を特別なサブドメインに置き ( admin.application.com など)、さらに管理アプリケーションを独立させてユーザー管理を独自に行えるようにする。
このような構成にすることで、通常の www.application.com ドメインからの管理者cookieを盗み出すことは不可能。
ブラウザには同一生成元ポリシーがあるので www.application.com に注入されたXSSスクリプトからはadmin.application.comのcookieは読み出せず、逆についても同様に読み出し不可となる。ユーザー管理
認証 (authentication) と認可 (authorization) はほぼすべてのWebアプリケーションにおいて不可欠。
認証システムは自前で作るよりも、広く使われているプラグイン (訳注: 現在ならgem) を使うべき。
ただし、常に最新の状態にアップデートするようにする。Railsでは多数の認証用プラグインを利用できる。
人気の高いdeviseやauthlogicなどの優れたプラグインは、パスワードを平文ではなく常に暗号化した状態で保存する。
Rails 3.1では、同様の機能を持つビルトインのhas_secure_passwordメソッドを使える。新規ユーザーは必ずメール経由でアクティベーションコードを受け取り、メール内のリンク先でアカウントを有効にするようになっている。
アカウントが有効になると、データベース上のアクティベーションコードのカラムはNULLに設定される。
以下のようなURLをリクエストするユーザーは、データベースで見つかる最初に有効になったユーザーとしてWebサイトにログインできてしまう可能性がある。そしてそれがたまたま管理者である可能性もありえる。http://localhost:3006/user/activate http://localhost:3006/user/activate?id=一部のサーバーでは、params[:id]で参照されるパラメータidがnilになってしまっていることがあるため、上のURLが通用してしまう可能性がある。アクティベーション操作中にこのことが敵に突き止められるまでの流れは以下のとおり。
User.find_by_activation_code(params[:id])パラメータがnilの場合、以下のSQLが生成される。
SELECT * FROM users WHERE (users.activation_code IS NULL) LIMIT 1この結果、データベースに実在する最初のユーザーが検索で見つかり、結果が返されてログインされてしまう。
アカウントに対する総当たり攻撃
アカウントに対する総当たり攻撃 (Brute-force attack) とは、ログイン情報に対して試行錯誤を繰り返す攻撃。
エラーメッセージを具体的でない、より一般的なものにすることで回避可能 だが、CAPTCHA (相手がコンピュータでないことを確認するためのテスト) への情報入力の義務付けも必要。Webアプリケーション用のユーザー名リスト (名簿) は、パスワードへの総当たり攻撃に悪用される可能性がある。
パスワードがユーザー名と同じなど、単純極まりないパスワードを使っている人が驚くほど多いため、総当たり攻撃にこうした名簿が利用されやすい。
辞書に載っている言葉に数字を混ぜた程度の弱いパスワードが使われていることもよくある。
従って、名簿と辞書を使って総当り攻撃を行なう自動化プログラムがあれば、ものの数分でパスワードを見破られている。このような総当たり攻撃を少しでもかわすため、多くのWebアプリケーションではわざと具体的な情報を出さずに「ユーザー名またはパスワードが違います」という一般的なエラーメッセージを表示するようにしている。
ユーザー名とパスワードどちらが違っているのかという情報を表示しないことで、総当たり攻撃による推測を少しでも遅らせる。
「入力されたユーザー名は登録されていません」などという絶好の手がかりとなるメッセージを表示したら最後、攻撃者はすぐさまユーザー名リストを大量にかき集めて自動で巨大名簿を作成する。しかし、Webアプリケーションのデザイナーがおろそかにしがちなのは、いわゆる「パスワードを忘れた場合」ページ。
こうしたページではよく「入力されたユーザー名またはメールアドレスは登録されていません」という情報が表示される。
こうした情報を表示してしまうと、攻撃者がアカウントへの総当り攻撃に使う有効なユーザー名一覧を作成するのに利用されてしまう。これを少しでも緩和するには、「パスワードを忘れた場合」ページでも一般的なエラーメッセージを表示するようにする。
さらに特定のIPアドレスからのログインが一定回数以上失敗した場合には、CAPTCHAの入力をユーザーに義務付けるようにする。
もちろん、この程度では自動化された総当たり攻撃プログラムからの攻撃から完全に逃れることはできない。
こうしたプログラムは送信元IPアドレスを頻繁に変更するぐらいのことはやってのけるから。
しかしこの対策は攻撃に対するある程度の防御になることも確か。アカウントのハイジャック
多くのWebアプリケーションでは、ユーザーアカウントを簡単にハイジャックできてしまう。
パスワード
攻撃者が、盗み出されたユーザーセッションcookieを手に入れ、それによってWebアプリケーションが標的ユーザーとの間で共用可能になった状態を考えてみる場合。
パスワードが簡単に変更できる画面設計(古いパスワードの入力が不要)であれば、攻撃者は数クリックするだけでアカウントをハイジャックできてしまう。
あるいは、パスワード変更画面がCSRF攻撃に対して脆弱な作りになっている場合、攻撃者は標的ユーザーを別のWebページに誘い込み、CSRFを実行するように仕込まれたimgタグを踏ませて、標的ユーザーのWebパスワードを変更する。
対応策としては、パスワード変更フォームがCSRF攻撃に対して脆弱にならないようにすること。
同時に、ユーザーにパスワードを変更させる場合は、古いパスワードを必ず入力させること。メール
しかし攻撃者は、登録されているメールアドレスを変更することでアカウントを乗っ取ろうとする可能性もある。
攻撃者は、メールアドレス変更に成功すると「パスワードを忘れた場合」ページに移動し、攻撃者の新しいメールアドレスに変更通知メールを送信する。
システムによってはこのメールに新しいパスワードが記載されていることもある。
対応策は、メールアドレスを変更する場合にもパスワード入力を必須にすること。その他
Webアプリケーションの構成によっては、ユーザーアカウントをハイジャックする方法が他にも潜んでいる可能性がある。
多くの場合、CSRFとXSSが原因となる。
GMailのCSRF脆弱性で紹介されている例をとりあげる。
同記事の概念実証によると、この攻撃を受けた場合、標的ユーザーは攻撃者が支配するWebサイトに誘い込まれる。
そのサイトのImgタグには仕掛けがあり、GMailのフィルタ設定を変更するHTTP GETリクエストがそこから送信される。
この標的ユーザーがGMailにログインしていた場合、フィルタ設定が攻撃者によって変更され、この場合はすべてのメールが攻撃者に転送されるようになる。
この状態は、アカウント全体がハイジャックされたのと同じぐらいに有害。
対応策は、アプリケーションのロジックを見なおしてXSSやCSRF脆弱性を完全に排除すること。CAPTCHA
CAPTCHAとは、コンピュータによる自動応答でないことを確認するためのチャレンジ-レスポンス式テスト。
コメント入力欄などで、歪んだ画像に表示されている文字を入力させることで、入力者が自動スパムボットでないことを確認する場合によく使われる。
ネガティブCAPTCHAという手法を使えば、入力者に自分が人間であることを証明させるかわりに、ボットを罠にはめて正体を暴くことができる。CAPTCHAのAPIとしてはreCAPTCHAが有名。
これは古書から引用した単語を歪んだ画像として表示する。
初期のCAPTCHAでは背景を歪めたり文字を曲げたりしていましたが、後者は突破されたため、現在では文字の上に曲線も書き加えて強化している。
なお、reCAPTCHAは古書のデジタル化にも使える。
ReCAPTCHAはRailsのプラグインにもなっており、APIとして同じ名前が使われている。このAPIからは公開鍵と秘密鍵の2つの鍵を受け取る。
これらはRailsの環境に置く必要がある。
それにより、ビューでrecaptcha_tagsメソッドを、コントローラではverify_recaptchaメソッドをそれぞれ利用できる。
検証に失敗するとverify_recaptchaからfalseが返される。CAPTCHAの問題は、ユーザーエクスペリエンスを多少損ねること。
さらに、弱視など視力に問題のあるユーザーはCAPTCHAの歪んだ画像をうまく読めないこともある。
なおポジティブCAPTCHAは、ボットによるあらゆるフォーム自動送信を防ぐ優れた方法のひとつ。ほとんどのボットは、単にWebページをクロールしてフォームを見つけてはスパム文を入力するだけのお粗末なもの。
ネガティブCAPTCHAではこれを逆手に取り、フォームに「ハニーポット」フィールドを置いておく。
これは、CSSやJavaScriptを用いて人間には表示されないように設定されたダミーのフィールド。ネガティブCAPTCHAが効果を発揮するのはWebをクロールする自動ボットからの保護のみであり、重要なサイトに狙いを定めるボットを防ぐのには不向き。
しかしネガティブCAPTCHAとポジティブCAPTCHAをうまく組み合わせればパフォーマンスを改善できることがある。
たとえば「ハニーポット」フィールドに何か入力された(=ボットが検出された)場合はポジティブCAPTCHAの検証は不要になり、レスポンス処理の前にGoogle ReCapchaにHTTPSリクエストを送信せずに済む。JavaScriptやCSSを用いてハニーポットフィールドを人間から隠す方法。
ハニーポットフィールドを画面の外に追いやってユーザーから見えないようにする
フィールドを目に見えないくらい小さくしたり、背景と同じ色にしたりする
ハニーポットフィールドをあえて隠さず、「このフィールドには何も入力しないでください」と表示する
最もシンプルなネガティブCAPTCHAは、「ハニーポット」フィールドを1つ使う。
このフィールドはサーバー側でチェックする。
フィールドに何か書き込まれていれば、入力者はボットであると判定できる。
後はフォームの内容を無視するなり、通常通りメッセージを表示する(データベースには保存しない)などすればよい。
通常のメッセージをもっともらしく表示しておけば、ボットは書き込み失敗に気が付かないまま満足して次の獲物を探す。Ned Batchelderのブログ記事には、さらに洗練されたネガティブCAPTCHA手法がいくつか紹介されている。
現在のUTCタイムスタンプを含めたフィールドをフォームに含めておき、サーバー側でこのフィールドをチェックする。
フィールドの時刻が遠い過去や未来の時刻であれば、そのフォームは無効。
フィールド名をランダムに変更します
送信ボタンを含むあらゆる型の数だけハニーポットフィールドを複数用意。
この方法で防御できるのは自動ボットだけであり、狙いを定めて特別に仕立てられたボットは防げない。
つまり、ネガティブキャプチャはログインフォームの保護には必ずしも向いているとは限らない。ログ出力
パスワードをRailsのログに出力しないこと。
デフォルトでは、RailsのログにはWebアプリケーションへのリクエストがすべて出力される。
しかしログファイルにはログイン情報、クレジットカード番号などの情報が含まれていることがあるため、重大なセキュリティ問題の原因になることがある。
Webアプリケーションのセキュリティコンセプトを設計するときには、攻撃者がWebサーバーへのフルアクセスに成功してしまった場合のことも必ず考慮に含めておく必要がある。
パスワードや機密情報をログファイルに平文のまま出力してしまうと、データベース上でこれらの情報を暗号化する意味がなくなってしまう。
Railsアプリケーションの設定ファイル config.filter_parameters に特定のリクエストパラメータをログ出力時にフィルタする設定を追加できる。
フィルタされたパラメータはログ内で[FILTERED]という文字に置き換えられる。config.filter_parameters << :password指定したパラメータは正規表現の「部分マッチ」によって除外される。
Railsはデフォルトで:passwordを適切なイニシャライザ(initializers/filter_parameter_logging.rb)に追加し、アプリケーションの典型的なpasswordパラメータやpassword_confirmationパラメータに配慮する。正規表現
Rubyの正規表現で落とし穴になりやすいのは、より安全な\Aや\zがあることを知らずに危険な^や$を使ってしまうこと。
Rubyの正規表現では、文字列の冒頭や末尾にマッチさせる方法が他の言語と若干異なる。
このため、多くのRuby本やRails本でもこの点について間違った記載がある。
たとえば、URL形式になっているかどうかをざっくりと検証するために、以下のような単純な正規表現を使ったとする。/^https?:\/\/[^\n]+$/iこれは一部の言語では正常に動作する。
しかし、Rubyでは^や$は、入力全体の冒頭と末尾ではなく、「 行の」冒頭と末尾にマッチしてしまう。
従って、この場合以下のような毒入りURLはフィルタを通過してしまう。javascript:exploit_code();/* http://hi.com */上のURLがフィルタに引っかからないのは、入力の2行目にマッチしてしまうため。
従って、1行目と3行目にどんな文字列があってもフィルタを通過してしまう。
フィルタをすり抜けてしまったURLが、今度はビューの以下の箇所で表示されたとする。link_to "Homepage", @user.homepage表示されるリンクは一見無害に見えますが、クリックすると、攻撃者が送り込んだ邪悪なJavaScript関数を初めとするJavaScriptコードが実行されてしまう。
これらの正規表現に含まれる危険な^や$は、安全な\Aや\zに置き換える必要がある。
/\Ahttps?:\/\/[^\n]+\z/i^や$をうっかり使ってしまうミスが頻発したため、Railsのフォーマットバリデータ(validates_format_of) では、正規表現の冒頭の^や末尾の$に対して例外を発生するようになった。
めったにないと思われるが、\Aや\zの代りに^や$をどうしても使いたい場合は、:multilineオプションをtrueに設定することもできる。# この文字列のどの行にも"Meanwhile"という文字が含まれている必要がある validates :content, format: { with: /^Meanwhile$/, multiline: true }この機能は、フォーマットバリデータ利用時に起きがちなミスから保護するだけのものであり、それ以上のものではない点にご注意。
^や$はRubyでは 1つの行 に対してマッチし、文字列全体にはマッチしないということを開発者が十分理解しておくことが重要。権限昇格
パラメータが1つ変更されただけでも、ユーザーが不正な権限でアクセスできるようになってしまうことがある。
パラメータは、たとえどれほど難読化し、隠蔽したとしても、変更される可能性が常にあることを肝に銘じる。改ざんされる可能性が高いパラメータといえばid。http://www.domain.com/project/1の1がid。
このidはコントローラのparamsを経由して取得できる。
コントローラ内では多くの場合、次のようなコードが使われている可能性がある。@project = Project.find(params[:id])このコードで問題がないWebアプリケーションもあるにはあるが、そのユーザーがすべてのビューを参照する権限を持っていない場合には問題となる。
このユーザーがURLのidを42に変更し、本来のidでは表示できないページを表示できてしまうため。
このようなことにならないよう、ユーザーのアクセス権も必ずクエリに含める。@project = @current_user.projects.find(params[:id])Webアプリケーションによっては、ユーザーが改ざん可能なパラメータが他にも潜んでいる可能性がある。
要するに、安全確認が終わっていないユーザー入力が安全である可能性はゼロであり、ユーザーから送信されるいかなるパラメータであっても、何らかの操作が加えられている可能性が常にあるということ。難読化とJavaScriptによる検証のセキュリティだけでお茶を濁してはいけない。
ブラウザのWeb Developer Toolbarを使えば、フォームの隠しフィールドを見つけて変更することもできる。
JavaScriptを使ってユーザーの入力データを検証することはできても、攻撃者が想定外の値を与えて邪悪なリクエストを送信することは阻止しようがない。
Mozilla Firefox用のFirebugアドオンを使えば、すべてのリクエストをログに記録して、リクエストを繰り返し送信することも、リクエストを変更することもできてしまう。
さらに、JavaScriptによる検証はブラウザのJavaScriptをオフにするだけで簡単にバイパスできてしまう。
さらに、クライアントやインターネットのあらゆるリクエストやレスポンスを密かに傍受するプロキシがクライアント側に潜んでいる可能性すらある。インジェクション
インジェクション (注入) とは、Webアプリケーションに邪悪なコードやパラメータを導入して、そのときのセキュリティ権限で実行させること。
XSS (クロスサイトスクリプティング) やSQLインジェクションはインジェクションの顕著な例。インジェクションによって注入されるコードやパラメータは、あるコンテキストではきわめて有害であっても、それ以外のほとんどのコンテキストでは無害。
その意味で、インジェクションは非常にトリッキーであると言える。
ここでいうコンテキストとは、スクリプティング、クエリ、プログラミング言語、シェル、RubyやRailsのメソッドなどがある。ホワイトリスト方式とブラックブラックリスト方式
通常、サニタイズや保護や検証では、ブラックリスト方式よりもホワイトリスト方式が望ましい方法。
ブラックリストに使われるのは、有害なメールアドレス、publicでないアクション、邪悪なHTMLタグなど。
ホワイトリストはこれと真逆で、有害ではないメールアドレス、publicなアクション、無害なHTMLタグなどがホワイトリストになる。
スパムフィルタなど、対象によってはホワイトリストを作成しようがないこともあるが、基本的にホワイトリスト方式を使う。セキュリティに関連するbefore_actionでは、except: [...]ではなくonly: [...]を使う。
なぜなら将来コントローラにアクションを追加するときにセキュリティチェックを忘れずに済むため。
クロスサイトスクリプティング (XSS) 対策として」という文字列の攻撃能力は失われていない。
だからこそ、ホワイトリストを用いるフィルタリングをおすすめする。
ホワイトリストによるフィルタは、Rails 2でアップデートされたsanitize()メソッドで使われている。tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p) s = sanitize(user_input, tags: tags, attributes: %w(href title))この方法なら指定されたタグのみが許可されるため、あらゆる攻撃方法や邪悪なタグに対してフィルタが健全に機能する。
第2段階として、Webアプリケーションからの出力をもれなくエスケープすることが優れた対策。これは特に、ユーザー入力の段階でフィルタされなかった文字列がWeb画面に再表示されてしまうようなことがあった場合に有効。escapeHTML() (または別名のh()) メソッドを用いて、HTML入力文字「&」「"」「<」「>」を、無害なHTML表現形式(&、"、<、>) に置き換える。
攻撃の難読化とエンコーディングインジェクション
従来のネットワークトラフィックは西欧文化圏のアルファベットがほとんどであったが、それ以外の言語を伝えるためにUnicodeなどの新しいエンコード方式が使われるようになってきた。
しかしこれはWebアプリケーションにとっては新たな脅威となるかもしれない。
異なるコードでエンコードされた中に、ブラウザでは処理可能だがサーバーでは処理されないような悪意のあるコードが潜んでいるかもしれないため。UTF-8による攻撃方法の例。<IMG SRC=javascript:a lert('XSS')>上の例を実行するとメッセージボックスが表示される。
なお、これは上のsanitize()フィルタで認識される。
Hackvertorは文字列の難読化とエンコードを行なう優れたツールであり、「敵を知る」のに最適。
Railsのsanitize()メソッドは、このようなエンコーディング攻撃をかわす。CSSインジェクション
CSSインジェクションは実際にはJavaScriptのインジェクション。
MySpace Samyワームは、攻撃者であるSamyのプロファイルページを開くだけで自動的にSamyに友達リクエストを送信するというもの。
MySpaceでは多くのタグをブロックしていたが、CSSについては禁止していなかったため、ワームの作者はCSSに以下のようなJavaScriptを仕込んだ。
<div style="background:url('javascript:alert(1)')">ここでスクリプトの正味の部分(ペイロード)はstyle属性に置かれる。
一重引用符と二重引用符が既に両方使われているので、このペイロードでは引用符を使えない。
しかしJavaScriptにはどんな文字列もコードとして実行できてしまう便利なeval()関数がある。
この関数は強力だが危険。<div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">eval()関数はブラックリスト方式の入力フィルタを実装した開発者にとってはまさに悪夢。
この関数を使われてしまうと、たとえば以下のように「innerHTML」という単語をstyle属性に隠しておくことができてしまうため。alert(eval('document.body.inne' + 'rHTML'));次は、MySpaceは"javascript"という単語をフィルタしていたにもかかわらず、「javascript」と書くことでこのフィルタを突破された。
<div id="mycode" expr="alert('hah!')" style="background:url('java script:eval(document.all.mycode.expr)')">さらに次は、ワームの作者がCSRFセキュリティトークンを利用していた。
ワームの作者は、ユーザーが追加される直前にページに送信されたGETリクエストの結果を解析してCSRFトークンを手に入れていた。最終的に4KBサイズのワームができあがり、作者は自分のプロファイルページにこれを注入。
moz-bindingというCSSプロパティは、FirefoxなどのGeckoベースのブラウザではCSS経由でJavaScriptを注入する手段に使われる可能性があることが判明。
対応策
ブラックリストによる完璧なフィルタは決して作れません。
しかしWebアプリケーションでカスタムCSSを使える機能はめったにないため、これを効果的にフィルタできるホワイトリストCSSフィルタを見つけるのは難しい。
Webアプリケーションの色や画像をカスタマイズできるようにしたいのであれば、ユーザーに色や画像を選ばせ、Webアプリケーションの側でCSSをビルドするようにする。
ユーザーがCSSを直接カスタマイズできるような作りにはしない。
どうしても必要であれば、ホワイトリストベースのCSSフィルタとしてRailsのsanitize()メソッドを使う。テキスタイルインジェクション(Textile Injection)
セキュリティ上の理由からHTML以外のテキストフォーマット機能を提供するのであれば、何らかのマークアップ言語を採用し、それをサーバー側でHTMLに変換するようにする。
RedClothはRuby用に開発されたマークアップ言語の一種だが、注意して使わないとXSSに対しても脆弱になる。対応策
RedClothは必ずホワイトリストフィルタと組み合わせて使う。
Ajaxインクジェクション
Ajaxでも、通常のWebアプリケーション開発上で必要となるセキュリティ上の注意と同様の注意が必要。
1つ例外がある。
ページヘの出力は、アクションがビューをレンダリングしない場合であってもエスケープが必要。in_place_editorプラグインや、ビューをレンダリングする代りに文字列を返すようなアクションを使う場合は、アクションで返される値を確実にエスケープする必要がある。
もしXSSで汚染された文字列が戻り値に含まれていると、ブラウザで表示されたときに悪意のあるコードが実行されてしまう。
入力値はすべてh()メソッドでエスケープする。コマンドラインインクジェクション
ユーザーが入力したデータをコマンドラインのオプションに使う場合は十分に注意が必要。
Webアプリケーションが背後のOSコマンドを実行しなければならない場合、Rubyにはexec(コマンド)、syscall(コマンド)、system(コマンド)、そしてバッククォート記法という方法が用意されている。
特に、これらのコマンド全体または一部を入力できる可能性に注意が必要。
ほとんどのシェルでは、コマンドにセミコロン;や垂直バー|を追加して別のコマンドを簡単に結合できてしまう。対応策は、コマンドラインのパラメータを安全に渡せるsystem(コマンド, パラメータ)メソッドを使うこと。
system("/bin/echo","hello; rm *") # "hello; rm *"を実行してもファイルは削除されないヘッダーインクジェクション
HTTPヘッダは動的に生成されるものであり、特定の状況ではヘッダにユーザー入力が注入されることがある。
これを使って、にせのリダイレクト、XSS、HTTPレスポンス分割攻撃が行われる可能性がある。HTTPリクエストヘッダで使われているフィールドの中にはReferer、User-Agent (クライアント側ソフトウェア)、Cookieフィールドがありまる。
Responseヘッダーには、たとえばステータスコード、Cookieフィールド、Locationフィールド (リダイレクト先を表す) がある。
これらのフィールド情報はユーザー側から提供されるものであり、さほど手間をかけずに操作できてしまう。
これらのフィールドもエスケープする。
エスケープが必要になるのは、管理画面でUser-Agentヘッダを表示する場合などが考えられる。さらに、ユーザー入力の一部を取り入れたレスポンスヘッダを生成する場合は、何が行われているのかを正確に把握することが重要。
たとえば、ユーザーを特定のページにリダイレクトしてから元のページに戻したいとする。
このとき、refererフィールドをフォームに導入して、指定のアドレスにリダイレクトしたとする。redirect_to params[:referer]このとき、Railsはその文字列をLocationヘッダフィールドに入れて302(リダイレクト)ステータスをブラウザに送信する。
悪意のあるユーザーがこのとき最初に行なうのは、以下のような操作。http://www.yourapplication.com/controller/action?referer=http://www.malicious.tldRails 2.1.2より前のバージョン(およびRuby)に含まれるバグが原因で、ハッカーが以下のように任意のヘッダを注入できてしまう可能性がある。
http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld%0d%0aX-Header:+Hi! http://www.yourapplication.com/controller/action?referer=path/at/your/app%0d%0aLocation:+http://www.malicious.tld上のURLにおける%0d%0aは\r\nがURLエンコードされたものであり、RubyにおけるCRLF文字。
2番目の例では2つ目のLocationヘッダーフィールドが1つ目のものを上書きするため、以下のようなHTTPヘッダーが生成される。HTTP/1.1 302 Moved Temporarily (...) Location: http://www.malicious.tldヘッダーインジェクションにおける攻撃方法とは、ヘッダーにCRLF文字を注入すること。
攻撃者は偽のリダイレクトでどんなことができてしまうのか。
攻撃者は、ユーザーをフィッシングサイトにリダイレクトし(フィッシングサイトの見た目は本物そっくりに作っておきます)、ユーザーを再度ログインさせてそのログイン情報を攻撃者に送信する可能性がある。
あるいは、フィッシングサイトからブラウザのセキュリティホールを経由して邪悪なソフトウェアを注入するかもしれない。
Rails 2.1.2ではredirect_toメソッドのLocationフィールドからこれらの文字をエスケープするようになった。
ユーザー入力を用いて通常以外のヘッダーフィールドを作成する場合には、CRLFのエスケープを必ず自分で実装する。レスポンス分割
ヘッダーインジェクションが実行可能になってしまっている場合、レスポンス分割(response splitting)攻撃も同様に実行可能になっている可能性がある。
HTTPのヘッダーブロックの後ろには2つのCRLFが置かれてヘッダーブロックの終了を示し、その後ろに実際のデータ(通常はHTML)が置かれる。
レスポンス分割とは、ヘッダーフィールドに2つのCRLFを注入し、その後ろに邪悪なHTMLを配置するという手法。
このときのレスポンスは以下のようになります。HTTP/1.1 302 Found [最初は通常の302レスポンス] Date: Tue, 12 Apr 2005 22:09:07 GMT Location: Content-Type: text/html HTTP/1.1 200 OK [ここより下は攻撃者によって作成された次の新しいレスポンス] Content-Type: text/html <html><font color=red>hey</font></html> [任意の邪悪な入力が Keep-Alive: timeout=15, max=100 リダイレクト先のページとして表示される] Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: text/html特定の条件下で、この邪悪なHTMLが標的ユーザーのブラウザで表示されることがある。
ただし、おそらくKeep-Alive接続が有効になっていないとこの攻撃は効かない。
多くのブラウザはワンタイム接続を使っているため。
かといって、Keep-Aliveが無効になっていることを当てにするわけにはいかない。
これはいずれにしろ重大なバグであり、ヘッダーインジェクションとレスポンス分割の可能性を排除するため、Railsを2.0.5または2.1.2にアップグレードする必要がある。安全ではないクエリ生成
Rackがクエリパラメータを解析(parse)する方法とActive Recordがパラメータを解釈する方法の組み合わせに問題があり、where句がIS NULLのデータベースクエリを本来の意図に反して生成することが可能になってしまう。
(CVE-2012-2660、CVE-2012-2694 および CVE-2013-0155) のセキュリティ問題への対応として、Railsの動作をデフォルトでセキュアにするためにdeep_mungeメソッドが導入された。以下は、deep_mungeが実行されなかった場合に攻撃者に利用される可能性のある脆弱なコードの例。
unless params[:token].nil? user = User.find_by_token(params[:token]) user.reset_password! endparams[:token]が[nil]、[nil, nil, ...]、['foo', nil]のいずれかの場合、nilチェックをパスするにもかかわらず、where句がIS NULLまたはIN ('foo', NULL)になってSQLクエリに追加されてしまう。
Railsをデフォルトでセキュアにするために、deep_mungeメソッドは一部の値をnilに置き換える。
リクエストで送信されたJSONベースのパラメータがどのように見えるかを以下に表示。JSON Parameters { "person": null } { :person => nil } { "person": [] } { :person => [] } { "person": [null] } { :person => [] } { "person": [null, null, ...] } { :person => [] } { "person": ["foo", null] } { :person => ["foo"] }リスクと取扱い上の注意を十分理解している場合に限り、deep_mungeをオフにしてアプリケーションを従来の動作に戻すことができる。
config.action_dispatch.perform_deep_munge = falseデフォルトのヘッダー
Railsアプリケーションから受け取るすべてのHTTPレスポンスには、以下のセキュリティヘッダーがデフォルトで含まれている。
config.action_dispatch.default_headers = { 'X-Frame-Options' => 'SAMEORIGIN', 'X-XSS-Protection' => '1; mode=block', 'X-Content-Type-Options' => 'nosniff', 'X-Download-Options' => 'noopen', 'X-Permitted-Cross-Domain-Policies' => 'none', 'Referrer-Policy' => 'strict-origin-when-cross-origin' }デフォルトのヘッダー設定はconfig/application.rbで変更できる。
config.action_dispatch.default_headers = { 'Header-Name' => 'Header-Value', 'X-Frame-Options' => 'DENY' }以下のようにヘッダーを除去することもできる。
config.action_dispatch.default_headers.clearよく使われるヘッダーのリストを以下に示す。
X-Frame-Options: Railsではデフォルトで'SAMEORIGIN'が指定される。
このヘッダーは、同一ドメインでのフレーミングを許可。
'DENY'を指定するとすべてのフレーミングが不許可になる。
すべてのWebサイトについてフレーミングを許可するには'ALLOWALL'を指定。
X-XSS-Protection: Railsではデフォルトで'1; mode=block'が指定される。
XSS攻撃が検出された場合は、XSS Auditorとブロックページを使う。
XSS Auditorをオフにしたい場合は'0;'を指定します(レスポンスがリクエストパラメータからのスクリプトを含んでいる場合に便利)。
X-Content-Type-Options: 'nosniff'はRailsではデフォルト。
このヘッダーは、ブラウザがファイルのMIMEタイプを推測しないようにする。
X-Content-Security-Policy: このヘッダーは、コンテンツタイプを読み込む元のサイトを制御するための強力なメカニズム。
Access-Control-Allow-Origin: このヘッダーは、同一生成元ポリシーのバイパスとクロスオリジン(cross-origin)リクエストをサイトごとに許可する。
Strict-Transport-Security: このヘッダーは、ブラウザからサイトへの接続をセキュアなものに限って許可するかどうかを指定。Content Security Policy(CSP)
Railsでは、アプリケーションでContent Security Policy(CSP)を設定するためのDSLが提供されている。
グローバルなデフォルトポリシーを設定し、それをリソースごとにオーバーライドすることも、lambdaを用いてリクエストごとに値をヘッダーに注入することもできる(マルチテナントのアプリケーションにおけるアカウントのサブドメインなど)。以下はグローバルなポリシーの例。
# config/initializers/content_security_policy.rb Rails.application.config.content_security_policy do |policy| policy.default_src :self, :https policy.font_src :self, :https, :data policy.img_src :self, :https, :data policy.object_src :none policy.script_src :self, :https policy.style_src :self, :https # 違反レポートの対象URIを指定する policy.report_uri "/csp-violation-report-endpoint" end以下はコントローラでオーバーライドするコード例。
# ポリシーをインラインでオーバーライドする場合 class PostsController < ApplicationController content_security_policy do |p| p.upgrade_insecure_requests true end end # リテラル値を使う場合 class PostsController < ApplicationController content_security_policy do |p| p.base_uri "https://www.example.com" end end # 静的値と動的値を両方使う場合 class PostsController < ApplicationController content_security_policy do |p| p.base_uri :self, -> { "https://#{current_user.domain}.example.com" } end end # グローバルCSPをオフにする場合 class LegacyPagesController < ApplicationController content_security_policy false, only: :index endレガシーなコンテンツを移行するときにコンテンツの違反だけをレポートしたい場合は、設定でcontent_security_policy_report_only属性を用いてContent-Security-Policy-Report-Onlyを設定。
# config/initializers/content_security_policy.rb Rails.application.config.content_security_policy_report_only = true # コントローラでオーバーライドする場合 class PostsController < ApplicationController content_security_policy_report_only only: :index end以下の方法でnonceの自動生成を有効にできる。
# config/initializers/content_security_policy.rb Rails.application.config.content_security_policy do |policy| policy.script_src :self, :https end Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }後は以下のようにhtml_optionsの中でnonce: trueを渡せばnonce値が自動的に追加される。
<%= javascript_tag nonce: true do -%> alert('Hello, World!'); <% end -%>javascript_include_tagでも同じことができる。
<%= javascript_include_tag "script", nonce: true %>セッションごとにインライン
- 投稿日:2020-05-16T17:13:37+09:00
【Rails】iframeタグを使って外部コンテンツが埋め込めない
iframeで外部ページを埋め込む時の問題
ページに組み込んだiframeが表示されない問題についての対処法です。
ブラウザのコンソールをチェックしてみると、以下のエラーが表示されている場合があるかもしれません。errorRefused to display xxxxx in a frame because it set 'X-Frame-Options' to SAMEORIGIN'.これはRailsでデフォルトのセキュリティヘッダーのX-Frame-Options が
SAMEORIGIN
になっているからです。X-Frame-Options
X-Frame-Options は HTTP のレスポンスヘッダーで、ブラウザーがページを frame, iframe, embed, object の中に表示することを許可するかどうかを示すために使用されます。サイトはコンテンツが他のサイトに埋め込まれないよう保証することで、クリックジャッキング攻撃を防ぐために使用することができます。
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
SAMEORIGIN
ではオリジンページに含まれているページのみフレームで表示することが可能という意味です。DENY
にするとフレームにページが読み込まれないようになります。
ALLOW-FROM (uri)
を使うと、指定されたuriのみがフレーム内で表示することが可能になります。response.headers['X-Frame-Options'] = 'DENY' response.headers['X-Frame-Options'] = 'ALLOW-FROM https://example.com'しかし
ALLOW-FROM
はブラウザ側の未サポートやバグがあり、非推奨となっています。chromeやFireFoxなどのメインブラウザで無効なので使わない方が良さそうです。
またALLOWALL
というディレクティブがあるという記事もあります。(参照:Rails4: Allow your site to be iframed by another site. )response.headers['X-Frame-Options'] = 'ALLOWALL'しかし試したところ無効でした。MDNで確認したところ有効なディレクティブは現在
SAMEORIGIN
とDENY
だけみたいです。X-Frame-Optionsのまとめ
- X-Frame-Options
- DENY - ページをフレーム内に表示できなくなる
- SAMEORIGIN - ページのドメインとフレームのドメインが同じ場合にのみ、ページがフレーム内で表示される。
- ALLOW-FROM http://example.com (非推奨) - 指定されたURIのページのみ、フレーム内で表示される。
- ALLOWALL (非推奨/無効) - どのページもフレーム内で表示される。
Content-Security-Policy (CSP)
コンテンツセキュリティポリシー (CSP) は、クロスサイトスクリプティング (XSS) やデータインジェクション攻撃などのような、特定の種類の攻撃を検知し、影響を軽減するために追加できるセキュリティレイヤーです。これらの攻撃はデータの窃取からサイトの改ざん、マルウェアの拡散に至るまで、様々な目的に用いられます。
CSP を有効にするには、ウェブサーバーから Content-Security-Policy HTTP ヘッダーを返すように設定する必要があります (X-Content-Security-Policy ヘッダーに関する記述が時々ありますが、これは古いバージョンのものであり、今日このヘッダーを指定する必要はありません)。
X-Frame-Optionsの代替方法
X-Frame-Optionsで
ALLOW-FROM uri
やALLOWALL
を使えないので、その代替としてContent-Security-Policy(CSP)を使うことができそうです。CSPのナビゲーションディレクティブを用いると埋め込み先を管理することができます。
frame-ancestors
というディレクティブでによって埋め込み先のドメインを限定化し、そのドメインでのみの表示を許可することができます。response.headers['Content-Security-Policy'] = "frame-ancestors https://example.com"また例えばstaging環境を許容したいという場合は以下のような感じで書くことができます。
url = Rails.env.production? ? "https://example.com" : "https://staging.example.com" response.headers['Content-Security-Policy'] = "frame-ancestors 'self' #{url}"これはこのように書き換えることも可能です。
response.headers['Content-Security-Policy'] = "frame-ancestors 'self' https://*.example.com"よって埋め込み元のcontrollerには以下のように記述すればOKです。
blogs_controller.rbclass BlogsController < ApplicationController after_action :allow_iframe, only: [:iframe] def iframe @blog = Blog.new end def allow_iframe response.headers['Content-Security-Policy'] = "frame-ancestors 'self' https://*.example.com" end endCSP frame-ancestorsの書き方まとめ
- 埋め込み先を自身のドメイン (サブドメインを除く) に限定させたい場合。
例えば
https://example.com/blogs
ページにhttps://example.com/articles
ページを埋め込みたい時、親がhttps://example.com
で同じなのでblogs(埋め込み元)のコントローラーに以下の処理を書けば親が共通のページはフレームで表示することができます。blogs_controller.rbresponse.headers['Content-Security-Policy'] = "frame-ancestors 'self'"
- 自身のドメインとそのサブドメインへの埋め込みを許可したい場合。
例えば
https://example.com/blogs
と同じ親を持つhttps://example.com/articles
ページと、サブドメインhttps://staging.example.com
を親に持つhttps://staging.example.com/articles
ページに埋め込みたい時、blogs(埋め込み元)のコントローラーに以下の処理を書けば埋め込み先のフレーム内に表示されます。blogs_controller.rbresponse.headers['Content-Security-Policy'] = "frame-ancestors 'self' https://*.example.com"
'self'
はメインドメインを親に持つページ内への埋め込みを許可するという意味であり、https://*.example.com
はサブドメイン(https://staging.example.com
)を親とするページ内への埋め込みを許可するという意味になります。CSPのまとめ
- CSP
- frame-ancestors -
<frame>
,<iframe>
,<object>
,<embed>
,<applet>
などの要素によって埋め込まれるページの親を指定し、埋め込みを有効にする。X-Frame-Optionsのエラーは出るものの変更する必要はなく(というか使えるディレクティブがなく)、その代わりにCSPを指定する必要があったというのが学びでした。
- 投稿日:2020-05-16T17:07:03+09:00
Rails6のActionTextでransackがうまく使えないので検索できるようにする
これはなに?
Rails6で導入されたActionTextにより簡単にブログ機能を実装できるようになりました。
大変ありがたいお話なのですが、検索機能を定番のgemであるransackを使って実装しようとしたらエラーになり使えませんでした。その時の対処法を記載します。
どうすればいいのか?
結論から言うと、ransack使わないですね。タイトルと若干逸脱してる気もしますが、動けばいいと思います。
手順
こっから具体的な解決までのプロセスです。
事象
まずransack使うとどうなるかって話ですね。下記のようなエラーになります。
Completed 500 Internal Server Error in 42ms (ActiveRecord: 1.4ms | Allocations: 5041) 05:42:11 web.1 | 05:42:11 web.1 | 05:42:11 web.1 | 05:42:11 web.1 | ActionView::Template::Error (undefined method `body_cont' for #<Ransack::Search:0x00007fc354bc9ac0>): 05:42:11 web.1 | 1: <h1>Blogs</h1> 05:42:11 web.1 | 2: 05:42:11 web.1 | 3: <%= search_form_for @q do |f| %> 05:42:11 web.1 | 4: <%= f.search_field :body_cont %> 05:42:11 web.1 | 5: 05:42:11 web.1 | 6: <%= f.submit class: "btn btn-outline-primary" %> 05:42:11 web.1 | 7: <% end %>
undefined method
で500エラーですね。とても残念です。
因みに、title_cont
だとうまくいきました。原因調査
まず、該当モデルの構成はこんな感じです。
title_cont
だとうまく検索できたので、ActionText使ってるとうまく検索できないんだろうなというのは容易に想像できます。class Blog < ApplicationRecord belongs_to :user has_many :comments has_many :favorites has_rich_text :body validates :title, presence: true validates :body, presence: true endDB構成
blogs
はtitle
と言うカラムしかもってなくて、ブログのコンテンツであるbody
はaction_text_rich_texts
が管理してます。なので、
body
をblogs
はカラムとして保持していないためメソッドが生成されてなくて、undefined method
で落ちていたのでした。create_table "action_text_rich_texts", force: :cascade do |t| t.string "name", null: false t.text "body" t.string "record_type", null: false t.bigint "record_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true end create_table "blogs", force: :cascade do |t| t.string "title" t.bigint "user_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["user_id"], name: "index_blogs_on_user_id" end解決方法
原因がわかったので、内部結合していい感じにransackで取得できないかと思い、READMEを見てみる。
https://github.com/activerecord-hackery/ransack
んー、なんかなさそう。頑張ればいけるのか。頑張りたくないからgem使ってるんだが。
Google先生に聞いてみよう
gemの使い方を熟読するのは面倒だったのでstackoverflowを見てみると、ドンピシャの質問がありました。
淡白すぎない?
しかも掲示してるコードだと動かないですね。ガッデム。自分で頑張ろう
調べてもransackでの解決方法が分からなかったので、scopeを定義してクエリで解決することにしました。
app/models/blog.rbclass Blog < ApplicationRecord belongs_to :user has_many :comments has_many :favorites has_rich_text :body validates :title, presence: true validates :body, presence: true # このscopeです scope :search, -> (search_param = nil) { return if search_param.blank? joins("INNER JOIN action_text_rich_texts ON action_text_rich_texts.record_id = blogs.id AND action_text_rich_texts.record_type = 'Blog'") .where("action_text_rich_texts.body LIKE ? OR blogs.title LIKE ? ", "%#{search_param}%", "%#{search_param}%") } endちょっと詳しく解説すると、
action_text_rich_texts
と内部結合してbody
の中身をLIKE句で検索してます。
結合条件は、record_id
とrecord_type
のそれぞれ2つです。それぞれ結合先のモデル名と該当レコードのIDですね。ViewとControllerはとてもシンプルです
app/controllers/blogs_controller.rbclass BlogsController < ApplicationController def index @blogs = Blog.search(params["q"]) end endapp/views/blogs/index.html.erb<h1>Blogs</h1> <%= form_tag blogs_path, method: :get do %> <%= text_field_tag :q %> <%= submit_tag "Search", class: "btn btn-outline-info btn-sm" %> <% end %> <% @blogs.each do |blog| %> <div class="my-3"> <h2> <%= link_to blog.title, blog %> <small class="pl-1"> by <em><%= blog.user.name %></em> </small> </h2> </div> <% end %>動かしてみる
でっきるかなあ? でっきるかなあ?
Started GET "/blogs?q=world&commit=Search" for 172.21.0.1 at 2020-05-16 06:47:08 +0000 06:47:08 web.1 | Processing by BlogsController#index as HTML 06:47:08 web.1 | Parameters: {"q"=>"world", "commit"=>"Search"} 06:47:08 web.1 | User Load (1.7ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 1], ["LIMIT", 1]] 06:47:08 web.1 | Rendering blogs/index.html.erb within layouts/application 06:47:08 web.1 | Blog Load (2.8ms) SELECT "blogs".* FROM "blogs" INNER JOIN action_text_rich_texts ON action_text_rich_texts.record_id = blogs.id AND action_text_rich_texts.record_type = 'Blog' WHERE (action_text_rich_texts.body LIKE '%world%' OR blogs.title LIKE '%world%' ) 06:47:08 web.1 | ↳ app/views/blogs/index.html.erb:9 06:47:08 web.1 | User Load (1.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]] 06:47:08 web.1 | ↳ app/views/blogs/index.html.erb:14 06:47:08 web.1 | Rendered blogs/index.html.erb within layouts/application (Duration: 39.4ms | Allocations: 1560) 06:47:08 web.1 | Completed 200 OK in 100ms (Views: 79.1ms | ActiveRecord: 5.9ms | Allocations: 4093)できたっぽい
まとめ
シンプルにテーブルのカラムを条件に検索したいならransack使うのが良さげですが、ちょっと複雑な条件での検索は難しそうです。僕がちゃんと検索できてないだけな気もしますが。
ransackだとformから世話してくれるので、titleだけの検索なら速攻で実装できて快適でした。
でもまあ、自前で検索機能作ってもそんなに手間じゃないので、Module化して各モデルでincludeする形でもいいかと思います。
- 投稿日:2020-05-16T16:41:41+09:00
Railsで7つの基本アクション以外の定義
基本アクションのおさらい
自分でアクションを定義する
上記の基本アクション以外の処理を行いたい場合は自身で定義することができます。
その際のルーティングの定義方法にはcollectionとmemberが使えます
Rails.application.routes.draw do resources :hoges do collection do HTTPメソッド 'オリジナルのメソッド名' end end endRails.application.routes.draw do resources :hoges do member do HTTPメソッド 'オリジナルのメソッド名' end end end違いとしては、生成されるルーティングにidが付くか、付か無いかです。
・collection → :idなし
・member → :idあり特定のページへ遷移する必要がある場合などは、memberを使うといった感じです。
そして、重要なのは、どこにメソッドの内容を記述するかです。
一般的に、開発現場などでも、テーブル(DB)とのやりとりに関するメソッドはモデルに記載するのが通例らしいです。
例えば、検索機能を実装したい時なんかはその処理を行うメソッドをモデルに書き、コントローラーで呼び出します(viewの検索フォームなどの記述は省略します)
使用例
routes.rbresources :tweets do collection do get 'search' end endtweet.rbclass Tweet < ApplicationRecord #省略 def self.search(search) return Tweet.all unless search Tweet.where('text LIKE(?)', "%#{search}%") end endtweets_controller.rbclass TweetsController < ApplicationController #省略 def search @tweets = Tweet.search(params[:keyword]) end endそれぞれを説明すると、
まず、searchアクションのルーティングを設定します。検索結果を表示するには、詳細ページに行く必要がなく、そのため、collectionを使っています。
formでユーザーが検索を行うと、controllerでsearchアクションからモデルに記述したsearchメソッドを呼び出します。その際、引数として検索結果を渡しています(params[:keyword])
検索結果はモデルのsearchメソッドの中で変数searchに代入されメソッド内で使用できるようになります。
処理の内容は、searchの中身が空なら全ての投稿を取得し、値が入っているならwhereメソッドの中身の条件式に一致した投稿を取得します。
- 投稿日:2020-05-16T16:31:24+09:00
【rails】RSpecによる単体テスト(コントローラ)
準備
Gem
rails-controller-testing
をインストールする。Gemfilegroup :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console ... gem 'rails-controller-testing' #追記 endterminal$ bundle install次のディレクトリを作成する。
spec/controllers
次のファイルを作成する。
spec/controllers/tweets_controller_spec.rb
spec/controllers/tweets_controller_spec.rbrequire 'rails_helper' describe TweetsController do endspecファイルを特定してテストを実行する。
terminal$ bundle exec rspec spec/controllers/tweets_controller_spec.rb Finished in ~ 0 examples, 0 failures正常に実行できることを確認した。
テストコード
newアクション
spec/controllers/tweets_controller_spec.rbrequire 'rails_helper' describe TweetsController do # describe 'http method' #action do describe 'GET' #new do it 'new.html.erbに遷移すること' do get :new expect(response).to render_template :new end end endresponse
example内で行われるリクエストの後の遷移先のビューの情報を持つインスタンス。
render_templateマッチャ
引数にシンボル型のアクションを指定することで、そのアクションがリクエストされた時に自動的に遷移するビューを返す。
テストを実行する。
terminal$ bundle exec rspec spec/controllers/tweets_controller_spec.rb TweetsController GET #new new.html.erbに遷移すること Finished in ~ 1 example, 0 failureseditアクション
コントローラ確認
app/controllers/tweets_controller.rbdef edit @tweet = Tweet.find(params[:id]) endeditアクションでは、@tweetインスタンスを定義している。
spec/controllers/tweets_controller_spec.rbrequire 'rails_helper' describe TweetsController do # describe 'http method' #action do describe 'GET' #new do ... describe 'GET #edit' do it "@tweetに正しい値が入っていること" do # factory_botで定義するインスタンスを一時的にDBに登録して取得する # 登録したデータはテスト完了時に削除される tweet = create(:tweet) # 作成したインスタンスのidをセットする get :edit, params: { id: tweet } #editアクションで取得するtweetが、テストで作成したtweetと同じであることを確認する expect(assigns(:tweet)).to eq tweet end it "edit.html.erbに遷移すること" do tweet = create(:tweet) get :edit, params: { id: tweet } expect(response).to render_template :edit end end endassignsメソッド
コントローラのテスト時に、アクションで定義するインスタンス変数をテストするためのメソッド。
直前で定義したアクションの中で定義されるインスタンス変数を、シンボル型でとる。
(@tweet -> :tweet)factory_botでtweetインスタンスを定義する。
spec/factories/tweets.rbFactoryBot.define do factory :tweet do text {"hello!"} image {"hoge.png"} # created_atをランダムに生成 created_at { Faker::Time.between(from: DateTime.now - 2, to: DateTime.now ) } user end end登録日カラムにて利用するFakerの説明は最後に。
indexアクション
indexアクションを確認する。
app/controllers/tweets_controller.rb... def index @tweets = Tweet.includes(:user).order("created_at DESC").page(params[:page]).per(5) end ...spec/controllers/tweets_controller_spec.rbdescribe 'GET #index' do it "@tweetに正しい値が入っていること" #tweetレコードを3つ保存して、変数tweetsにインスタンスに取得 tweets = create_list(:tweet, 3) #indexアクションへの擬似的なリクエスト get :index # created_atを基準にして、降順にtweetsを並び替える処理 expect(assigns(:tweets)).to match(tweets.sort{ |a, b| b.created_at <=> a.created_at }) end it "index.html.erbに遷移すること" get :index expect(response).to render_template :index end endmatchマッチャ
引数に配列クラスのインスタンスをとり、expectの引数と比較する。
配列の中身の順番までチェックする。factory_botでのuserインスタンス定義する。
spec/factories/users.rbFactoryBot.define do factory :user do nickname {"abe"} # email {"kkk@gmail.com"} password {"00000000"} password_confirmation {"00000000"} sequence(:email) {Faker::Internet.email} end endFaker
今回、tweetインスタンスを複数作ることになるが、emailの値が重複するとバリデーションによりエラーが起こる。
そのため、Fakerで動的にダミーデータを生成する。Gemfilegroup :test do ... gem 'faker', "~> 2.8" endterminal$ bundle installテストの実行
terminal$ bundle exec rspec spec/controllers/tweets_controller_spec.rb 2020-05-16 16:26:37 WARN Selenium [DEPRECATION] Selenium::WebDriver::Chrome#driver_path= is deprecated. Use Selenium::WebDriver::Chrome::Service#driver_path= instead. TweetsController GET #new new.html.erbに遷移すること GET #edit @tweetに正しい値が入っていること edit.html.erbに遷移すること GET #index @tweetに正しい値が入っていること index.html.erbに遷移にすること Finished in 0.7887 seconds (files took 3.68 seconds to load) 5 examples, 0 failures予期する通り動作することを確認できた。
RSpec参考書籍
https://leanpub.com/everydayrailsrspec-jp/read#leanpub-auto-section
- 投稿日:2020-05-16T15:39:03+09:00
docker-composeでRails5.2+MySQL開発環境
(社内の特殊な環境を移行した際のメモです)
rubyのイメージでうまく環境を構築できなかった(=既存のrails環境のGemfileと一致するバージョンのgemが揃わなかった)ため、Ubuntuのイメージから出発してRailsの環境を構築した際の設定です。
docker-compose.ymlversion: '3' services: db: image: mysql:5.7 command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci volumes: - ./tmp/mysql:/var/lib/mysql environment: - MYSQL_ROOT_PASSWORD=secret web: build: . command: "rails s -p 3000 -b '0.0.0.0'" volumes: - ./:/railsapp ports: - "3000:3000" depends_on: - dbDockerfileFROM ubuntu:18.04 RUN apt-get update && apt-get upgrade && apt-get install -y \ ruby-dev libmysqlclient-dev libmagick++-dev libcurl4-openssl-dev libssl-dev nodejs \ && gem install rails -v "~> 5.2" -N && gem install bundler -v "~> 1.16" RUN mkdir /railsapp WORKDIR /railsapp COPY ./ /railsapp RUN bundle install
- 投稿日:2020-05-16T15:30:23+09:00
情報セキュリティについてのまとめ
情報セキュリティ
情報セキュリティとは、WEBサービスにおいてのセキュリティのことを指します。
情報漏洩や不正なアクセスを防ぎつつ権限のあるユーザーの利便性を高めるのが理想です。下記の3つを保持することがWEBサービスの使命です。
1.機密性
-権限のない人が情報資産を見たり使用したりできないようにする
2.完全性
-権限のない人が情報を消したり書き換えたりできないようにする
3.可用性
-権限のある人(ユーザー)がサービスをいつでも利用できるようにする全てにおいてのセキュリティをおびやかす欠陥や問題点のことを脆弱性と言います。
また、脆弱性は開発者のチェック不足やバグによって生まれます。脆弱性の具体例は以下です
-個人情報を勝手に閲覧される(機密性の侵害)
-WEBページの内容が改ざんされる(完全性の侵害)
-WEBページの利用ができなくなる(可用性の侵害)ユーザーへの金銭的補填、開発者の信頼の失墜、機会損失などの被害が生まれてしまうため、脆弱性への対策はしっかり行わなければいけません。
- 投稿日:2020-05-16T14:49:30+09:00
Rubyとは?Railsとは?
記事の概要
Ruby/Ruby on Railsとは何か分からない人が少し理解できるようになります。
Rubyとは
Rubyとはプログラミング言語の一つです。
小さいプログラムから大きいWebアプリケーションまでを実用的に作成することができます。Rubyの特徴
・簡潔な文法で記述することができる
・コードが読みやすい
・プログラムを記述してすぐに実行することができるRubyを使用しているサイト
・hulu
・クックパッド
・食べログ
・楽天市場
・AirbnbRailsとは
Railsとは、Ruby on Railsの略称です。Railsと言われることが多いです。
RailsはWebアプリケーションフレームワークの1つで、最も多く使われています。
Rubyという言語を使ってWebアプリケーションを作っていきます。Webアプリケーションフレームワークとは?
Webアプリケーションフレームワークとは、Webアプリケーションを簡単に作るための骨組みのことです。
これを使うことによってより少ない労力で開発することができます。この記事を読んでいただきありがとうございました。
- 投稿日:2020-05-16T14:49:13+09:00
ルーティングのネスト
ルーティングのネストとは
通常のルーティングの記述は
Rails.application.routes.draw do resources :親となるコントローラー resources :親となるコントローラー resources :親となるコントローラー ,,,,, endという感じでそれぞれ独立した形でコントローラーへのルーティングを生成していますが、
ルーティングのネストをすると、あるコントローラーのルーティング内に、別のコントローラーのルーティングを記述することができます
Rails.application.routes.draw do resources :親となるコントローラー do resources :子となるコントローラー ←階層を下げ、do,,,endで囲む end end使用するメリット
例えば、インスタグラムやツイッターなどにはコメント機能があります。
そして、そのコメントは、必ず投稿先が存在しています。
それでは、ネストをしないでルーティングを設定した場合と、ネストをした場合の生成されるルーティングの違いを見てみます。
ネストなし
Rails.application.routes.draw do #省略 resources :tweets resources :comments, only: :create endPrefix Verb URI Pattern Controller#Action #省略 comments POST /comments(.:format) comments#createネストあり
Rails.application.routes.draw do #省略 resources :tweets do resources :comments, only: :create end endPrefix Verb URI Pattern Controller#Action #省略 tweet_comments POST /tweets/:tweet_id/comments(.:format) comments#createURIに注目して下さい。コメントには投稿先が必ずあるのにも関わらず、ネストをしない場合のルーティングは、どの投稿先のコメントなのかを示す情報がありません。
それに対し、ネストをした場合は、tweet_idの箇所にツイートのid番号が入ります。それにより、どのツイートに対するコメントなのかというのがURIから判断できるようになります。
まとめ
・ネストをすることで関係性のあるもの(アソシエーション先)のid情報が取得できます
- 投稿日:2020-05-16T11:22:27+09:00
【Rails】remote:true形式でAjax通信を行う(ブックマーク機能のajax化)
Ajaxとは
Ajaxとは、Webブラウザ上で
非同期通信
を行い、ページ全体の再読み込み無しにページを更新する方法のことです。同期通信について
同期通信
では、クライアントはwebページ全体の情報(HTMLとそれに紐づくcss,js,imageなどのアセット)をサーバーから受け取って、ページを一から作り直します。
例えばページの一部を変更するだけなのに、他の部分も組み立て直すってことはその分ページの表示に時間がかかっちゃいます。(サーバー側の処理を待つことになる)しかも、このリクエスト〜レスポンスの処理を行っている間は、他の処理を行わずにサーバーからレスポンスが返ってくるのを待ち続ける必要があります(よくあるのが画面が真っ白になって何もできない状態)。
そこでAjaxのような非同期通信を使用すれば、ページ遷移無しに、高速で更新処理を行い、尚且つ、リクエスト〜レスポンスの処理を行っている間も他の処理が行えます。
非同期通信の方法は2種類
この便利なAjaxによる非同期通信を行う方法としては、
①remote:true形式
②ajax関数を使った形式
の2パターンが存在しますが、今回はremote:true形式について以下に記していきます。仕組みだけ知りたいよって方は、コードの説明は読み飛ばしちゃっても大丈夫です。
コードの説明
今回作るもの
掲示板のブックマーク(いいね)ボタンを押した時に、ブックマークの登録、解除を行うという仕組みをajax化させていきます。
ルーティングの設定
ブックマークの登録、削除を行うために必要なルーティングの設定を行う。
config/routes.rbresources :boards do resources :bookmarks, only: %i[create destroy], shallow: true endモデルの設定
モデルでUsersテーブル、Boardsテーブル、Bookmarksテーブルの関連付を行う。
app/models/user.rbclass User < ApplicationRecord has_many :boards, dependent: :destroy has_many :bookmarks, dependent: :destroy has_many :bookmark_boards, through: :bookmarks, source: :board # ブックマーク関連のインスタンスメソッド # ブックマークをする def bookmark(board) bookmark_boards << board end # ブックマークを解除する def unbookmark(board) bookmark_boards.destroy(board) end # ブックマークをしているかどうかを判定する def bookmark?(board) bookmarks.where(board_id: board.id).exists? end endapp/models/board.rbclass Board < ApplicationRecord belongs_to :user has_many :bookmarks, dependent: :destroy endapp/models/bookmark.rbclass Bookmark < ApplicationRecord belongs_to :user belongs_to :board validates :user_id, uniqueness: { scope: :board_id } endコントローラの設定
AjaxによるHTTP通信を行うには、formに
remote:true
オプションを設定する必要がある。
form_withメソッドでAjax通信を利用しない場合(
local: true
オプション)
bookmarksコントローラのcreateアクション実行の際に、bookmarks/create.html.erb
というファイルをレンダリングしようとするため、別のページへリダイレクトさせていた。Ajax通信を利用する場合(
remote: true
オプション)
remote: true
の記述によって、AjaxでHTTPリクエストを送信するように設定される。
更に、html.erb
ファイルではなくjs.erb
ファイルをレンダリングしてくれる。そして、このjs.erbファイルをjsのコードに変換した文字列が、レスポンスボディとしてブラウザに返される(詳細は後述)。app/controllers/bookmarks_controller.rbclass BookmarksController < ApplicationController # js.erbファイルで変数を使用するため、インスタンス変数を設定 def create @board = Board.find(params[:board_id]) current_user.bookmark(@board) end def destroy @board = current_user.bookmark_boards.find(params[:id]) current_user.unbookmark(@board) end endブックマークボタンを切り替えるためのビュー
bookmarks/_bookmark_area.html.erb
ファイルで、ログイン中のユーザーが掲示板をブックマークしているかどうかによって呼び出すテンプレートを分ける。
- ブックマークしていない場合は
_bookmark.html.erb
を呼び出す。
- ブックマークボタンは色無しの状態
- ブックマークする機能
- ブックマークしている場合は
_unbookmark.html.erb
を呼び出す。
- ブックマークボタンは色付きの状態
- ブックマークを削除する機能
app/views/bookmarks/_bookmark_area.html.erb<% if current_user.bookmark?(board) %> <%= render 'bookmarks/unbookmark', { board: board } %> <% else %> <%= render 'bookmarks/bookmark', { board: board } %> <% end %>ブックマークしていない場合のボタンを実装
_bookmark.html.erb
ファイルを作成
- ブックマークするので、HTTPメソッドはpost
。対応するコントローラがcreate.js.erb
を呼び出す。
- id属性を付与(どのボタンをクリックしたか判別するため、各レコードのidを使用し、一意性を保つ)
-remote: true
オプションを付与。app/views/bookmarks/_bookmark.html.erb<%= link_to board_bookmarks_path(board), id: "js-bookmark-button-for-board-#{board.id}", method: :post, remote: true do %> <%= icon 'far', 'star' %> <% end %>ブックマークしている場合のボタンを実装
_unbookmark.html.erb
ファイルを作成
- ブックマークを削除するので、HTTPメソッドはdelete
。destroy.js.erb
を呼び出す。
- id属性を付与。
-remote: true
オプションを付与。app/views/bookmarks/_unbookmark.html.erb<%= link_to bookmark_path(board), id: "js-bookmark-button-for-board-#{board.id}", method: :delete, remote: true do %> <%= icon 'fas', 'star' %> <% end %>js.erbファイルを作成
js.erbファイルは以下2つの記述が可能。
1. jsの処理
2. rubyの記述(erbファイルだから)以下のjs.erbファイルによって、画面上に表示するブックマークボタンをAjax通信で切り替えられるようにします。
【
create.js.erb
ファイルを作成】
create.js.erbでhtml()メソッドを用い、指定したセレクタのhtml部分(指定したid属性を持つ部分)を置き換える。_unbookmark.html.erb
に置き換えるよう記述。app/views/bookmarks/create.js.erb$("#btn-bookmark-<%= @board.id %>").html("<%= j(render('boards/unbookmark', board: @board)) %>");【
destroy.js.erb
ファイルを作成】
create.js.erbと逆の内容を記述する。app/views/bookmarks/destroy.js.erb$("#btn-bookmark-<%= @board.id %>").html("<%= j(render('boards/bookmark', board: @board)) %>");ここまでがコードの細かい話!!
ブックマークボタンを押した時のHTTPレスポンスについて
上記の実装によって、なぜブックマークボタンをAjax通信で切り替えられるのか、その仕組みについて以下で説明します。
先に結論を述べると、それはサーバーからレスポンスボディとしてJavaScriptのコードを返し、そのコードに対する処理をクライアント側が実行してくれているからです。HTTPレスポンスの中身とクライアントの処理は?
- ブックマークボタンを押した時のHTTPレスポンスの中身はどうなっているのか?
- それに対してクライアント(ブラウザ)側はどのような処理を行うのか?
の2点を押さえれば、ブックマークボタンをAjax通信で切り替えられる仕組みを理解できるはずです。ブックマークボタンを押した時のHTTPレスポンスの中身は?
erbファイルをJS形式のコード(この段階ではただの文字列!)に変換したものが、レスポンスボディとしてクライアントに返されます。
つまり、erbファイルをそのままクライアントに返すのではなく、サーバー側でjs.erbファイルのrubyの記述(
j render
とか@board
とか)を事前に実行し、HTMLのコードとして展開した結果を、クライアント側に返しているのです。
一言で表すなら、クライアント側が読める内容に変換してから返している、ということです。レスポンスに対するクライアント側の処理
これに対し、クライアントはサーバーから返ってきたレスポンスボディを見て、「これはjs形式のものだな」と判断し、そこでようやくレスポンスボディの文字列に対してJavaScriptを実行してくれる、といった感じです。
検証ツールのネットワークタブを確認
- HTTPレスポンスのContent-Type(どういうコンテンツの種類か)が text/javascriptになっている。
→ajax通信に設定しているから、RailsがJS形式でレスポンスを送ってくれている。
→クライアント側はContent-typeを見て「JSで処理するんだな」と判断している。$("#js-bookmark-button-for-board-152").replaceWith("<a id=\"js-bookmark-button-for-bo ard-152\" data-remote=\"true\" rel=\"nofollow\" data-method=\"delete\" href=\"/bookma rks/152\">\n <i class=\"fas fa-star\"><\/i>\n<\/a>");おわりに
以上でremote:true形式でAjax通信を行う方法の説明を追えます。
なにか説明部分について誤りがございましたら、ご指摘頂きたく思います。
- 投稿日:2020-05-16T10:38:26+09:00
Day13-14 Rails 検索機能を実装
1.検索フォーム作成
app/views/tweets/index.html.erb<%= form_with(url: search_tweets_path, local: true, method: :get, class: "search-form") do |form| %> <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "search-input" %> <%= form.submit "検索", class: "search-btn" %> <% end %>2. ルーティング
collection:ルーティングに:idがつかない
member:ルーティングに:idがつく
→検索機能は:id取得する必要がないので、collenctionconfig/routes.rbcollection do get 'search' end3. searchメソッドをTweetモデルに定義
ビジネスロジック(プログラムの処理の流れ)は、メソッドとして、モデルにまとめて定義する。別の場所でも使うことができるため。
また、テーブルとのやりとりに関するメソッドはモデルに置く。app/models/tweet.rbdef self.search(search) return Tweet.all unless search Tweet.where('text LIKE(?)', "%#{search}%") end もしくは def self.search(search) if search Tweet.where('text LIKE(?)', "%#{search}%") else Tweet.all end end4. searchアクションをコントローラーに定義
app/controllers/tweets_controller.rbclass TweetsController < ApplicationController before_action :set_tweet, only: [:edit, :show] before_action :move_to_index, except: [:index, :show, :search] def search @tweets = Tweet.search(params[:keyword]) end5.検索結果のビュー作成
app/views/tweets/search.html.erb<%= form_with(url: search_tweets_path, local: true, method: :get, class: "search-form") do |form| %> <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "search-input" %> <%= form.submit "検索", class: "search-btn" %> <% end %> <div class="contents row"> <% @tweets.each do |tweet| %> <%= render partial: "tweet", locals: { tweet: tweet } %> <% end %> </div>別解(7つのアクションを使った方法)
1. 検索フォーム作成
app/views/tweets/index.html.erb<%= form_with(url: search_tweets_path, local: true, method: :get, class: "search-form") do |form| %> <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "search-input" %> <%= form.submit "検索", class: "search-btn" %> <% end %>2. tweets::searchesコントローラー作成
$ rails g controller tweets::searchestweetsディレクトリ配下にsearches_controller.rbを作成することで、Tweets::という名前空間が名付けられ、tweetを検索する機能とわかる。
3. ルーティング
config/routes.rbnamespace :tweets do resources :searches, only: :index end
namespace :ディレクトリ名 do ~ end
と囲んでルーティングすると、そのディレクトリ内のコントローラーアクションを指定できる。4. 検索フォームの送信先設定
app/views/tweets/index.html.erb<%= form_with(url: tweets_searches_path, local: true, method: :get, class: "search-form") do |form| %>上記ルーティング通り、tweets_search_pathに合わせる
5. indexアクションをコントローラーに追加
app/controllers/tweets/searches_controller.rbclass Tweets::SearchesController < ApplicationController def index @tweets = Tweet.search(params[:keyword]) end end6. 検索結果表示ビューの作成
app/views/tweets/searches/index.html.erb<%= form_with(url: tweets_searches_path, local: true, method: :get, class: "search-form") do |form| %> <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "search-input" %> <%= form.submit "検索", class: "search-btn" %> <% end %> <div class="contents row"> <% @tweets.each do |tweet| %> <%= render partial: "tweets/tweet", locals: { tweet: tweet } %> <% end %> </div>
- 投稿日:2020-05-16T10:16:44+09:00
Rails6+Reactで付箋アプリっぽいページを作ってみた。4 (UI作成編2)
記事について
前回まででUIの作成を行いましたが、見た目があまりに寂しいので、
スタイルシートを使って、それっぽく見せてみます。関連する記事
書いているうちに分量がすごくなって記事を分割したので、リンク先をまとめておきます。
その1(環境構築〜モデル作成編)
その2(API作成編)
その3(UI作成編1)
その4(UI作成編2)
おまけ(モデルのテスト編)スタイルシート
white_board/main.htmlで使われるwhite_board.scssを編集して、各DOM要素の見た目を目ます。
全体的なレイアウトにはflexレイアウト。
付箋の中身は段組が指定しやすかったのでgridレイアウトを使用しています。app/assets/stylesheets/white_board.scss// タイトル要素 div#WhiteBoardTitle { font-weight: bold; font-family: Monospace; font-size: 24px; border-collapse: collapse; } // ホワイトボード全体 // "display: flex"でflexレイアウトを指定しています。 // 左上からピチっと並べてもらいます。 div#WhiteBoard { width: 100vw; height: 100vh; display: flex; flex-flow: row wrap; justify-content: flex-start; align-items: flex-start; align-content: flex-start; } // ユーザ毎の箱 div.UserBox { margin: 1px; width: 45vw; height: 45vh; border: 1px solid; border-collapse: collapse; } // ユーザ名を表示する箱 div.UserName { width: 100%; height: 25px; font-weight: bold; font-size: 18px; font-family: Monospace; text-align: center; background-color: #FFEEAA; border-bottom: 1px solid; } // 付箋表示エリア // ここもflexレイアウト div.TaskArea { width: 100%; height: 100%; display: flex; flex-flow: row wrap; align-items: flex-start; align-content: flex-start; } // 付箋本体 // 影をつけてそれっぽく div.Sticky { display: grid; padding: 1px; margin: 5px; width: 100px; height: 80px; font-size: 10px; font-family: monospace; text-align: left; border: 1px #FFFF00 solid; background-color: #FFFF00; box-shadow: 0 5px 5px rgba(0, 0, 0, 0.3); } // タスクのタイトル div.TaskTitle { grid-row: 1; grid-column: 1 / 3; font-size: 12px; font-weight: bold; font-family: monospace; border-bottom: #FF0000 1px solid; } // タスクの本文 div.TaskDescription { grid-row: 2 / 6; grid-column: 1 / 3; font-size: 10px; font-family: monospace; } // タスクの期日 div TaskDueDate { grid-row: 7 / 8; grid-column: 1 / 3; font-size:8px; font-family: monospace; }完成イメージ
こんな感じになると思います。
これで少しはましになったかなぁ。
スタイルの変更後もテストだ。
まずは、これまで作ったテストを流してみます。
(間違ってvisible: hiddenになってしまったりなどの影響がないことが確認できます。)で、狙ったスタイルが適用されているかもテストしてみます。
test/system/whiteboards_test.rbtest "style check" do # white_board/mainを開く。 visit white_board_main_url; # タイトルはfont-weigt: bold, font-size: 24pxのはず。 # font-weightをマッチする時はboldでなく、数値(700)でマッチします。 find("div#WhiteBoardTitle").assert_matches_style("font-weight" => "700", "font-size" => "24px"); find("div#WhiteBoard").assert_matches_style("display" => "flex", "flex-flow" => "row wrap", "align-content" => "flex-start", "align-items" => "flex-start"); # ユーザ名部分 username_divs = all("div", class: "UserName"); username_divs.each do | username_div | username_div.assert_matches_style("font-weight" => "700", "font-size" => "18px"); end # 付箋 # 色はrgba(赤, 緑, 青, 透過率)で比較するようです。 stickies = all("div", class: "Sticky"); stickies.each do | sticky | sticky.assert_matches_style("background-color" => "rgba(255, 255, 0, 1)", "width" => "100px", "display" => "grid"); end endこういうテストを作っておくとスタイルシートの書き間違いなどで、狙ったスタイルが適用されてないことを検出することができます。
さらに、期日が近くなったら背景を赤くするなどの仕組みを取り入れたら、そういうのも自動でテストできるようになります。
- 投稿日:2020-05-16T09:54:35+09:00
【Rails】gem ' active_hash' プルダウンメニュー作成
手順
1:gem導入
2:手動でモデルを作成
3:データを引っ張る
4:アソシエーションを定義
5:実装中のエラー1:gem 導入
Gemfail.gem 'active_hash'
bundle install
する。2:モデル作成
※モデルは手動で作成
model/Shipping_fee.rbclass ShippingFee < ActiveHash::Base self.data = [ {id: 1, name: '送料込み(出品者負担)'}, {id: 2, name: '着払い(購入者負担)'} ] endターミナルで確認
[2] pry(main)> ShippingFee.find(1) => #<ShippingFee:0x00007fa012181140 @attributes={:id=>1, :name=>"送料込み(出品者負担)"}>3:プルダウンメニューにデーターを引っ張る
new.html.haml.new-contents__box__title 発送量の負担 %supan{class: "required"} 必須 = f.collection_select :shipping_fee_id, ShippingFee.all, :id, :name, {prompt: "選択してください"}, {class: ""}
ShippingFee.all
でモデルからデータを取得。4:アソシエーションを定義
item.rbextend ActiveHash::Associations::ActiveRecordExtensions belongs_to_active_hash :shipping_feeactive_hashにはbelongs_to_active_hashメソッドが用意されているため、
親になるモデルのみ
にアソシエーションを定義する。アソシエーションの確認:
[5] pry(main)> item = Item.find(1) [6] pry(main)> item.shipping_fee => #<ShippingFee:0x00007fa012181140 @attributes={:id=>1, :name=>"送料込み(出品者負担)"}>5:実装中のエラー
LoadError Unable to autoload constant ShippingFee
原因:作成したモデルのクラス名model/Shipping_fee.rb#修正前 class Shipping_fee < ActiveHash::Base #修正後 class ShippingFee < ActiveHash::Base
- 投稿日:2020-05-16T08:44:01+09:00
【Rails】テーブルを分けて複数の画像をアップロードする
複数枚の写真を一度に保存する機能の実装において、
【itemテーブル】と【item_imageテーブル】の二つに
分けて複数枚の写真を保存する機能を実装した際の手順をまとめたのでご紹介します。完成形
○ 商品写真は最大で10枚まで投稿可能。
○ 一つのファイルフィールドに一つの写真でアップロードしていく。
○ 5枚投稿時点で下段に切り替わる。
1. HTMLの用意
sample.haml~ 省略 ~ .image-container .image-container_box .form-title %span.box-form-explanaion 商品画像 %span.indispensable 必須 %p.image-container_box_message 最大10枚までアップロードできます .image-container_box_alart-10 ※ 1枚目は必須です .image-container_unit-man 【写真が順にプレビューされていく箱】 .item-image-container__unit.preview-0 【写真一枚が入る箱 ※投稿ごとに生成されていく】 = f.fields_for :item_images do |i| .image-container__unit--guide = i.file_field :image, class: 'img-man', id:"image-label-0",type: 'file' .have-image %i.fas.fa-camera ~ 省略 ~◉【写真一枚が入る箱】に2つクラスがあるのは、2枚目以降では毎回クラス名を
変えていくためです。
◉この仕様では、1つのinputに写真は1つなので、multiple属性は付けていません。2. CSSの用意
sample.scss.image-container { padding: 40px; border-bottom: solid 1px rgb(235, 235, 235); &_box { &_message { height: 19px; margin: 16px 0 5px; font-size: 14px; line-height: 1.4em; display: block; } &_alart-10 { margin-bottom: 4px; font-size: 14px; font-weight: bold; color:red; } } &_unit-man { min-height: 152px; display: flex; flex-direction: row; flex-wrap: wrap; .item-image-container__unit { align-content: center; align-items: center; background-color: rgb(245, 245, 245); display: flex; justify-content: flex-start; height: 150px; width: 118px; margin-left: 5px; margin-bottom: 2px; justify-content: center; position: relative; border-width: 1px; border-style: dashed; border-color: rgb(204, 204, 204); border-image: initial; .have-image { position: absolute; left: 32px; top: 40px; z-index: 0; cursor: pointer; font-size: 16px; .fas.fa-camera { margin-left: 16px; font-size: 1.2rem; } .fas.fa-camera:hover { cursor: pointer; transform: scale(1.3, 1.3); transition: all 0.1s ease 0s; } } .item-image-container__unit__image { z-index: 1; height: 145px; width: 100%; margin: 0 3px; background-color: #ffffff; position: relative; img { width: 100%; height: auto; } .image-option__list--tag { width: 100%; background-color: lightblue; text-align: center; position: absolute; bottom: 0; left: 0; } .image-option__list--tag:hover { cursor: pointer; transform: scale(1.0, 1.0); transition: all 0.1s ease 0s; background-color: #ea352d; color:#ffffff; } } } .item-image-container__unit { input{ display: none; } } } }[flex-direction: row;]と[flex-wrap: wrap;]により、
写真が既定の幅まで投稿されたら下段に折り返してくれます。3. JSでプレビューさせる
sample.js$(function(){ ~ 省略 ~ var dataBox = new DataTransfer(); //データ用の箱を作る $(document).on('change', '.img-man', function(){ //inputの中身が変化したら発火する $.each(this.files, function(i, file){ var fileReader = new FileReader(); dataBox.items.add(file) fileReader.readAsDataURL(file); //ファイルの読み込み fileReader.onloadend = function() { //読み込み完了すると発火 var num = $('.item-image-container__unit').length //写真の枚数をnumに代入 var src = fileReader.result //写真のデータをsrcに代入 var html = `<div class="item-image-container__unit__image"> <img src="${src}"> <div class="image-option__list--tag btn-${num}">削除</div> </div>` var field = `<div class="item-image-container__unit preview-${num}"> <div class="image-container__unit--guide"> <label for="image-label-${num}"> <input class="img-man" id="image-label-${num}" type="file" name="item[item_images_attributes][${num}][image]"> <div class="have-image"> <i class="fas fa-camera"></i> </div> </label> </div> </div>` $(html).appendTo(`.preview-${num - 1}`) //枚数で該当するクラスに写真を追加する if (num < 10 ) { //10枚分まで新しいinputの生成を行う $(field).appendTo('.image-container_unit-man') } }; }); }); //削除機能 $(document).on("click", ".image-option__list--tag", function(){ //削除ボタンクリックで発火 var num = $('.item-image-container__unit').length var field_2 = `<div class="item-image-container__unit preview-0"> <div class="image-container__unit--guide"> <label for="image-label"> <input class="img-man" id="image-label-0" type="file"> <div class="have-image"> <i class="fas fa-camera"></i> </div> </label> </div> </div>` $(this).parent().parent().remove(); //写真が入っている箱ごと削除 $(".item-image-container__unit").removeClass(`preview-${num - 1}`) $(".item-image-container__unit").addClass(`preview-${num - 2}`) if(num == 1) { //全部削除した時に新たに1つフィールドを生成する $(field_2).appendTo('.image-container_unit-man') } }); ~ 省略 ~◉変数【num】は、写真の読み込みごとに代入され、その度に横に生成される
【preview-${num}】クラスが書き換わって横に生成されるようになっています。◉ 下部の10枚制限の記述によって、10枚までアップされるとinputの生成が止まります。
また、multiple属性が付いていないので写真の一括選択はできなくなります。◉削除機能に関しても、クラス名の書き換えを行う必要があります。
登録枚数に関しては、モデルにも別でバリーデーションは書いてあります。
sample.rbdef item_images_number errors.add(:item_images, "は10枚までです") if item_images.size > 10 end
以上で終了です。
ご覧いただきありがとうございました。
- 投稿日:2020-05-16T07:44:05+09:00
Ruby on Rails Ⅱ
投稿一覧ページ
データベースをHTMLに直接書く
投稿に関するページ
postsコントローラ
indexアクション名コマンド
「rails generate controller posts index」は「rails g controller posts index」と省略可
作成された「views/posts/index.html.erb」に投稿一覧ページ用のHTMLを書いていく「config/routes.rb」を開いて、
get "posts/index" => "posts#index"
というルーティング「app/controllers/posts_controller.rb」を開いて、
indexアクションを持ったpostsコントローラ「app/views/posts/index.html.erb」を開いて、ビューファイルが作成されている
HTMLファイルを張り付け
今日からProgateでRailsの勉強するよー!投稿一覧ページ作成中!「app/assets/stylesheets/posts.scss」
CSS
/* posts/index ================================ */
.posts-index-item {
padding: 20px 30px;
background-color: white;
overflow: hidden;
box-shadow: 0 2px 6px #c1ced7;
}.post-left img {
width: 50px;
height: 50px;
border-radius: 40%;
box-shadow: 0 2px 6px #c1ced7;
object-fit: cover;
}.post-user-name a {
font-weight: 600;
}.post-user-name a:hover {
color: #3ecdc6;
}.post-left {
float: left;
width: 10%;
}.post-right {
float: left;
width: 90%;
padding-left: 25px;
text-align: left;
}/* posts/show ================================ */
.posts-show form {
display: inline;
}.posts-show-item {
padding: 30px;
background-color: white;
box-shadow: 0 2px 6px #c1ced7;
overflow: hidden;
}.posts-show-item img {
width: 60px;
height: 60px;
border-radius: 40%;
box-shadow: 0 2px 6px #c1ced7;
vertical-align: middle;
object-fit: cover;
}.posts-show-item .post-user-name a {
vertical-align: middle;
font-size: 24px;
margin-left: 15px;
}.posts-show-item p {
font-size: 24px;
margin: 20px 0;
}.post-time {
color: #8899a6;
margin-bottom: 10px;
}.post-menus {
float: right;
}.post-menus a, .post-menus input {
color: #8899a6;
text-decoration: underline;
font-size: 14px;
}/* posts/new ================================ */
.posts-new textarea {
font-size: 20px;
padding: 10px;
min-height: 140px;
border: 1px solid rgb(216, 218, 223);
resize: none;
}.posts-new textarea::-webkit-input-placeholder {
font-size: 24px;
opacity: 0.5;
}viewファイル内で変数を定義
index.html.erbのようなerbという形式のファイルでは、以下の図のように<% %>で囲むことで、HTMLファイルの中にRubyのコードを記述することができます。
「erb」とは「Embedded Ruby(埋め込みRuby)」の略
埋め込むRubyコードをブラウザに表示したい場合には、以下の図のように
<% %>ではなく、<%= %>を用いま
<% %> 変数
<%= %> 表示<% post1 = "今日からProgateでRailsの勉強するよー!" %>
<!-- ここに変数post2を定義して、指定された投稿内容を代入してください -->
<% post2 = "投稿一覧ページ作成中!" %>each文で表示
<%
posts = [
"今日からProgateでRailsの勉強するよー!",
"投稿一覧ページ作成中!"
]
%>アクションで変数を定義
Rails ではビューではなく、アクションで定義することが一般的
@変数
変数名を「@」から始めることでこの変数は特殊な変数となり、ビューファイルでも使用することができる
.rb ビューで使える変数
def index
@post1 = "にんじゃわんこ"
end.erb アクションで定義した変数の呼び出し
<%= @post1 %>データベースを用意
マイグレーションファイルと呼ばれる、データベースに変更を指示するためのファイルを作成
「rails g model Post content:text」というコマンドで作成する
generate Postsテーブル カラム名 データ型
db/migrateフォルダの下にマイグレーションファイルが作成テーブルを用意
「rails db:migrate」を実行
マイグレーションファイルを作成した場合は必ず「rails db:migrate」を実行する必要があるモデルを確認
テーブルを操作するためのモデルと呼ばれる特殊なクラス
「rails g model」コマンドでposts テーブルを操作するための Post モデルがすでに生成
「post.rb」が、app/modelsフォルダの中に作成されていま
・app/modelsフォルダにモデルが定義されたファイル
・db/migrateフォルダにマイグレーションファイルrails consoleを使ってみよう
ターミナル上で「rails console」と入力し実行(Enter)することで、コンソールを起動することができます。コンソールを起動した状態で、「1+1」を実行すると、その実行結果が表示されます。
また、「quit」を実行すると、コンソールを終了することができるテーブルに投稿データを保存しよう
posts テーブルにデータを追加するには右の図のように、
① new メソッドで Post モデルのインスタンスを作成
② posts テーブルに保存newメソッド
saveメソッドpost1 = Post.new(content: " ")
post1.saveテーブルからデータを取り出そう
# テーブルから1つのデータを取り出す「Post.first」とすることで、 posts テーブルにある最初のデータを取得する
contentカラムの値を取り出そう
「post.content」とすることで「Post.first」で取得したデータから投稿内容を取得することができるテーブルから全てのデータを取り出そう
全ての投稿を取り出す posts = Post.all
投稿の配列から1つのデータを取り出す Post.all [0]
配列のデータから投稿内容を取り出す Post.all[0].contentデータベースのデータを表示しよう
rails consoleのまとめ
データの作成 (new,save)
データの取得 (Post.all,post.content)# 全ての投稿を表示する class PostsController < ApplicationControllerdef index
@posts = Post.all
end
end共通のレイアウトをまとめよう
Railsでは、「views/layouts/application.html.erb」に共通のHTMLを書いておくことができるlink_toメソッド
Rails ではlink_toというメソッドを使うと<a>タグを作成することができるぞ。 link_to メソッドは Ruby のコードなので、「<%=%>」で囲む 第一引数に表示する文字を、第二引数に URLを書くことでリンクが作成される
<!-- 以下のリンクをlink_toメソッドを用いて変更してください -->
<%= link_to("TweetApp","/") %>
<!-- 以下のリンクをlink_toメソッドを用いて変更してください -->
<%= link_to("TweetAppとは","/about") %>
<!-- ここにlink_toメソッドを用いて投稿一覧ページへのリンクを作成してください -->
<%= link_to("投稿一覧","/posts/index") %>
<%= yield %>
- 投稿日:2020-05-16T03:19:06+09:00
MiniMagick(imagemagick)で複数のフォントファイルを使って絵文字に対応する
ちょっと前にimagemagickで絵文字を含むテキストを合成する時に結構ハマったのでなんとなく備忘録を。
imagemagickはフォントが1つしか指定できないので、大抵の場合絵文字があると文字化けしてしまいます。
そこで、pangoを使って複数フォントに対応します。(以下、dockerでrailsを動かす前提で進めます。)
フォントを置く
まずは使いたいフォントをDLしてコンテナのfontsに置きます。
(apt-getで手に入るならそっちのほうがいいと思います)
今回はNoto Sans CJK JPとNoto Color EmojiをDLして/assets/fontsに置きました。# Dockerfile COPY /assets/fonts /usr/share/fontsmemo: Noto Color Emojiについて
Noto Color Emojiは定番のフォントだと思いますが、入手先が3つあってそれぞれ対応しているUnicodeのバージョンが違います。
- 公式サイト → Unicode10
- apt-get → Unicode11
- Github → Unicode12(最新)
Github以外は更新が止まってるようなので、Unicodeが更新される度にGithubからDLして上書きする必要があります。フォントの設定を変える
Noto Color Emojiはビットマップフォントですが、設定によってはこれがデフォルトで無効になってることがあるので書き換えます。
# Dockerfile RUN rm /etc/fonts/conf.d/70-no-bitmaps.conf RUN ln -s /etc/fonts/conf.avail/70-yes-bitmaps.conf /etc/fonts/conf.d/ビットマップフォントを無効にする設定ファイルを消して、conf.availから有効にする設定をコピーしてます。
次にNoto Color Emojiの優先度を上げるため、設定ファイルを作って/etc/fonts/に置きます。(/usr/share/fontsではないので注意)
local.conf<?xml version='1.0'?> <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'> <fontconfig> <alias> <family>sans-serif</family> <prefer> <family>Noto Color Emoji</family> </prefer> </alias> <alias> <family>serif</family> <prefer> <family>Noto Color Emoji</family> </prefer> </alias> <alias> <family>monospace</family> <prefer> <family>Noto Color Emoji</family> </prefer> </alias> </fontconfig># Dockerfile COPY /config/local.conf /etc/fonts/ RUN fc-cache -f最後に念の為fc-cache -fでキャッシュを消して設定を読み込ませます。
これで、フォントの設定は完了です。pangoで画像を生成する
image.rbMiniMagick::Tool::Convert.new do |convert| convert.size "600x200" convert.pango("<span font='Noto Sans CJK JP' size='36864'>#{title}</span>") convert << "image.png" end2行目で指定したサイズに合わせてよしなに改行してくれます。
pangoはフォールバックフォントに対応しているので、Noto Sans CJK JPで表示できない文字が来た場合は先程設定したfontconfigに基づいてNoto Color Emojiで表示されるという仕組みです。
sizeはフォントサイズに1024掛けた数値を指定します。pangoは他にも色々リッチなテキストを生成できるので、興味ある方は公式のdocを御覧ください。
https://www.imagemagick.org/Usage/text/#pango絵文字がモノクロになる
出力された画像を見たら絵文字がモノクロだった…なんて時はOSのバージョンが古いかもです。
linuxの場合はUbuntu 18.04以降でないとカラーになりません。
自分はdebianのdockerコンテナ使ってたので、バージョンをbusterに変えて無事カラーになりました。# Dockerfile FROM ruby:2.6.5-busterおわりに
こうしてまとめると大したことやってないですが、これに辿り着くまでにえらい時間かかりました…
あと例えば画像サイズに収まるように文字数をカウントしてトリミングしたい時は絵文字の扱いに注意が必要です。
サロゲートペアとか、肌の色を表す文字とか、複数の絵文字を合成してたりとか、ややこしい仕様が色々あります…
その辺の仕組みは↓の記事に詳しく書かれてます。
https://qiita.com/_sobataro/items/47989ee4b573e0c2adfc
https://tech.drecom.co.jp/count-length-of-string-including-pictogram/絵文字、知れば知るほど難しい……
- 投稿日:2020-05-16T01:36:01+09:00
貧乏環境(t3.small)でFWのベンチマーク Rails6.x vs SpringBoot2.3.x vs Vert.x3.9.0 vs actix-web1.0.x (2020/05現在)
最近はVert.x推し。でもだれでも使えるSpringBootは実際どれくらいなの?ってことを知りたかった。
CPUが潤沢な場合にパフォーマンスが出るのはすごいよ。すごい。
でも貧乏人には大きなサーバーは無理。だから小さめの環境で効率が良くさばける?という観点でベンチマークする。
有名なベンチマークのアレではPlay Frameworkはそんなスコア出てない。
Vert.x vs Play となってPlayはScalaで脳死するのでJavaが使えるVert.xが勝つ、っていう私の前提。
プロジェクトでみんなが使えるか、って視点。環境
- EC2のt3.small
- EC2にSSHして内部からabコマンド実行(堪忍して、傾向はわかるはず)
- SpringBootとVert.xはKotlinで書いたけどまぁ、この程度のプログラムでそんな関係あるはずない(はず)
- Vert.xはwebプラグイン付き。流石に生は辛い
プログラム
簡単なJSONを吐き出すエンドポイントを実装(DBとかしらん、FWの単純性能JSOシリアライズってやつ?)
ベンチマーク
同時接続100の世界
普通にやっとくべきベンチマークの世界
Rails6 => 230.17[#/sec]
$ ab -n 1000 -c 100 http://127.0.0.1:3000/test => Requests per second: 230.17 [#/sec] (mean)Railsに温まるという概念はなかった。何回やってもこれぐらい
SpringBoot => 3196.47 #/sec
$ ab -n 1000 -c 100 http://127.0.0.1:8081/test => Requests per second: 3196.47 [#/sec] (mean)叩くたびに温まって3000ぐらいで落ち着く感じ
Vert.x => 5473.96 #/sec
$ ab -n 1000 -c 100 http://127.0.0.1:8889/test => Requests per second: 5473.96 [#/sec] (mean)叩くたびに温まって5400ぐらいで落ち着く感じ
actix-web => 5800.87 #/sec
$ ab -n 1000 -c 100 http://127.0.0.1:8082/test => Requests per second: 5800.87 [#/sec] (mean)速い。
同時接続1000の世界
Yahoo砲ぐらい
Rails6 => 87.49 #/sec
$ ab -n 5000 -c 1000 http://127.0.0.1:3000/test => Requests per second: 87.49 [#/sec] (mean)負けてる。複数台アプリを横に並べるのを検討しなくてはならない
SpringBoot => 2819.57 #/sec
$ ab -n 5000 -c 1000 http://127.0.0.1:8081/test => Requests per second: 2819.57 [#/sec] (mean)負け始めてるけどこれだけ出てればまぁ、サーバー増やさなくても良さそう
Vert.x => 5045.04 #/sec
$ ab -n 5000 -c 1000 http://127.0.0.1:8889/test => Requests per second: 5045.04 [#/sec] (mean)負け始めてるけどこれだけ出てればまぁ、サーバー増やさなくても良さそう
actix-web => 5593.91 #/sec
$ ab -n 5000 -c 1000 http://127.0.0.1:8082/test => Requests per second: 5593.91 [#/sec] (mean)速い。
同時接続10000の世界
C10Kって言われてるアレ
Rails6 => テスト完走できず
$ ab -n 20000 -c 10000 http://127.0.0.1:3000/test => テスト完走できず負けてる。複数台アプリを横に並べるのを検討しなくてはならない
SpringBoot => 完走不可、500ぐらい、3000 #/sec
$ ab -n 2000 -c 10000 http://127.0.0.1:8081/test => 完走不可、500ぐらい、3000 [#/sec] (mean)もはや信頼できない感じ
Vert.x => 4714.83 #/sec
$ ab -n 20000 -c 10000 http://127.0.0.1:8889/test => Requests per second: 4714.83 [#/sec] (mean)信頼して4500ぐらい稼げる。信頼ができる
actix-web => 1160.18 #/sec
$ ab -n 20000 -c 10000 http://127.0.0.1:8082/test => Requests per second: 1160.18 [#/sec] (mean)あれ?だめじゃん。
感想
感想1
Rails6
Railsはスパゲティーになるし、早期にパフォーマンスの問題にぶち当たって苦しむことがわかる。
2020年現在では魅力的とは言い難い構成でしょう。
きっとLaravelも同様。スクリプト言語の辛み
LAはめちゃくちゃあがって怖くなるSpring Boot
頑張ってる。誰でも見通しの良いプロジェクト構成にできるメリット。IDEのサポートも最高レベルなのが良い。
ただし、C10Kぐらいまでサービスが育ってしまうと信頼性がおちる。
同時接続1000ぐらいに抑えられるようにサーバーを横に並べる必要がありそう。
あとLAがかなり上がるが、まぁ、こんなものかレベル。Vert.x
さすがC10K問題に対応している。
見通しの良いプロジェクト構成のベストプラクティスをこれを書いた人は知らないのでそのへんは試行錯誤が必要そう。
ということは開発に参加するたびそのサービスの構成の文化を知ることから始める必要がありそうなのかも。
IDEのサポートは最高レベルなのが良い。
LAは一番低かった。actix-web
すくないアクセス数では最速。
だけどC10Kになると死。Rustの書きづらさを考えるとこれは痛い。
開発環境はVSCodeなので開発も一歩Kotlinに劣る。コンパイル鬼遅い。
クロスコンパイルは結局成功せず。Linuxへ持っていってコンパイルした。これもつらい。
バイナリ一発でサーバーにrustの環境がなくても動くのはネイティブバイナリの強み。
静的リンクしてくれるようにコンパイルしたので更に可搬性は優れる。
私がRsut慣れしてないもあるが開発は辛い。デファクトのIDEがないのが辛みかな。感想2
SpringBootはやはり鉄板。便利機能てんこ盛りできっと開発体験は最高。
C10Kになっちゃうことなんて基本ないのでぱっぱり鉄板SpringBootなんでしょう。(Playどうなんでしょ?)
もはやRailsは情弱なのかも(あ、ごめんなさい、こういう強い否定言葉は良くないですね)
プログラミングを楽しみたい人はVert.xを使えばよいのでは、って感じ。
Vert.xは肌触りSinatraです。ってことはプロジェクト構成大変そうでしょ?
私は楽しみたいのでRustのWAFが非同期周りの仕様追加を受けて落ち着くまではVert.xで楽しむつもり
actix-webは今回は残念な結果になった。actix-webの知識がなさすぎるのでなにか間違っているのかもしれない。RustのFWは非同期まわりの仕様追加したてなので、これからどんどん強くなっていくとおもうので将来に期待ですね。感想3
Railsは比較するとデプロイがめんどい。
SpringBootとVert.xはjar一個にまとまるのでデプロイめっちゃ楽。
こういうところも大事感想4
Rails系はテストコード書きやすいよね。そこは高評価。
Rustもテスト書きやすい(テスト前提の感じすらある)感想5
貧乏環境で楽したいならSpringBoot、C10Kまでなにも心配したくないならVet.x
JVM強い。感想6
Kotlin歴3日ぐらいだけどJava書ける人は何も勉強しなくてもKotlinできるはず。なぜならJavaのソースをIDEに貼り付けると自動で変換してくれるのよ。Kotlin良いよKotlin。
感想7
h2oは最近動きないので
ELB(SSL終端) -> nginx(不要かも) -> Vet.xが貧乏人には「さいつよ」とおもいました。
以上です。
- TODO: Playどうなの?
- 投稿日:2020-05-16T01:03:03+09:00
before_action
before_actionとは
railsのコントローラーのアクションを実行する前に処理を行いたいときや、同じ記述の処理をまとめたい時に使います。
基本的な書き方は以下です
hoges_controller.rbbefore_action :処理させたいメソッド名この場合だと、同じコントローラーに定義されている各メソッドの実行の前に、指定したメソッドによる処理が行われます。
もしくはresourcesメソッドと同じようにonlyやexceptなどでアクションの限定をすることもできます。
hoges_controller.rbbefore_action :処理させたいメソッド名, only: [:アクション1,:アクション2]この場合はアクション1と2が実行される前にのみ、指定した「処理したいメソッド」が実行されます。
使用例
コントローラーに下記の記述があったとします。
tweets_controller.rbclass TweetsController < ApplicationController def index @tweets = Tweet.all end def new @tweet = Tweet.new end def create Tweet.create(tweet_params) end def destroy tweet = Tweet.find(params[:id]) tweet.destroy end def edit @tweet = Tweet.find(params[:id]) ←処理が被ってる end def update tweet = Tweet.find(params[:id]) tweet.update(tweet_params) end def show @tweet = Tweet.find(params[:id]) ←処理が被ってる end private def tweet_params params.require(:tweet).permit(:name,:text) end endみてもらうと分かりますが、editとshowにおいて記述が被っています
そのため、上記のコードをリファクタリングし、
tweets_controller.rbclass TweetsController < ApplicationController before_action :set_tweet, only: [:edit, :show] def index @tweets = Tweet.all end def new @tweet = Tweet.new end def create Tweet.create(tweet_params) end def destroy tweet = Tweet.find(params[:id]) tweet.destroy end def edit end def update tweet = Tweet.find(params[:id]) tweet.update(tweet_params) end def show end private def tweet_params params.require(:tweet).permit(:name,:text) end def set_tweet ←共通の処理をまとめている @tweet = Tweet.find(params[:id]) end end共通の処理をset_tweetというメソッドでまとめ、showアクションとeditアクションの実行の際にbefore_actionで呼び出しています。
もっと分かりやすいイメージ
before_action :authenticate_user!上記はdeviseを導入した際に使えるようになるメソッドである「authenticate_user!」を指定しています。ログインしていない場合はログイン画面に遷移させるメソッドです。
よく、Amazonなどの通販サイトなどを使用していると分かりますが、商品の閲覧はログインしていなくても出来るけど、購入に進むとログインを求められますよね。
このように何らかのアクションを実行する前に行なって欲しい共通の処理をbefore_actionでは指定します。
- 投稿日:2020-05-16T00:19:19+09:00
[Rails]ユーザー情報にenumを使った実装
はじめに
初めまして、Railsを勉強中のものです。
現在SNSアプリを開発中です。よくある投稿機能や出品機能などに持たせるカラムとして、あらかじめ用意した値の中から選択させる属性を扱う場合
enum
が便利だと色んな記事で見てたのですが、
思っていたよりすんなり実装できなかったので初めてenum
を使う方の参考になればと思い記録しておきます。あくまで私が実装した内容を載せるだけなので、細かい説明は参考にしたリンクを貼っておきますのでそちらをご参照ください。
実装する内容
ユーザー情報に持たせるカラムとして、性別と年齢を
enum
を使って実装していきます。
Railsのバージョンは5.2.4
です。今回はhaml
で書いてます。それではやってみましょう。
enumで値を定義する
まずはマイグレーションファイルから。
enum
を使うカラムはinteger
型で用意します。db/migrate/xxxx_devise_create_users.rbclass DeviseCreateUsers < ActiveRecord::Migration[5.2] def change create_table :users do |t| t.string :nickname, null: false t.string :email, null: false, default: "" t.string :encrypted_password, null: false, default: "" t.string :profile, null: false # 型はintegerにする t.integer :gender, default: 0 t.integer :age, default: 0 # .省略 end end初めオプションは
null :false
にしてました。今回はそれでも問題はなかったのですが、他の記事を見るとどれもdefault: 0
で設定してたのと、公式ではこちらが推奨されてるみたいなので直しました。次にモデルで実際にデータを用意します。
app/models/user.rbclass User < ApplicationRecord enum gender: { gender_private: 0, male: 1, female: 2, others: 3 } enum age: { age_private: 0, teens: 1, twenties: 2, thirties: 3, forties: 4, fifties: 5, over_sixties: 6 } end配列で定義してる方もいました。
app/models/user.rbclass User < ApplicationRecord enum gender: [ :gender_private, :male, :female, :others ] enum age: [ :age_private, :teens, :twenties, :thirties, :forties, :fifties, :over_sixties ] endどちらでも大丈夫ですが、配列の場合対応する数字が自動で0から順になるということだけ注意が必要です。
他のカラムと区別したり、わかりやすくするために1、10、20などの数字にする方もいるみたいです。その場合はハッシュで書くしかないですね。viewでフォームを実装する
次にフォームの実装です。
私はセレクトボックス で用意しました。app/views/devise/registrations/new.html.haml= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| .field = f.label :gender %br/ = f.select :gender, options_for_select_from_enum(User, :gender) .field = f.label :age %br/ = f.select :age, options_for_select_from_enum(User, :age)フォームの実装はいろいろな書き方があると思いますが、私の場合
ArgumentError ('2' is not a valid gender):
というエラーが出てしまい、色々試した結果最終的に上の書き方になりました。
同じエラーに当たった方は、こちらの記事が詳しく書いてありました。enumを使うとArgumentError in TasksController#create => '2' is not a valid status_id
これで
enum
を使って性別と年齢を登録できるようになりましたが、現状セレクトボックスに表示されるのは英語のままになっています(age_private、teens、twenties ...)プルダウンの表示を日本語化する
こちらもいくつかやり方があるみたいです。
gemを使う方法もあるみたいですが今回は別の方法で実装します。こちらの記事を参考にさせてもらいました。
というかまんまこの通りやりました。ちなみにgemを使う方法はこちらに書いてあります。
【初心者向け】i18nを利用して、enumのf.selectオプションを日本語化する[Rails]ja.ymlファイルの作成
まず、i18機能で日本語設定をしていない場合はファイルを用意する必要があります。
Gemfile#rails5系 gem 'rails-i18n', '~> 5.1' #rails4系 gem 'rails-i18n', '~> 4.0'こちらのgemを導入、もしくは
svenfuchs/rails-i18n
こちらのファイルをコピーして、config/locales/ja.yml
ファイルを作ります。そのファイルに
enum
で定義した値を日本語に変換させるのための記述をします。config/locales/ya.yml:ja #省略 enums: user: gender: gender_private: "非公開" male: "男性" female: "女性" others: "他" #省略デフォルトの言語を日本語化
次に
application.rb
でデフォルトの言語を日本語化するための設定です。config/application.rbmodule SnsApp class Application < Rails::Application #省略 #追記 config.i18n.default_locale = :ja config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] #省略 end endこれで先程の
ja.yml
ファイルが読み込まれます。そしてもう一つファイルを作ります。
Helperメソッドを用意する
app/helpers
の下にファイルを作ります。app/helpers/enum_helper.rbmodule EnumHelper # クラスオブジェクトとカラム名を引数として呼ばれるコールバック関数です def options_for_select_from_enum(klass,column) #該当クラスのEnum型リストをハッシュで取得 enum_list = klass.send(column.to_s.pluralize) #Enum型のハッシュリストに対して、nameと日本語化文字列の配列を取得(valueは使わないため_) enum_list.map do | name , _value | # selectで使うための組み合わせ順は[ 表示値, value値 ]のため以下の通り設定 [ t("enums.#{klass.to_s.downcase}.#{column}.#{name}") , name ] end end end上の記事から丸ごと頂きました。
これでプルダウンの日本語も完了です!
最後に
今回はenumを使った実装方法を書きましたが、他に都道府県のカラムも欲しかったのでそちらは
active_hash
で実装しました。使い勝手も実装のしやすさも
active_hash
の方が楽なのですが、性別など2個か3個しかない値のためにモデルをいっぱい作るのも無駄なような感じがしたのと、
大体皆さん性別はenum
で実装してる方が多いみたいなので今回使ってみました。
実際覚えてしまえば使う場面はいっぱいあると思うので、必要に応じて積極的に使っていこうと思います。私が書いたやり方でうまくいかなかった方は、今回参考にした記事をまとめておきますので色々見て見てください!
また、無駄な記述や間違った認識などがありましたらご指摘いただけるとありがたいです!
参考にした記事
Ruby on Rails:ModelのプロパティにEnumを使う※おまけ:ラジオボタンでEnumを扱う
enumを使うとArgumentError in TasksController#create => '2' is not a valid status_id