10
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails】Userをメールアドレスで検索するときは find_by を使わない

Last updated at Posted at 2025-10-26

はじめに:そんな検索方法で大丈夫か?

たとえばRailsアプリケーションで以下のようなメソッドがあったとします。

def welcome_user(email)
  user = User.find_by(email: email)

  if user
    "おかえりなさい"
  else
    "どうもはじめまして"
  end
end

このメソッドは次のような仕様を想定しています。

  • 引数で指定されたメールアドレスに合致するUserが存在するなら「おかえりなさい」を戻り値として返す
  • それ以外は「どうもはじめまして」を返す

とてもシンプルなメソッドで特に問題はなさそうですね。
.
.
.
本当に?

思いがけない結果を引き起こすケース

実は、引数のemailがユーザーの入力値だったりすると、場合によっては予想外の動きをすることがあります。
たとえば、以下のようなケースです。

DBに保存されているemailの値はjnchito@example.comだが、
画面から入力されたemailJnchito@example.comだった

この場合、welcome_userメソッドは「どうもはじめまして」を返します。
なぜなら、Jnchito@example.comでデータベースを検索しても合致するemailのUserは見つからないからです。(小文字のjと大文字のJは別物ですから!)

ログインユーザーをメールアドレスで識別するwebアプリケーションの場合、わざわざjnchito@example.comJnchito@example.comを別々のユーザーとして扱いたいと思うケースは少ないはずです。

というわけで、大文字小文字を無視してメールアドレスを検索するようにしましょう!

解決策1: 小文字に変換してから検索する

大文字と小文字を無視して扱うのであれば、先ほどのメソッドは次のように修正する必要があります。

 def welcome_user(email)
-  user = User.find_by(email: email)
+  user = User.where('LOWER(email) = ?', email.downcase).first
 
   if user
     "おかえりなさい"
   else
     "どうもはじめまして"
   end
 end

こうすれば画面から入力されたemailJnchito@example.comJNCHITO@EXAMPLE.COMだった場合でも「おかえりなさい」が表示されます。

ただし、LOWER関数を使っているので、emailカラムに設定したindexは使われない可能性が高い点に注意してください。
(usersテーブルのレコードが何万、何十万とある場合は要注意)

解決策2: Deviseを使っているならfind_for_authenticationを使う

Railsアプリケーションの場合、認証用のgemとしてDeviseを使っていることも多いと思います。

その場合は次のようにfind_for_authenticationを使ってください。

 def welcome_user(email)
-  user = User.find_by(email: email)
+  user = User.find_for_authentication(email: email)

   if user
     "おかえりなさい"
   else
     "どうもはじめまして"
   end
 end

この場合、入力値のemailがJnchito@example.comJnChItO@ExAmPlE.cOmのように大文字小文字混じりだったとしても、SQL実行時は以下のように小文字に変換されます。

SELECT "users".* 
FROM "users" 
WHERE "users"."email" = 'jnchito@example.com' 
ORDER BY "users"."id" ASC 
LIMIT 1

Deviseはemailを小文字に変換してから保存している

上のSQLを見て「あれ、"users"."email"にはLOWER関数を使わないの?」と思った人もいるかもしれません。
ですが、これで問題ありません。
なぜなら、Deviseはemailをすべて小文字に変換してからDBに保存(INSERT/UPDATE)するからです。
よって、usersテーブルに保存されるemailはすべて小文字になります。

なお、Deviseが大文字小文字を無視するデフォルトの検索キーはemailですが、これはconfig/initializers/devise.rbcase_insensitive_keysで変更することができます。

config/initializers/devise.rb
# Configure which authentication keys should be case-insensitive.
# These keys will be downcased upon creating or modifying a user and when used
# to authenticate or find a user. Default is :email.
config.case_insensitive_keys = [:email]

ふーん、でもこれって何か問題があるの?→あります!

ここでは問題をわかりやすく説明するために、welcome_userというごく単純なメソッドを使いました。
これだと別に深刻な問題を引き起こすように見えないので、「へえ、そうなんだ」で終わってしまうかもしれません。

ですが、実務だとセキュリティ上の問題を引き起こす可能性があります。
たとえば、次のようにDeviseのログイン処理をカスタマイズするために以下のようなコードを書いていたとします。

class Users::SessionsController < Devise::SessionsController
  def create
    # ⚠️ BANされたUserを検索する...?
    banned_user = User.banned.find_by(email: resource_params[:email])
    
    if banned_user
      alert = "あなたはBANされています"
      redirect_to new_session_path(resource_name), alert: alert
    else
      # BANされていないので、デフォルトのログイン処理を実行する
      super
    end
  end
end

上のコード例では、BANされたUserをログインさせたくないので、find_by(email: resource_params[:email])を使ってUserを検索しています。

ですが、これだと先ほど説明したように、利用者がわざと大文字小文字を混在させたメールアドレスを入力してきた場合、banned_usernilになり、ログインに成功してしまいます(superで呼び出されるDeviseのログイン処理は、emailの大文字小文字を無視するため)。

そこで find_for_authentication

こういう場合はfind_for_authenticationメソッドを使って、大文字小文字を無視してemailを検索するようにしましょう。

-banned_user = User.banned.find_by(email: resource_params[:email])
+banned_user = User.banned.find_for_authentication(email: resource_params[:email])

こうすれば大文字小文字を無視してUserを検索するので、BANされたUserはログインできなくなります!

おまけ:怪しいコードがないか調べてみよう

以下のgit grepコマンドを実行すると、プロジェクト内にUser.find_by(email: email)User.some_scope.find_by email: emailのようなコードがないか検索することができます。

git grep -P 'find_by[\s(]\s*email:'

なお、このgit grepコマンドでは正規表現を使っています。
正規表現が苦手な人は、僕が以前書いた以下の記事を読んでもらえれば意味がわかるようになるはずです!

まとめ

というわけで、この記事ではUserをメールアドレスで検索するときは find_by を使っちゃダメですよ、という話を書いてみました。

Userを直接メールアドレスで検索するケースはそこまで多くないかもしれませんが、たまにそういう機会がやってきたときに「大文字小文字を無視するかどうか」まで意識が回らず、ついUser.find_by(email: email)のようなコードを書いてしまいがちなんですよねえ。。(経験者談)

ドキッとした人はこのドキュメントをチーム内で共有して、これまでにこういうコードを書いてこなかったか、チェックしてみてください!

あわせて読みたい

記事内で使っているメールアドレスのドメインをexample.comにしている理由は、こちらの記事に載っています↓

10
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?