Laravelでパスワードを扱いたいけど、どうすればいいんだろう….
パスワードを安全に扱いたいし、パスワードの照合とかさせたいな…
パスワードの一致とかもどうやるんだろう…
こんな疑問を解決します。
Laravelでパスワードを登録するときに、ハッシュ化せずにそのままデータを登録していませんか?
それ非常にセキュリティ面で危険です。
ハッシュ化とは、パスワードを暗号化させるので、パスワードを安全に保管するもの。
この記事では、ハッシュ化やハッシュ化させたパスワードが一致するか調べる方法などについて解説しているので、ぜひ最後まで読んでくださいね!
- パスワードのハッシュ化や照合
- パスワードとパスワード確認用が一致するか
休日で空いた時間の暇つぶしを探せるアプリを公開しています。
Laravelでパスワードをハッシュ化する
セキュリティの面から入力したパスワードをそのままデータベースに登録するのはやめましょう。
パスワードをデータベースに登録するなら、ハッシュ化させて安全にパスワードを扱うべきです。
LaravelではHash::makeを使うことで、入力したパスワードをハッシュ化させることができます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;←←←←←←追加することを忘れないで!!!
class Company extends Model
{
use HasFactory;
// モデルに関連付けるテーブル
protected $table = 'companies';
// テーブルに関連付ける主キー
protected $primaryKey = 'company_id';
// 登録・更新可能なカラムの指定
protected $fillable = [
'company_name',
'password',
'created_at',
'updated_at'
];
/**
* companiesテーブルにデータを登録する
*
* @param string $companyName 会社名
* @param string $password パスワード
* @return コレクション配列
*/
public function insertCompany($companyName, $password)
{
return $this->create([
'company_name' => $companyName,
'password' => Hash::make($password),
]);
}
}
上記のようにHash::make(パスワード)とすれば、以下のようにハッシュ化されます。
Hashを使うにはuse Illuminate\Support\Facades\Hash;を追加する必要があることにも注意しましょう!
Laravelでパスワードを照会する
パスワードのハッシュでもう一つ覚えておきたいのが、
入力されたパスワードとハッシュ化されたパスワードが一致しているか調べたい場合はどうすんねん
ってこと。この場合は、Hash::checkを使いましょう。
// フォームから入力された会社パスワードを取得
$password = $request->password;
// パスワードをハッシュ化
①$hash_password = Hash::make($password);
②$hash_password = Hash::make('bbb');
if (Hash::check($password, $hash_password)) {
dd('パスワード一致');
} else {
dd('パスワードが一致しない');
}
Hash::check(‘入力されたパスワード’, ‘ハッシュ化されたパスワード’)のように使います。
もし入力されたパスワードがaaaの場合、①の時はパスワードが一致し、②の時はbbbのハッシュ化なのでパスワードは一致しません。
このようにHash::checkを使えば、パスワードの照会ができます。
Hash::checkを使わなくてもPHPのパスワード照会する関数であるpassword_verifyを使ってもできます。
// フォームから入力された会社パスワードを取得
$password = $request->password;
// パスワードをハッシュ化
$hash_password = Hash::make($password);
if (password_verify($password, $hash_password)) {
dd('パスワード一致');
} else {
dd('パスワードが一致しない');
}
実務で使うなら、入力したパスワードとすでに保存しているハッシュ化されたパスワードをチェックし、同じパスワードであればエラーにするなどが挙げられます。
/**
* companiesテーブルからcompany_id = 1のレコード1件取得
*/
public function findByCompanyId()
{
return $this
->where('company_id', 1)
->firstOrFail();
}
// フォームから入力された会社パスワードを取得
$password = $request->password;
// companiesテーブルからcompany_id =1のレコード1件を取得
$company = $this->company->findByCompanyId();
// 上記のレコードよりハッシュ化されたパスワードを取得
$hash_password = $company->password;
if (password_verify($password, $hash_password)) {
dd('パスワード一致');
} else {
dd('パスワードが一致しない');
}
こんな感じでできます。
設計におすすめの本
良いコード/悪いコードで学ぶ設計入門―保守しやすい 成長し続けるコードの書き方
Laravelの基礎におすすめの本
PHPフレームワーク Laravel入門 第2版
パスワードとパスワード確認用が一致するか
登録画面でパスワードとパスワード確認用が一致するかのバリデーションを実装していきます。
また、編集画面で現在のパスワード(DBでハッシュ化されたパスワードの値)と入力したパスワードが一致するかも解説していきます。
本記事で動作確認した環境は、PHP7.4,Laravel8系です。bootstrapを導入しているので、フォームはbootstrapを参考にしてます。
登録画面でパスワードとパスワード確認用が一致するか
パスワードとパスワード確認用が一致するかは、結論sameを使えば実装できます。
登録画面
@extends('layouts.app')
@section('content')
<div class="container small">
<h1>ユーザーの登録画面</h1>
<form action="{{ route('user.store') }}" method="POST">
@csrf
<fieldset>
<div class="form-group">
<label for="user_name">{{ __('ユーザー名') }}<span class="badge badge-danger ml-2">{{ __('必須') }}</span></label>
<input type="text" class="form-control {{ $errors->has('user_name') ? 'is-invalid' : '' }}" name="user_name" id="user_name">
@if ($errors->has('user_name'))
<span class="invalid-feedback" role="alert">
{{ $errors->first('user_name') }}
</span>
@endif
</div>
<div class="form-group">
<label for="email">{{ __('Eメール') }}<span class="badge badge-danger ml-2">{{ __('必須') }}</span></label>
<input type="email" class="form-control {{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" id="email" value="{{ old('email') }}">
@if ($errors->has('email'))
<span class="invalid-feedback" role="alert">
{{ $errors->first('email') }}
</span>
@endif
</div>
<div class="form-group">
<label for="user_password">{{ __('パスワード') }}<span class="badge badge-danger ml-2">{{ __('必須') }}</span></label>
<input type="password" class="form-control {{ $errors->has('user_password') ? ' is-invalid' : '' }}" name="user_password" id="user_password" value="{{ old('user_password') }}">
@if ($errors->has('user_password'))
<span class="invalid-feedback" role="alert">
{{ $errors->first('user_password') }}
</span>
@endif
</div>
<div class="form-group">
<label for="confirm_password">{{ __('パスワード確認用') }}<span class="badge badge-danger ml-2">{{ __('必須') }}</span></label>
<input type="password" class="form-control {{ $errors->has('confirm_password') ? ' is-invalid' : '' }}" name="confirm_password" id="confirm_password" value="{{ old('confirm_password') }}">
@if ($errors->has('confirm_password'))
<span class="invalid-feedback" role="alert">
{{ $errors->first('confirm_password') }}
</span>
@endif
</div>
<button type="submit" class="btn btn-success">
{{ __('登録') }}
</button>
</div>
</fieldset>
</form>
</div>
@endsection
以下のような画面が作成できます。
フォームリクエストのバリーデーションはこんな感じ。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
// ユーザー名
'user_name' => 'required', // 必須
// メールアドレス
'email' => 'required', // 必須
// パスワード
'user_password' => [
'required', // 必須
'min:8', // 最低8文字以上
'max:16', // 最高16文字まで
'regex:/^(?=.*?[a-zA-Z])(?=.*?\d)[a-zA-Z\d]/' // 正規表現を使って、半角英数字混在
],
// パスワード確認用
'confirm_password' => [
'required', // 必須
'same:user_password', // user_passwordと値が同じか
],
];
}
public function messages()
{
return [
'user_name.required' => 'ユーザー名は必須です。',
'email.required' => 'メールアドレスは必須です。',
'user_password.required' => 'パスワードは必須です。',
'user_password.min' => 'パスワードは :min桁以上で入力してください。',
'user_password.max' => 'パスワードは :max桁以下で入力してください。',
'user_password.regex' => 'パスワードは半角英数字混在で入力してください。',
'confirm_password.required' => 'パスワード確認用は必須です。',
'confirm_password.same' => 'パスワードとパスワード確認用が一致しません。',
];
}
}
注目すべきはsame:passwordの部分。
same:〇〇と書けば、〇〇と同じ値であるかバリデーションします。
これで目的のバリデーションである、パスワードとパスワード確認用の値が一致するか検証できます。
ついでに、編集画面で現在のパスワードと入力したパスワードが一致するかも解説します。
現在のパスワードと入力したパスワードが一致するか
ここからは、編集画面で現在のパスワードと入力したパスワードが一致するか解説していきます。
まずは、画面レイアウト
@extends('layouts.app')
@section('content')
<div class="container small">
<h1>ユーザー編集</h1>
<form action="{{ route('user.update') }}" method="POST">
@csrf
<input type="hidden" name="user_id" value="{{ $user->user_id }}">
<fieldset>
<div class="form-group">
<label for="user_name">{{ __('ユーザー名') }}<span class="badge badge-danger ml-2">{{ __('必須') }}</span></label>
<input type="text" class="form-control" name="user_name" id="user_name" value="{{ old('email', $user->user_name) }}">
</div>
@if ($errors->has('user_name'))
<span class="invalid-feedback" role="alert">
{{ $errors->first('user_name') }}
</span>
@endif
<div class="form-group">
<label for="email">{{ __('メールアドレス') }}<span class="badge badge-danger ml-2">{{ __('必須') }}</span></label>
<input type="text" class="form-control {{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" id="email" value="{{ old('email', $user->email) }}">
@if ($errors->has('email'))
<span class="invalid-feedback" role="alert">
{{ $errors->first('email') }}
</span>
@endif
</div>
<div class="form-group">
<label for="current_password">{{ __('現在のパスワード') }}<span class="badge badge-danger ml-2">{{ __('必須') }}</span></label>
<input type="password" class="form-control {{ $errors->has('current_password') ? ' is-invalid' : '' }}" name="current_password" id="current_password">
@if ($errors->has('current_password'))
<span class="invalid-feedback" role="alert">
{{ $errors->first('current_password') }}
</span>
@endif
</div>
<div class="form-group">
<label for="user_password">{{ __('新しいパスワード') }}<span class="badge badge-danger ml-2">{{ __('必須') }}</span></label>
<input type="password" class="form-control {{ $errors->has('user_password') ? ' is-invalid' : '' }}" name="user_password" id="user_password">
@if ($errors->has('user_password'))
<span class="invalid-feedback" role="alert">
{{ $errors->first('user_password') }}
</span>
@endif
</div>
<div class="form-group">
<label for="confirm_password">{{ __('新しいパスワード確認用') }}<span class="badge badge-danger ml-2">{{ __('必須') }}</span></label>
<input type="password" class="form-control {{ $errors->has('confirm_password') ? ' is-invalid' : '' }}" name="confirm_password" id="confirm_password">
@if ($errors->has('confirm_password'))
<span class="invalid-feedback" role="alert">
{{ $errors->first('confirm_password') }}
</span>
@endif
</div>
<div class="d-flex justify-content-between pt-3">
<button type="submit" class="btn btn-success">
{{ __('更新') }}
</button>
</div>
</fieldset>
</form>
</div>
@endsection
以下のような画面になります。
- 現在のパスワードにパスワードを入力して、一致しなかったらエラーメッセージ
- 新しいパスワードと新しいパスワード確認用には、登録画面で実装したバリデーションを使う
このような仕様になります。
上記を考慮したパスワードのバリデーションは以下のようになります。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Request; // 追加
use Illuminate\Support\Facades\Hash; // 追加
use App\Models\User; //追加
class UserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(Request $request)
{
if (isset($request->user_id)) {
// 編集画面のバリデーション(編集画面ではinputのtype="hidden"でuser_idの値が送られてくるので値があるかで登録画面か編集画面か判別できる)
// 現在のパスワードフォームに入力したパスワード
$pass = $request->current_password;
// user_idをもとに、usersテーブルから一意にレコードを絞り込む
$user = User::find($request->user_id);
// 現在のパスワードを取得
$currentPass = $user->password;
return [
// ユーザー名
'user_name' => 'required', // 必須
// メールアドレス
'email' => 'required', // 必須
// 現在のパスワード
'current_password' => [ // 無名関数を使ってできる。
function ($attribute, $value, $fail) use ($pass, $currentPass) { // use(変数)でfunctionn内で使用する変数を宣言する。
if (!Hash::check($pass, $currentPass)) {
// $passはフォームに入力した値で、$currentPassはusersのDBにあるハッシュ化されたパスワードで、
// Hash::check('入力パスワード', 'ハッシュ化されたパスワード')で比較できる。
return $fail('現在のパスワードと入力したパスワードが一致しません。');
}
}
],
// 新しいパスワード
'user_password' => [
'required', // 必須
'min:8', // 最低8文字以上
'max:16', // 最高16文字まで
'regex:/^(?=.*?[a-zA-Z])(?=.*?\d)[a-zA-Z\d]/' // 正規表現を使って、半角英数字混在
],
// 新しいパスワード確認用
'confirm_password' => [
'required', // 必須
'same:user_password', // user_passwordと値が同じか
],
];
} else {
// 登録画面のバリデーション
return [
// ユーザー名
'user_name' => 'required', // 必須
// メールアドレス
'email' => 'required', // 必須
// パスワード
'user_password' => [
'required', // 必須
'min:8', // 最低8文字以上
'max:16', // 最高16文字まで
'regex:/^(?=.*?[a-zA-Z])(?=.*?\d)[a-zA-Z\d]/' // 正規表現を使って、半角英数字混在
],
// パスワード確認用
'confirm_password' => [
'required', // 必須
'same:user_password', // user_passwordと値が同じか
],
];
}
}
public function messages()
{
return [
'user_name.required' => 'ユーザー名は必須です。',
'email.required' => 'メールアドレスは必須です。',
'user_password.required' => 'パスワードは必須です。',
'user_password.min' => 'パスワードは :min桁以上で入力してください。',
'user_password.max' => 'パスワードは :max桁以下で入力してください。',
'user_password.regex' => 'パスワードは半角英数字混在で入力してください。',
'confirm_password.required' => 'パスワード確認用は必須です。',
'confirm_password.same' => 'パスワードとパスワード確認用が一致しません。',
];
}
}
コメントアウトでほぼ解説していますが、注意点として冒頭にRequest,Hash,Userの追加を忘れないようにしましょう。
また、編集画面と登録画面でバリデーションが異なるので、そこも注意です。
休日で空いた時間の暇つぶしを探せるアプリを公開しています。
コメント