krgm.so-manager-dev.com/app/Http/Controllers/Auth/LoginController.php
OU.ZAIKOU 13d2ecfceb
All checks were successful
Deploy main / deploy (push) Successful in 25s
【ログイン】二重認証実装
2026-01-21 22:37:38 +09:00

315 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Mail\EmailOtpMail;
use App\Services\EmailOtpService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class LoginController extends Controller
{
/**
* ログイン成功後のリダイレクト先
*
* @var string
*/
protected $redirectTo = '/home';
/**
* コントローラーのインスタンス作成
* Laravel 12変更点ミドルウェアは routes/web.php で処理するように変更
*
* @return void
*/
public function __construct()
{
// Laravel 12: ミドルウェアは routes/web.php で処理
// Laravel 5.7: $this->middleware('guest')->except('logout'); を使用していた
}
/**
* ログインフォームを表示
*
* @return \Illuminate\View\View
*/
public function showLoginForm()
{
return view('auth.login');
}
/**
* ログインリクエストを処理
* Laravel 12変更点AuthenticatesUsersトレイトを使わず独自実装
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
public function login(Request $request)
{
$this->validateLogin($request);
// ログイン試行回数制限チェック
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return $this->sendLockoutResponse($request);
}
// ログイン認証試行
if ($this->attemptLogin($request)) {
return $this->sendLoginResponse($request);
}
// ログイン失敗時の処理
$this->incrementLoginAttempts($request);
return $this->sendFailedLoginResponse($request);
}
/**
* ログインリクエストのバリデーション
*
* 仕様上の入力名(フォーム側)は ope_id / ope_pass のまま維持し、
* 内部の認証キーは login_id に寄せるlogin_id 統一)。
*
* @param \Illuminate\Http\Request $request
* @return void
*/
protected function validateLogin(Request $request)
{
// 個別未入力メッセージ仕様1,2
$request->validate([
'ope_id' => 'required|string', // フォームの入力名は現状維持(実体は login_id
'ope_pass' => 'required|string',
], [
'ope_id.required' => 'ログインIDが未入力です。',
'ope_pass.required' => 'パスワードが未入力です。',
]);
}
/**
* ログイン認証を試行
*
*
* - 画面入力ope_id= DBの login_id として扱う
* - 退職フラグチェックも login_id で取得して判定する
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
protected function attemptLogin(Request $request)
{
// 先にIDのみでオペレータ取得して退職フラグを確認仕様5-1
$loginId = $request->input('ope_id'); // 入力名は ope_id だが中身は login_id
$operator = \App\Models\Ope::where('login_id', $loginId)->first();
if ($operator && (int)($operator->ope_quit_flag) === 1) {
// 退職扱いは認証失敗と同じメッセージ仕様5-1 と 3/4 統一表示)
return false;
}
// 認証実行credentials() で login_id / password を渡す)
return Auth::attempt($this->credentials($request), false);
}
/**
* 認証用の資格情報を取得
*
*
* - 認証IDを login_id に統一
* - パスワード入力ope_passは Auth 側の password にマッピング
*
* @param \Illuminate\Http\Request $request
* @return array
*/
protected function credentials(Request $request)
{
return [
'login_id' => $request->input('ope_id'), // フォーム入力ope_id→ DB列 login_id
'password' => $request->input('ope_pass'), // フォーム入力ope_pass→ 認証用 password
];
}
/**
* ログイン成功時のレスポンス
*
* OTP認証チェック
* - 24時間以内に OTP 認証済みの場合:/home にリダイレクト
* - 未認証の場合OTP メール送信 → /otp にリダイレクト
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
protected function sendLoginResponse(Request $request)
{
$request->session()->regenerate();
$this->clearLoginAttempts($request);
// 仕様5: ログインIDをセッション保持
// ここで保持する値も login_id入力名は ope_id のまま)
$request->session()->put('login_ope_id', $request->input('ope_id'));
// OTP認証チェック
$otpService = app(EmailOtpService::class);
$user = Auth::user();
// 24時間以内に OTP 認証済みの場合
if ($otpService->isOtpRecent($user)) {
return redirect()->intended($this->redirectTo);
}
// OTP 未認証の場合OTP コード発行 → メール送信 → /otp にリダイレクト
try {
$otpCode = $otpService->issue($user);
// ope_mail はセミコロン区切りで複数アドレスを保持する可能性があるため、最初のアドレスのみ抽出
$operatorEmails = explode(';', trim($user->ope_mail));
$primaryEmail = trim($operatorEmails[0] ?? $user->ope_mail);
Log::info('OTP メール送信開始: ' . $primaryEmail);
Mail::to($primaryEmail)->send(new EmailOtpMail(
$otpCode,
$user->name ?? 'ユーザー'
));
Log::info('OTP メール送信完了: ' . $primaryEmail);
return redirect()->route('otp.show')
->with('info', 'OTP認証コードをメール送信しました。');
} catch (\Exception $e) {
Log::error('OTP issue/send failed: ' . $e->getMessage(), [
'exception' => $e,
'user_id' => $user->ope_id ?? null,
'user_email' => $user->ope_mail ?? null,
]);
// メール送信エラー時は home にリダイレクトするか、カスタムエラーを返す
return redirect($this->redirectTo)
->with('warning', 'OTP認証メールの送信に失敗しました。');
}
}
/**
* ログイン失敗時のレスポンス
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
protected function sendFailedLoginResponse(Request $request)
{
// 画面側のエラー表示キーは仕様に合わせて ope_id のまま
throw ValidationException::withMessages([
'ope_id' => [trans('auth.failed')],
]);
}
/**
* ログアウト処理
* Laravel 12変更点セッション無効化処理を追加
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
public function logout(Request $request)
{
Auth::logout();
// Laravel 12: セッション無効化とトークン再生成を明示的に実行
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/login');
}
/**
* ログイン試行回数が上限を超えているかチェック
* Laravel 12変更点RateLimiterファサードを使用
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
protected function hasTooManyLoginAttempts(Request $request)
{
return RateLimiter::tooManyAttempts(
$this->throttleKey($request),
5
);
}
/**
* ログイン試行回数をインクリメント
*
* @param \Illuminate\Http\Request $request
* @return void
*/
protected function incrementLoginAttempts(Request $request)
{
RateLimiter::hit(
$this->throttleKey($request),
60
);
}
/**
* ログイン試行回数制限をクリア
*
* @param \Illuminate\Http\Request $request
* @return void
*/
protected function clearLoginAttempts(Request $request)
{
RateLimiter::clear($this->throttleKey($request));
}
/**
* ロックアウト発生時のイベント処理
*
* @param \Illuminate\Http\Request $request
* @return void
*/
protected function fireLockoutEvent(Request $request)
{
// 必要に応じてイベントを発火
}
/**
* レート制限用のスロットルキーを取得
*
*
* - 画面入力名は ope_id のまま
* - ただし内容は login_id を想定ログインID文字列
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function throttleKey(Request $request)
{
return Str::lower($request->input('ope_id')) . '|' . $request->ip();
}
/**
* ロックアウト時のレスポンス
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
protected function sendLockoutResponse(Request $request)
{
$seconds = RateLimiter::availableIn(
$this->throttleKey($request)
);
// 画面側のエラー表示キーは仕様に合わせて ope_id のまま
throw ValidationException::withMessages([
'ope_id' => [trans('auth.throttle', ['seconds' => $seconds])],
]);
}
}