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

149 lines
4.4 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\Services;
use App\Models\Ope;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* メール OTP 認証サービス
*
* 6桁の OTP コード生成、送信、検証、および24時間免OTPの判定を担当します
*/
class EmailOtpService
{
// OTP コード長6桁
const OTP_CODE_LENGTH = 6;
// OTP 有効期限(分)
const OTP_VALIDITY_MINUTES = 10;
// OTP 重発制限時間(秒)
const OTP_RESEND_DELAY_SECONDS = 60;
// 24時間免OTP期限時間
const OTP_FREE_PERIOD_HOURS = 24;
/**
* OTP コードを生成・保存して送信
*
* 6桁の数字コードを生成し、hash化して保存します
* 同時に有効期限と最後の送信時刻を更新します
*
* @param Ope $ope オペレータモデル
* @return string 明文のOTPコードメール送信用
*/
public function issue(Ope $ope): string
{
// 6桁の数字コードを生成
$plainCode = str_pad(random_int(0, 999999), self::OTP_CODE_LENGTH, '0', STR_PAD_LEFT);
// hash化して保存
$ope->email_otp_code_hash = Hash::make($plainCode);
$ope->email_otp_expires_at = now()->addMinutes(self::OTP_VALIDITY_MINUTES);
$ope->email_otp_last_sent_at = now();
$ope->save();
return $plainCode;
}
/**
* OTP コードを検証
*
* 入力されたコードが正しく、且つ有効期限内かを確認します
*
* @param Ope $ope オペレータモデル
* @param string $inputCode ユーザーが入力したコード
* @return bool 検証結果
*/
public function verify(Ope $ope, string $inputCode): bool
{
// 有効期限チェック
if (!$ope->email_otp_expires_at || $ope->email_otp_expires_at < now()) {
return false;
}
// hash化されたコードと比較
if (!Hash::check($inputCode, $ope->email_otp_code_hash)) {
return false;
}
// 検証成功検証完了時刻を記録し、OTPコードをクリア
$ope->email_otp_verified_at = now();
$ope->email_otp_code_hash = null;
$ope->email_otp_expires_at = null;
$ope->save();
return true;
}
/**
* OTP が最近検証されたかを確認24時間以内
*
* @param Ope $ope オペレータモデル
* @return bool true: 24時間以内に検証済み、false: 未検証または24時間以上経過
*/
public function isOtpRecent(Ope $ope): bool
{
if (!$ope->email_otp_verified_at) {
return false;
}
return $ope->email_otp_verified_at > now()->subHours(self::OTP_FREE_PERIOD_HOURS);
}
/**
* 重発可能かを確認
*
* 前回送信から60秒以上経過しているかを確認します
*
* @param Ope $ope オペレータモデル
* @return bool true: 重発可能、false: 待機中
*/
public function canResend(Ope $ope): bool
{
if (!$ope->email_otp_last_sent_at) {
return true;
}
return $ope->email_otp_last_sent_at <= now()->subSeconds(self::OTP_RESEND_DELAY_SECONDS);
}
/**
* 次の重発までの待機時間を取得(秒)
*
* @param Ope $ope オペレータモデル
* @return int 待機時間、0以下は重発可能
*/
public function getResendWaitSeconds(Ope $ope): int
{
if (!$ope->email_otp_last_sent_at) {
return 0;
}
$diffSeconds = now()->diffInSeconds($ope->email_otp_last_sent_at, absolute: false);
$waitSeconds = self::OTP_RESEND_DELAY_SECONDS - abs($diffSeconds);
return max(0, $waitSeconds);
}
/**
* OTP コードのマスク済みメールアドレスを取得
*
* abc@example.com -> abc***@example.com のようなマスク表示用
*
* @param string $email メールアドレス
* @return string マスク済みメールアドレス
*/
public function maskEmail(string $email): string
{
[$name, $domain] = explode('@', $email);
// 名前部分の最初の1文字を残して、残りは*でマスク
$maskedName = substr($name, 0, 1) . str_repeat('*', max(0, strlen($name) - 1));
return "{$maskedName}@{$domain}";
}
}