## **简介**
Laravel 框架开箱为我们提供了一些用户认证需要的脚手架代码,包括数据库迁移文件、用户模型、用户认证中间件和控制控制器等。我们先来简单介绍下它们。
- 数据库迁移:新安装的 Laravel 应用都包含下面两个迁移文件,分别用于创建用户表和密码重置表;
![](https://img.kancloud.cn/f3/e2/f3e240e48c61fcc4d4f64d937d215b7d_1320x206.jpg)
- User模型类:Laravel 框架还在`app`目录下为我们提供了与用户表相对应的模型类`User`,在基于 Eloquent 模型驱动的认证提供者中,我们通过该模型类实现登录认证,你可以在配置文件`config/auth.php`中查看相应的配置:
~~~
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
~~~
如果你不是通过`User`模型类进行认证,可以在这里修改对应的`model`配置项。如果你不想通过 Eloquent 模型驱动,而是基于原生的数据库查询,可以注释掉`eloquent`对应的`users`配置,启用下面这个`database`对应的`users`配置,这样的话就是直接去查询`users`表,而不是通过模型类进行认证了。
回到我们的默认配置,我们看下`User`模型类代码:
~~~
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
}
~~~
如果某个模型类需要用于认证,必须继承自`Illuminate\Foundation\Auth\User`基类,否则会报错。然后我们在这个模型类中使用了`Notifiable`Trait,里面提供了用户发送通知的相关方法。我们在白名单`$fillable`中配置了三个字段,这三个字段会在登录和注册时用到。最后,我们还配置了`$hidden`属性,在返回查询结果的时候将敏感信息过滤掉,避免安全隐患。
- 认证中间件:Laravel 框架内置了几个认证中间件,用于在需要认证的路由中拒绝未认证用户发起的请求,或者将已登录用户重定向到认证页面,打开`app/Http/Kernel.php`,可以在`$routeMiddleware`看到对应的路由中间件:
![](https://img.kancloud.cn/2f/07/2f0725372f13c216bbf075038ef924e9_1636x410.jpg)
我们平时主要用到的是`auth`中间件和`guest`中间件,`auth.basic`用于基于 HTTP 的简单认证,很少用到,`throttle`中间件会在用户多次登录失败时使用,单位登录失败超过指定次数不允许继续发起登录请求,提高系统安全性。
`auth`中间件是`\App\Http\Middleware\Authenticate::class`的别名,`Authenticate`主要用于将未登录用户重定向到登录页面。
`guest`中间件是`\App\Http\Middleware\RedirectIfAuthenticated::class`的别名,`RedirectIfAuthenticated`主要用于将已登录用户重定向到认证后页面,未登录则继续原来的请求。
- 认证控制器:Laravel 还为我们开箱提供了注册、登录、重置密码、邮箱验证、忘记密码对应的控制器:
![](https://img.kancloud.cn/4e/62/4e625c18d2173d5198ef77e2d4ab8828_1404x316.jpg)
其中`ForgotPasswordController`用于忘记密码后通过填写注册邮箱发送重置密码链接,对应逻辑位于`Illuminate\Foundation\Auth\SendsPasswordResetEmails`Trait 中;`LoginController`用于用户登录和退出,对应逻辑位于`Illuminate\Foundation\Auth\AuthenticatesUsers`Trait 中;`RegisterController`用于新用户注册,对应逻辑位于`Illuminate\Foundation\Auth\RegistersUsers`Trait 中;`ResetPasswordController`用于重置密码,对应逻辑位于`Illuminate\Foundation\Auth\ResetsPasswords`Trait 中。
上述控制器的构造函数中都应用了`guest`中间件(退出功能除外),表示这些控制器提供的方法都是给未登录用户使用的。
`VerificationController`是 Laravel 5.7 新提供的,用于新注册用户邮箱验证,对应逻辑位于`Illuminate\Foundation\Auth\VerifiesEmails`Trait 中。
## **通过Artisan命令快速实现注册登录**
我们可以自己编写用户认证路由、视图然后对接到这些控制器,提供用户认证功能,不过,Laravel 开箱为我们提供了 Artisan 命令`make:auth`,运行该命令可以帮助我们在最短时间内完成认证路由注册和认证视图(兼容 Bootstrap 4)发布,何乐而不为?
我们在系统根目录下运行如下命令激活「休眠」的用户认证功能:
~~~
php artisan make:auth # 注册认证路由、发布认证视图
php artisan migrate # 创建认证相关数据表,如果之前已经运行过,可以跳过
npm run dev # 编译前端资源,如果之前已经运行过,可以跳过
~~~
`make:auth`命令会在`routes/web.php`中注册以下路由:
~~~
Auth::routes();
Route::get('/home', 'HomeController@index')->name('home');
~~~
`home`路由是用户认证成功后默认跳转路由,此外,该命令还会在`resources/views`下发布以下登录认证相关的视图文件:
* `resources/views/home.blade.php`
* `resources/views/layouts/app.blade.php`
* `resources/views/auth/login.blade.php`
* `resources/views/auth/register.blade.php`
* `resources/views/auth/verify.blade.php`
* `resources/views/auth/passwords/email.blade.php`
* `resources/views/auth/passwords/reset.blade.php`
这样,不需要编写任何代码,只需要简单运行几个命令,就可以在 Laravel 应用中实现登录认证功能了。
![](https://img.kancloud.cn/b7/e0/b7e0345333981b5f3c1401477facb326_2274x734.jpg)
## **获取登录用户信息**
用户登录成功后,就可以获取登录用户信息了,我们可以通过多种方式获取用户信息。
### **通过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`控制器中通过设置如下属性来实现,比如设置半小时内只能尝试3次:
~~~
// 单位时间内最大登录尝试次数
protected $maxAttempts = 3;
// 单位时间值
protected $decayMinutes = 30;
~~~
## **支持用户名/邮箱登录**
Laravel 支持在用户名和邮箱之间切换登录,默认是通过注册邮箱登录的,如果你想要调整为通过用户名登录,很简单,在`LoginController`控制器中定义一个`username()`方法重写`AuthenticatesUsers`Trait 中的同名方法即可,Laravel 底层通过该方法定义登录字段名,而不是写死的,我们在该方法中返回登录字段名:
~~~
public function username()
{
return 'name';
}
//记得把前端视图对应的登录表单字段调整为`name`,这样就可以基于用户名进行登录了
~~~
实际开发中,有时候我们的登录字段可能需要同时支持用户名/注册邮箱/手机号登录【前提是这三个字段都是唯一的】,即用户既可以在登录框中输入邮箱,也可以输入用户名,这个时候,Laravel 框架底层自带的 UserProvider 已经满足不了这个需求了,我们需要扩展默认的`EloquentUserProvider`来实现这个功能。
为此,我们需要创建一个继承自底层`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.php`的`boot`方法中使用自定义的 UserProvider 覆盖系统自带的`EloquentUserProvider`:
~~~
// 通过自定义的 EloquentUserProvider 覆盖系统默认的
Auth::provider('eloquent', function ($app, $config) {
return new EloquentUserProvider($app->make('hash'), $config['model']);
});
~~~
接下来,我们到控制器中修改传入字段逻辑以便在业务层支持传入不同字段。打开`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`属性中添加对应的字段就可以了。