[ Laravel 从入门到精通 ] 用户认证与授权系列 —— 用户注册登录流程及多字段登录实现

注册登录流程

我们接着上一篇教程继续往下走。如果系统中还没有任何用户的话,首先需要点击注册按钮进入注册页面:

填写注册信息,点击注册按钮,完成注册。如果设置了邮箱验证功能,需要验证邮箱后才能登录,否则会直接登录,并跳转到 home 路由:

如果在登录和注册过程中验证表单失败,则会在对应字段提示错误信息:

对应的验证规则在相应的控制器中都可以看到,如果你对这一块还不太熟悉,可以查看在 Laravel 控制器中进行表单请求字段验证这篇教程。

获取登录用户信息

用户登录成功后,就可以获取登录用户信息了,我们可以通过多种方式获取用户信息。

通过 Auth 门面

我们可以在控制器中通过 Auth 门面快速获取当前登录用户信息:

$user = Auth::user();  // 获取当前登录用户的完整信息
$userId = Auth::id();  // 获取当前登录用户 ID

上述返回的 $user 数据是一个 User 模型实例,可以通过它获取用户所有信息。此外,你还可以通过 Auth 门面提供的其他方法快速进行一些常见判断,比如判断用户是否已经登录,可以通过 Auth::check() 方法,如果已登录,该方法返回 true,否则返回 false。相对的,还有 Auth::guest() 方法判断用户是否未登录,逻辑与 Auth::check() 方法刚好相反。

Blade 指令

我们还可以在 Blade 视图上使用上述门面方法进行流程判断,此外,Blade 模板引擎还为我们提供了对应的快捷指令:

@auth
    // 用户已登录...
@endauth

@guest
    // 用户未登录...
@endguest

通过 Request 实例

除了 Auth 门面外,我们还可以在控制器方法中通过 Request 请求对象实例获取登录用户信息:

public function update(Request $request)
{
    $user = $request->user();  # 获取当前登录用户实例
}

获取到的 $user 数据和通过 Auth::user() 返回结果完全一致。

学院君注:尽量不要在控制器和视图以外的地方使用 Auth 门面获取用户信息,在其他地方获取可以通过数据传递的方式,因为服务类或模型类的应用场景不一定是 Web 层,有可能出现获取不到 Session 而导致获取数据为空的情况。

登录失败次数限制

需要注意的是对于用户登录功能而言,框架底层会校验登录失败次数,超过指定阈值会报错:

默认阈值是 1 分钟内尝试 5 次,超过这个次数就会提示失败次数过多,过段时间再来尝试。对于未登录用户而言,这个限制维度是基于 IP 的。对应的实现细节位于 Illuminate\Foundation\Auth\ThrottlesLogins 中。

如果你想要修改这个阈值,可以在 LoginController 控制器中通过设置如下属性来实现:

// 单位时间内最大登录尝试次数
protected $maxAttempts = 3;
// 单位时间值
protected $decayMinutes = 30;

这次,我们设置半小时(30分钟)内只能尝试 3 次。再次尝试登录功能,检查设置是否生效:

第四次尝试登录,就会报错了。

支持用户名/邮箱登录

通过用户名登录

Laravel 支持在用户名和邮箱之间切换登录,默认是通过注册邮箱登录的,如果你想要调整为通过用户名登录,很简单,在 LoginController 控制器中定义一个 username() 方法重写 AuthenticatesUsers Trait 中的同名方法即可,Laravel 底层通过该方法定义登录字段名,而不是写死的,我们在该方法中返回登录字段名:

public function username()
{
    return 'name';
}

然后把前端视图对应的登录表单字段调整为 name。这样,就可以基于用户名进行登录了。

通过用户名或邮箱登录

实际开发中,有时候我们的登录字段可能需要同时支持用户名/注册邮箱/手机号登录,即用户既可以在登录框中输入邮箱,也可以输入用户名,这个时候,Laravel 框架底层自带的 UserProvider 已经满足不了这个需求了,我们需要扩展默认的 EloquentUserProvider 来实现这个功能。

注:实现此功能有个前提,那就是用户名、手机号和邮箱字段一样,在数据库中是唯一的。

自定义 UserProvider

为此,我们需要创建一个继承自底层 EloquentUserProvider 的子类,并将其存放到 app/Extensions 目录下,重写父类用户记录获取方法 retrieveByCredentials 如下:

<?php
namespace App\Extentions;

use Illuminate\Support\Str;
use Illuminate\Auth\EloquentUserProvider as BaseUserProvider;

class EloquentUserProvider extends BaseUserProvider
{
    /**
     * Retrieve a user by the given credentials.
     *
     * @param  array  $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByCredentials(array $credentials)
    {
        if (empty($credentials) ||
            (count($credentials) === 1 &&
                array_key_exists('password', $credentials))) {
            return;
        }

        // First we will add each credential element to the query as a where clause.
        // Then we can execute the query and, if we found a user, return it in a
        // Eloquent User "model" that will be utilized by the Guard instances.
        $query = $this->createModel()->newQuery();

        // 用于标识是否是第一个登录字段,如果包含多个登录字段,使用 OR 查询
        $flag = false;
        foreach ($credentials as $key => $value) {
            if (Str::contains($key, 'password')) {
                continue;
            }

            if ($flag) {
                $query->orWhere($key, $value);
            } else {
                $query->where($key, $value);
                $flag = true;
            }
        }

        return $query->first();
    }
}

核心是获取用户记录那段逻辑,我们支持用户登录凭证数组 $credentials 除密码字段外传入一个或多个登录字段,多个登录字段使用 OR 查询。

然后我们需要到 app/Providers/AuthServiceProvider.phpboot 方法中使用自定义的 UserProvider 覆盖系统自带的 EloquentUserProvider

// 通过自定义的 EloquentUserProvider 覆盖系统默认的
Auth::provider('eloquent', function ($app, $config) {
    return new EloquentUserProvider($app->make('hash'), $config['model']);
});

这样,用户登录认证时使用的就是我们自定义的这个 EloquentUserProvider 了。

修改控制器传入字段

接下来,我们到控制器中修改传入字段逻辑以便在业务层支持传入不同字段。打开 app/Http/Controllers/Auth/LoginController.php 控制器文件,为控制器设置一个新的属性用于包含系统支持的登录字段:

// 支持的登录字段
protected $supportFields = ['name', 'email'];

然后重写 AuthenticatesUsers 中的 credentials 方法用于传入系统支持的所有登录字段,并将其值都设置为用户在登录表单输入框中输入的值:

// 将支持的登录字段都传递到 UserProvider 进行查询
public function credentials(Request $request)
{
    $credentials = $request->only($this->username(), 'password');
    foreach ($this->supportFields as $field) {
        if (empty($credentials[$field])) {
            $credentials[$field] = $credentials[$this->username()];
        }
    }
    return $credentials;
}

修改登录表单字段

最后去掉登录视图 login.blade.php 对邮箱字段的验证,以支持不同的字段输入:

<div class="form-group row">
   <label for="email" class="col-sm-4 col-form-label text-md-right">{{ __('邮箱') . '/' . __('用户名') }}</label>

   <div class="col-md-6">
       <input id="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required autofocus>

       @if ($errors->has('email'))
           <span class="invalid-feedback" role="alert">
               <strong>{{ $errors->first('email') }}</strong>
           </span>
       @endif
   </div>
</div>

测试多字段登录

这样,多字段登录功能就完成了,我们在浏览器中测试下,切换邮箱/用户名,都可以实现正常登录了:

此外,如果你还有其他支持登录的字段,比如用户昵称、手机号,都可以实现,只需在控制器的 $supportFields 属性中添加对应的字段就可以了,非常方便。

学院君 has written 1198 articles

Laravel学院院长,终身学习者

积分:157887 等级:P12 职业:手艺人 城市:杭州

11 条回复

  1. 樱花树下 樱花树下 says:

    5.8好像不支持 ,credentials 打印的数据是 email,name,password 都有的,的但是提示却是These credentials do not match our records.

  2. laravel版本 5.4.36,这段---------到 app/Providers/AuthServiceProvider.php 的 boot 方法中使用自定义的 UserProvider 覆盖系统自带的 EloquentUserProvider,修改后项目就报错了,能贴一下app/Providers/AuthServiceProvider.php这个文件的整体代码吗?

  3. lzygjq lzygjq says:

    @学院君 这一部分的修改,根据文档调了之后不起作用。

    然后我们需要到 app/Providers/AuthServiceProvider.php 的 boot 方法中使用自定义的 UserProvider 覆盖系统自带的 EloquentUserProvider:

    // 通过自定义的 EloquentUserProvider 覆盖系统默认的 Auth::provider('eloquent', function ($app, $config) { return new EloquentUserProvider($app->make('hash'), $config['model']); });

登录后才能进行评论,立即登录?