返回

Laravel 11 Breeze: 手机号/邮箱登录实现教程

php

Laravel 11 Breeze: 使用手机号或邮箱登录

遇到个事儿:想在 Laravel 11 里,用 Breeze 实现个登录功能,允许用户输手机号或者邮箱都能登录。 默认的 Breeze 只支持邮箱,这咋整?

问题根源

Breeze 默认的 LoginRequest.php 文件里,写死了用 email 字段做验证和登录:

// ...
    public function rules(): array
    {
        return [
            'email' => ['required', 'email','exists:users,email'],
            'password' => ['required', 'string'],
        ];
    }
// ...
    public function authenticate(): void
    {
        $this->ensureIsNotRateLimited();

        if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
           // ...
        }
    // ...
    }
// ...
    public function throttleKey(): string
    {
        return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
    }

关键点有这么几个:

  1. rules() 方法里,规定了必须是 email 格式,且在 users 表里得有。
  2. authenticate() 方法,直接用了 $this->only('email', 'password') 去尝试登录。
  3. throttleKey() 方法, 也直接拿 email 字段来做登录限制。

要实现手机号/邮箱登录,就得把这几个地方都改了。

解决方法

1. 修改数据表结构 (可选)

如果你的 users 表里,手机号和邮箱是分开的俩字段(比如 emailphone_number),那这步可以跳过。

如果你想用一个字段(比如就叫 username)来存手机号或者邮箱,那就得改下表结构:

  1. 创建迁移文件:

    php artisan make:migration alter_users_table_add_username_column
    
  2. 修改迁移文件:

    // database/migrations/xxxx_xx_xx_xxxxxx_alter_users_table_add_username_column.php
    
    use Illuminate\Database\Migrations\Migration;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Support\Facades\Schema;
    
    return new class extends Migration
    {
        public function up(): void
        {
            Schema::table('users', function (Blueprint $table) {
                $table->string('username')->unique()->after('name'); // after('name') 只是放个位置,你随意.
                $table->dropUnique('users_email_unique'); // 删除 email 的唯一索引, 因为 username 要变成唯一的了.
                $table->dropColumn('email'); //删掉 email 列
            });
        }
    
        public function down(): void
        {
            Schema::table('users', function (Blueprint $table) {
                $table->string('email')->unique()->after('name');
                $table->dropUnique('users_username_unique');
                $table->dropColumn('username');
    
            });
        }
    };
    
    
  3. 执行迁移:

    php artisan migrate
    
  • 温馨提示, 你最好在迁移文件执行前备份你的数据库.

2. 修改 LoginRequest.php

这是最关键的一步,要改三个地方:

<?php

namespace App\Http\Requests\Auth;

use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Facades\Validator; // 引入 Validator

class LoginRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'login' => ['required', 'string'], // 改成 login 字段
            'password' => ['required', 'string'],
        ];
    }
    public function messages():array
    {
      return [
          'login.required' => '请输入邮箱或手机号',
      ];
    }

     /**
     * 自定义验证逻辑。
     */
     protected function prepareForValidation()
      {

          $loginValue = $this->input('login');


          $validator = Validator::make(['login' => $loginValue], [
              'login' => 'email',
          ]);


        //如果是邮箱
          if (! $validator->fails()) {
              $this->merge([
                'email' => $loginValue,
                'username' => $loginValue, //假设你有username,就这么用.
                ]);

          }else
             {
                $this->merge([
                'phone_number' => $loginValue, //手机号
            ]);
          }
      }

    public function authenticate(): void
    {
        $this->ensureIsNotRateLimited();

        $loginType = filter_var($this->input('login'), FILTER_VALIDATE_EMAIL) ? 'email' : 'phone_number';
          //如果 users 表里 统一使用一个 username 字段
          //$loginType = filter_var($this->input('login'), FILTER_VALIDATE_EMAIL) ? 'username' : 'username';
        if (!Auth::attempt([$loginType => $this->input('login'), 'password' => $this->input('password')], $this->boolean('remember')))
             {
            RateLimiter::hit($this->throttleKey());

            throw ValidationException::withMessages([
                'login' => trans('auth.failed'),  // 这里的 'login' 对应前端的 name="login"
            ]);
        }

        RateLimiter::clear($this->throttleKey());
    }

    public function ensureIsNotRateLimited(): void
    {
        if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
            return;
        }

        event(new Lockout($this));

        $seconds = RateLimiter::availableIn($this->throttleKey());

        throw ValidationException::withMessages([
            'login' => trans('auth.throttle', [
                'seconds' => $seconds,
                'minutes' => ceil($seconds / 60),
            ]),
        ]);
    }

    public function throttleKey(): string
    {

        return Str::transliterate(Str::lower($this->input('login')).'|'.$this->ip());
    }
}

主要变动说明:

  1. rules() 方法
    将验证规则改为验证 login 字段,
    新增 messages() 自定义验证提示消息.
  2. 新增 prepareForValidation() 方法 :
    自定义预验证逻辑,判断如果输入的内容符合邮箱格式, 则合并 'email' => $loginValue,反之, 则合并 phone_number
  3. authenticate() 方法 :
    filter_var 判断一下,看 login 字段是邮箱还是手机号,然后构造对应的查询条件。
  4. throttleKey() :
    限流的key 也得改, 直接使用 login 字段, 防御暴力破解。

3. 修改登录表单 (login.blade.php)

把表单里的 email 字段改成 login

<!-- resources/views/auth/login.blade.php -->

<form method="POST" action="{{ route('login') }}">
    @csrf

    <!-- Email or Phone Number Address -->
    <div>
        <label for="login">Email or Phone Number</label>
        <input id="login" type="text" name="login" value="{{ old('login') }}" required autofocus autocomplete="username" />
        <x-input-error :messages="$errors->get('login')" class="mt-2" />
    </div>

    <!-- Password -->
    <div class="mt-4">
        <label for="password">Password</label>
        <input id="password" type="password" name="password" required autocomplete="current-password" />
        <x-input-error :messages="$errors->get('password')" class="mt-2" />
    </div>

    <!-- Remember Me -->
    <div class="block mt-4">
        <label for="remember_me">
            <input id="remember_me" type="checkbox" name="remember">
            <span>Remember me</span>
        </label>
    </div>

    <div>
        <a href="{{ route('password.request') }}">
            Forgot your password?
        </a>

        <button type="submit">
            Log in
        </button>
    </div>
</form>

进阶使用和安全建议

  • 手机号格式验证 : 你可以在prepareForValidation() 方法使用更严格的正则, 对手机号进行强校验。

  • Throttle 限流 : 虽然改了 throttleKey(),但如果攻击者猜到你的某个用户名,还是可以针对性攻击。 可以考虑结合其他方式,如验证码。

  • 数据库字段 : 设计数据库的时候就考虑到这点能省很多事,比如用一个 login_id 字段,存各种类型的唯一标识。

  • 双因子认证(2FA) : 更安全的做法是,登录成功后,再加一层验证,比如短信验证码、邮箱验证码。

希望这详细步骤能解决你在用 Laravel 11 Breeze 时遇到的登录问题。