はじめに
前回2本立てと言いましたが、想像以上に内容が長くなったので、分割します。
とぅみまてぇん。
今回は問題集:初心者編の、
「Userモデルを作成して、email, password_digestを持たせる」
を進めていきます。
Userモデルを作成
RailsでUserモデルを作成します。
# Userモデルを作成(初期テーブルにemailとpassword_digestを追加)
rails g model User email:string password_digest:string
# DBに反映
rails db:migrate
Userモデルを作成したら、パスワードの暗号化とバリデーションを設定していきます。
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
validates :email, presence: true, uniqueness: true
end
1つ1つ解説していきます。
パスワードを暗号化
ユーザーのパスワードを安全に守るためにハッシュ化します。
パスワードのハッシュ化について
ハッシュ化とは一方向性の変換です。
要は元のパスワードを全く別のものに変換することです。
なぜパスワードをハッシュ化する必要があるのか。
元のパスワードをわからなくして漏洩の被害を減らすため
もしデータベースが漏洩したときに、パスワードがそのまま保存されていたら不正アクセスし放題になります。
password: password123
それを防ぐためにハッシュ化で元のパスワードをわからなくすることで、
被害を減らすことができます。
元のパスワード: "password123"
ハッシュ化: "$2a$12$uYzqsf0cHF...GxHgGOzUwG9xNQ7K8mWT2ka"
ハッシュ化したものから元のパスワードを求めるのは非常に困難になります。
has_secure_password
これを使うことで次の機能が有効になります。
- passwordおよびpassword_confirmationという仮想属性が追加される。
(実際のカラムには存在しない)。 - password_digestというカラムに、passwordのハッシュを保存する。
- パスワードのバリデーション機能(例:空でないこと、確認との一致など)
- authenticateメソッドが追加され、パスワードの検証ができる。
authenticateとは?
認証用のメソッドで、主に「入力されたパスワードが正しいかどうか」を確認するためのもの。
user = User.find_by(email: "test@example.com")
if user&.authenticate("入力されたパスワード")
# 認証成功
else
# 認証失敗
end
また、has_secure_password(= BCrypt)は毎回異なるソルト(salt)というランダムデータを混ぜてハッシュ化します。
どういうことかというと、複数のユーザーが同じパスワードを使って登録しても、ハッシュ化の結果はランダムとなり被ることはありません。
これによりハッシュ化したパスワードが漏洩しても同じパスワードを使っていることがバレることはありません。
3人の元のパスワード: "password123"
1人目ハッシュ化: "$2a$12$uYzqsf0cHF...GxHgGOzUwG9xNQ7K8mWT2ka"
2人目ハッシュ化: "a$sld&ashdioh...gpjoaspojaspdojaspdjoakjsdojka"
3人目ハッシュ化: "&asdkjas234#...sdpgihjsdighsoiddfsplrkqweq34"
そして、ユーザーがログインするときは、入力されたパスワードを登録したときと同じ方法でハッシュ化することで、データベース内のハッシュ化パスワードと比較します。
バリデーションの設定
validates :email, presence: true, uniqueness: true
emailカラムに対してバリデーションの設定をしています。
パスワードはhas_secure_passwordで自動的にバリデーション設定されているので、記述する必要はなし。
- presence: true
空でないこと。(空文字やnilを許容しない) - uniqueness: true
他のユーザーと重複していないこと。
uniquenessはアプリケーションレベルでの検証です。厳密な一意性を保証するためには、DBのインデックスにユニーク制約を加えることも重要です。
By GPTえもん
これでUserモデルの作成完了!
次にUserコントローラーを作成します。
Userコントローラーを作成
RailsでUserコントローラーを作成します。
rails g controller Users
作成したUserコントローラーに、会員登録機能を実装するための記述を書いていきます。
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
user = User.new(user_params)
if user.save
session[:user_id] = user.id
render json: {status: 'created',user: user}
else
render json: {erroes: user.errors.full_message},status: 422
end
end
private
def user_params
params.require(:user).permit(:email,:password,:password_confirmation)
end
end
1つ1つ解説していきます。
create
user = User.new(user_params)
これで先ほど作成したUserモデルの新しいインスタンスを作成。
「user_params」は許可されたパラメータ(例: email, password, name など)をハッシュで返すプライベートメソッド(private以下の記述)
Strong Parameters を使って安全にパラメータを受け取っています。
「user_params」についてはのちほど。
if user.save
バリデーションを通過して、ユーザーがデータベースに保存できたかを確認。
成功していれば中の処理に進む。
session[:user_id] = user.id
セッションに登録したユーザーIDを保存して、
ログイン状態を作成します。
Railsはsessionを使ってログイン中のユーザーを識別します。
render json: { status: 'created', user: user }
ユーザー作成成功時に、JSON形式で「status」と作成された「user」オブジェクト全体を返しています。
render json: { errors: user.errors.full_message }, status: 422
ユーザー作成に失敗した場合の処理です。
ステータスコード「422」は「Unprocessable Entity(処理できないエンティティ)」で、バリデーションエラー時によく使います。
sessionについて
RailsをAPIモードで作成した場合、セッション(session[:user_id]など)をデフォルトで使うことはできません。
なぜなら、APIモードは「JSON APIに特化した軽量なモード」であり、Cookieやセッションなどの機能を省略しているからです。
その為、Cookieやセッションを有効化する必要があります。
# config/application.rb
class Application < Rails::Application
# 以下を追加
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore, key: '_your_app_session'
end
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins "http://localhost:5173"
resource "*",
headers: :any,
credentials: true, # 超大事!必ずtrueにする
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
セッションについては以上!
「private」と「user_params」
private
def user_params
params.require(:user).permit(:email,:password,:password_confirmation)
end
private
private(プライベートメソッド)とは、クラスの内部だけで使えるメソッドのことです。
他のクラスやコントローラ、ビューなどから直接呼び出すことができないようにすることで、プログラムの安全性や設計の明確さを保ちます。
なぜ「プライベート」にするのか?
目的は「隠す」ことです。
- 内部的な処理のみに使うメソッドを外部からアクセスできないようにして、安全性や保守性を高める。
- 開発者に「これは外から触るべきじゃないメソッドですよ」と示す。
「private」以降に定義されたメソッドはすべてプライベートになります。
user_params
このuser_paramsはStrong Parametersを使用しています。
Strong Parameters(ストロングパラメータ)とは?
Strong Parametersとは、RailsでフォームやAPIから送られてきたパラメータの中から、許可されたものだけを取り出す仕組みです。
Rails4以降で導入されたセキュリティ機能で、「マスアサインメント脆弱性」からアプリを守るために使われます。
なぜ必要なのか?
user = User.new(params[:user])
上記のようなコードの場合、外部から送られてきたデータに、
例えば「admin:true」といったような権限が付与されていた場合、
誰でも管理者になれてしまいます。
そういった渡したくない情報がある場合、
渡す情報を制限することで安全な操作になります。
今回だと、:email, :password, :password_confirmationの3つを許可しています。
routes
次にroutesを記述します。
# config/routes.rb
post "/signup/" => "users#create"
今回はユーザー作成の為、データ送信を行うのでpostにしています。
React側の作成
import { useEffect, useState } from "react"
import axios from "axios";
import '../App.css'
function SignupForm() {
const [email,setEmail] = useState('')
const [password,setPassword] = useState('')
const [passwordConfirmation,setPasswordConfirmation] = useState('')
const [message,setMessage] = useState('')
const handleSubmit = async(e)=> {
e.preventDefault();
try {
const res = await axios.post('http://127.0.0.1:3000/signup',
{
user: {email,password,password_confirmation: passwordConfirmation}
},
{
headers: {
"Content-Type": "application/json"
},
withCredentials: true
}
)
setMessage('登録成功!');
}catch(err){
setMessage('登録失敗: ' + err.response?.data?.errors?.join(', '));
}
}
return (
<>
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
value={email}
onChange={e=>setEmail(e.target.value)}
placeholder="email"
/>
<input
type="password"
name="password"
value={password}
onChange={e=>setPassword(e.target.value)}
placeholder="password"
/>
<input
type="password"
name="passwordConfirmation"
value={passwordConfirmation}
onChange={e=>setPasswordConfirmation(e.target.value)}
placeholder="password"
/>
<button className="btn" type="submit">登録</button>
<p>{message}</p>
</form>
</>
)
}
export default SignupForm
1つ1つ解説していきます。
e.preventDefault()
フォーム標準の再読み込み動作を防ぎます。
stateの定義
今回は「メールアドレス」「パスワード」「パスワード再確認」「エラーメッセージ」のstateを定義しています。
handleSubmit
JSONのやりとりについては、以前投稿機能を作成した際のコードと似ていますので、詳しい解説は省きます。
const res = await axios.post('http://127.0.0.1:3000/signup',
投稿とは違い今回はデータを送信しますので、「get」ではなく「post」。
送信先は「http://127.0.0.1:3000/signup 」に指定しています。
user: {
email,
password,
password_confirmation: passwordConfirmation
}
送信するデータです。
出力
return (
<>
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
value={email}
onChange={e=>setEmail(e.target.value)}
placeholder="email"
/>
<input
type="password"
name="password"
value={password}
onChange={e=>setPassword(e.target.value)}
placeholder="password"
/>
<input
type="password"
name="passwordConfirmation"
value={passwordConfirmation}
onChange={e=>setPasswordConfirmation(e.target.value)}
placeholder="password"
/>
<button className="btn" type="submit">登録</button>
<p>{message}</p>
</form>
</>
)
コントローラーまで作っちゃいましたが、
以上で完了!!!!
まとめ
パッと見た感じ、「あ、短そうだし2本立てでいっか!」と軽い感じで思っていたんですが、深掘っていくとめっちゃ重要な機能の追加だったので、長くなりすぎないように分割させていただきました。
次回は「SessionControllerを作成して、emailとpasswordでログインできるAPIを実装」を進めていきます。
ではまた。