はじめに
パスワードのバリデーションを例に挙げます。
'password' => 'required|min:8|max:128|alpha_dash',
のようにすれば、
とりあえず最低限のバリデーションは実装できます。
ですが、alpha_dash
は全角文字が通るという、ちょっと嬉しくない仕様になっています。
まあパスワードに全角文字を使う酔狂なユーザーはいないと思うので、無視してもいい気もしますが、
うちは結構、そのあたりうるさいので、より厳格なパスワードバリデーションを実装します。
ただ、「パスワードが正しくありません」と出すだけでは不親切ですよね?
パスワードの正しくない箇所に応じて、スクショのように入力内容に応じたエラーメッセージを出すようにします。
パスワードのルール
- 8文字以上、128文字以下であること
- 少なくとも1文字以上、半角のアルファベット小文字を含むこと
- 少なくとも1文字以上、半角のアルファベット大文字を含むこと
- 少なくとも1文字以上、半角数字を含むこと
- 少なくとも1文字以上、右記の記号のいずれかを含むこと (
!?@#$%^&*()\-_=+{};:,<.>~
)
実装
こんな風にする
cd /path/to/project_dir/
php artisan make:rule StrongPassword
php artisan make:request AuthRequest
php artisan make:controller AuthController --model=User
touch resources/views/auth/login.blade.php
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class StrongPassword implements ValidationRule
{
/**
* バリデーションルールの実行
* NOTE: Laravel 9系までと10系ではバリデーションの書き方が大きく異なっているので
* 必ずリファレンスを確認すること
*
* @see https://readouble.com/laravel/10.x/ja/validation.html#custom-validation-rules
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$regexLowercase = '/[a-z]/';
$regexUppercase = '/[A-Z]/';
$regexNumber = '/[0-9]/';
$specialPattern = '!?@#$%^&*()\-_=+{};:,<.>~';
$regexSpecial = "/[{$specialPattern}]/";
$hasLowerCase = (preg_match_all($regexLowercase, $value) > 0);
$hasUpperCase = (preg_match_all($regexUppercase, $value) > 0);
$hasNumber = (preg_match_all($regexNumber, $value) > 0);
$hasSpecial = (preg_match_all($regexSpecial, $value) > 0);
if(! $hasLowerCase) $fail(':attributeは少なくとも1文字以上、半角英小文字を含む必要があります');
if(! $hasUpperCase) $fail(':attributeは少なくとも1文字以上、半角英大文字を含む必要があります');
if(! $hasNumber) $fail(':attributeは少なくとも1文字以上、半角数字を含む必要があります');
if(! $hasSpecial) $fail(":attributeは少なくとも1文字以上、右記の記号のいずれかを含む必要があります {$specialPattern}");
}
}
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use App\Rules\StrongPassword;
class AuthRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
return [
'email' => ['required','email','max:255'],
'password' => ['required','min:8','max:128', new StrongPassword()],
];
}
}
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Models\User;
use App\Http\Requests\AuthRequest;
class AuthController extends Controller
{
use AuthorizesRequests, ValidatesRequests;
public function login()
{
$viewName = Route::currentRouteName();
return view($viewName);
}
public function loginAuth(AuthRequest $request)
{
$email = $request['email'];
$password = $request['password'];
$is_auth = false;
$user = User::where('email', $email)->first();
if ($user != null) {
if (password_verify($password, $user->password)) {
$is_auth = true; // パスワード認証成功
}
}
if ($is_auth) {
session()->forget('user');
session()->push('user', $user);
$routeName = 'admin.top';
$message = trans('messages.login');
return redirect()->route($routeName)->with('success', $message);
}
else {
$res = back()->withErrors([
'password' => trans('auth.invalid')
]);
return $res;
}
}
}
@if(session('success'))
<div id="alert-message" class="alert alert-success">{{ session('success') }}</div>
@endif
@if ($errors->any())
<div id="alert-messages" class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
{{ Form::open(['url' => 'login', 'method' => 'post']) }}
{{ Form::email('email', '') }}
{{ Form::password('password') }}
<button type="submit">{{ __('action.login') }}</button>
{{ Form::close() }}
@endsection
※ViewにはLaravel Collectiveを使用しています
ポイント
バリデーションメッセージを複数出すには、 $fail
コールバックを使います。
$fail(':attributeは少なくとも1文字以上、半角英小文字を含む必要があります');
みたいな感じのやつを書けば、
エラーメッセージを複数ストックできますので、Viewで @foreach ($errors->all() as $error)
のループで回すことで全てのエラーを表示可能です。
追記
パスワードはパスワードルールオブジェクトというものを使えば
自前で実装する必要がなかったです。
すいません知りませんでした (*ノωノ)
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
class AuthRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
// パスワードルールオブジェクトについて
// https://readouble.com/laravel/10.x/ja/validation.html#validating-passwords
// https://laravel.com/api/10.x/Illuminate/Validation/Rules/Password.html
return [
'email' => ['required','email','max:255'],
'password' => [
'required',
'max:255',
Password::min(8)
->letters() // 最低1文字の文字が必要
->mixedCase() // 最低大文字小文字が1文字ずつ必要
->numbers() // 最低1文字の数字が必要
->symbols() // 最低1文字の記号が必要 右記の記号は通ることを確認済 !?@#$%^&*()\-_=+{};:,<.>~
->uncompromised(3), // 右記サイトでデータ漏洩の実績が3回以下のパスワードであること https://haveibeenpwned.com/Passwords
],
];
}
}
<?php
return [
'password' => [
'letters' => ':attributeは、少なくとも1つの文字が含まれていなければなりません。',
'mixed' => ':attributeは、少なくとも大文字と小文字を1つずつ含める必要があります。',
'numbers' => ':attributeは、少なくとも1つの数字が含まれていなければなりません。',
'symbols' => ':attributeは、少なくとも1つの記号が含まれていなければなりません。',
'uncompromised' => 'この:attributeは過去に漏洩したことのある脆弱な:attributeです。別の:attributeを入力してください。',
]
];
さらに追記
他フィールドの値を取得するには、DataAwareRule を使用するようにして、setDataで値をセットすれば利用可能になります。
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Contracts\Validation\DataAwareRule; // ★追記
class ValidDate implements DataAwareRule, ValidationRule
{
/**
* バリデーション下の全データ
*
* @var array<string, mixed>
*/
protected $data = [];
/**
* バリデーション下のデータをセット
*
* @param array<string, mixed> $data
*/
public function setData(array $data): static
{
$this->data = $data;
return $this;
}
/**
* Run the validation rule.
*
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$year = $this->data['year'];
$month = $this->data['month'];
$day = $value;
$isValidDate = checkdate($month, $day, $year);
if(! $isValidDate) $fail("設定された日付は正しくありません");
}
}
さらにさらに追記
パスワードルールに違反した場合、それぞれのエラーメッセージを出すのはユーザーフレンドリーである一方で、悪意ある第三者にパスワードルールを予測され、攻撃されるセキュリティリスクを増大させる恐れがあります。
そのため、パスワードルールに抵触した場合は単に「メールアドレス、またはパスワードが違います。」とだけ表示するように改修します。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
class AuthRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
*/
public function rules(): array
{
// パスワードルールオブジェクトについて
// https://readouble.com/laravel/10.x/ja/validation.html#validating-passwords
// https://laravel.com/api/10.x/Illuminate/Validation/Rules/Password.html
return [
'email' => ['required','email','max:255'],
'password' => [
'required',
'max:255',
Password::min(8)
->letters() // 最低1文字の文字が必要
->mixedCase() // 最低大文字小文字が1文字ずつ必要
->numbers() // 最低1文字の数字が必要
->symbols() // 最低1文字の記号が必要 右記の記号は通ることを確認済 !?@#$%^&*()\-_=+{};:,<.>~
->uncompromised(3), // 右記サイトでデータ漏洩の実績が3回以下のパスワードであること https://haveibeenpwned.com/Passwords
],
];
}
public function messages()
{
return [
'password.required' => trans('validation.required'),
'password.max' => trans('validation.max.string'),
'password.*' => trans('auth.invalid'),
];
}
}
<?php
return [
'invalid' => 'メールアドレス、またはパスワードが違います。'
];
<?php
return [
'max' => [
'numeric' => ':attributeには:max以下の数値を指定してください。',
'file' => ':attributeには:max KB以下のファイルを指定してください。',
'string' => ':attributeには:max文字以下の文字列を指定してください。',
'array' => ':attributeには:max個以下の要素を持つ配列を指定してください。',
],
'required' => ':attributeは必須です。',
'password' => [
'letters' => ':attributeは、少なくとも1つの文字が含まれていなければなりません。',
'mixed' => ':attributeは、少なくとも大文字と小文字を1つずつ含める必要があります。',
'numbers' => ':attributeは、少なくとも1つの数字が含まれていなければなりません。',
'symbols' => ':attributeは、少なくとも1つの記号が含まれていなければなりません。',
'uncompromised' => 'この:attributeは過去に漏洩したことのある脆弱な:attributeです。別の:attributeを入力してください。',
]
];