# 用户认证
> {tip}**想要快点开始?**只需在新的 Laravel 应用上运行`php artisan make:auth`和`php artisan migrate`命令。 然后,可以用浏览器访问`http://your-app.test/register`或者你在程序中定义的其他 URL 。 这两个命令就可以构建好整个认证系统!
Laravel 中实现用户认证非常简单。事实上,几乎所有的东西都已经为你配置好了。 其配置文件位于`config/auth.php`, 其中包含了用于调整认证服务行为的注释清晰的选项配置。
其核心是由 Laravel 的认证组件的「看守器」和「提供器」组成。 看守器定义了该如何认证每个请求中的用户。例如, Laravel 自带的`session`看守器会使用 session 存储 和 cookies来维护状态。
提供器中定义了该如何从持久化的存储数据中检索用户。 Laravel 自带支持使用 Eloquent 和 数据库查询构造器来检索用户。当然,你可以根据需要自定义其他提供器。
不过不用担心,对于大多数应用而言,可能永远都不需要修改默认身份认证配置。
### 数据库注意事项
默认情况下, Laravel 在`app`目录中包含了一个[Eloquent 模型](https://laravel-china.org/docs/laravel/5.7/eloquent)`App\User`。这个模型和默认的 Eloquent 认证驱动一起使用。如果你的应用不使用 Eloquent , 也可以使用 Laravel 查询构造器的`database`认证驱动。
为`App\User`模型创建数据库表结构时,确保密码字段至少长度为 60 个字符以及默认字符串长度为 255 个字符。
此外,你要验证的用户(或等效的)表要包含一个空的、长度为 100 的字符串`remember_token`。这个字段将用于存储当用户登录应用并勾选「记住我」时的令牌。
## 快速认证
Laravel 自带几个预构建的认证控制器,它们被放置在`App\Http\Controllers\Auth`命名空间内。`RegisterController`处理新用户注册,`LoginController`处理用户认证,`ForgotPasswordController`处理用于重置密码的邮件链接,而`ResetPasswordController`包含重置密码的逻辑。这些控制器都使用 trait 来引入所必要的方法。对于大多数应用而言,你根本不需要修改这些控制器。
### 路由
Laravel 提供了一个简单的命令来快速生成身份验证所需的路由和视图:
~~~php
php artisan make:auth
~~~
该命令最好在新的应用下使用,它会生成布局、注册和登录视图以及所有的认证接口的路由。同时它还会生成`HomeController`来处理应用的登录请求。
### 视图
`php artisan make:auth`命令会在`resources/views/auth`目录创建所有认证需要的视图。
同时,`make:auth`命令还创建了`resources/views/layouts`目录,该目录包含了应用的基本布局视图。所有这些视图都是用 Bootstrap CSS 框架,你也可以根据需要对其自定义。
### 认证
现在,已经为认证的控制器设置好了路由和视图,你可以为应用注册和认证新用户了!你现在可以在浏览器中访问你的应用了,因为控制器已经默认包含了验证用户是否存在和保存用户到数据库中的认证逻辑(通过 traits实现的)。
#### 自定义路径
当一个用户成功通过身份认证后,将会重定向到`/home`的 URI。你可以通过在`LoginController`,`RegisterController`,和`ResetPasswordController`中设置`redirectTo`属性来自定义重定向的路径:
~~~php
protected $redirectTo = '/';
~~~
接下来,修改`RedirectIfAuthenticated`中间件的`handle`方法以便在重定向用户时重定向到新的 URI。
如果重定向路径需要自定义生成逻辑,你可以定义`redirectTo`方法替代`redirectTo`属性:
~~~php
protected function redirectTo()
{
return '/path';
}
~~~
> {提示}`redirectTo`方法优先于`redirectTo`属性。
#### 自定义用户名
Laravel 默认使用`email`字段来认证。如果你想使用其他的字段,可以在`LoginController`控制器里面定义一个`username`方法:
~~~php
public function username()
{
return 'username';
}
~~~
#### 自定义看守器
你还可以自定义用户认证和注册的 「看守器」。要实现这一功能,需要在`LoginController`,`RegisterController`,和`ResetPasswordController`中定义`guard`方法。该方法需要返回一个看守器实例:
~~~php
use Illuminate\Support\Facades\Auth;
protected function guard()
{
return Auth::guard('guard-name');
}
~~~
#### 自定义验证 / 存储
为了修改新用户在注册时所需要填写的表单字段,或者自定义如何将新用户存储到数据库中,你可以修改`RegisterController`类。该类负责验证和创建新用户。`RegisterController`类的`validator`方法包含了验证新用户的规则,你可以按照需要自定义该方法。`RegisterController`的`create`方法负责使用[Eloquent ORM](https://laravel-china.org/docs/laravel/5.7/eloquent)在数据库中创建新的`App\User`记录。你可以根据数据库的需要自定义该方法。
### 检索认证用户
你可以通过`Auth`facade 来访问已认证的用户:
~~~php
use Illuminate\Support\Facades\Auth;
// 获取当前通过认证的用户...
$user = Auth::user();
// 获取当前通过认证的用户 ID...
$id = Auth::id();
~~~
或者,你可以通过`Illuminate\Http\Request`实例来访问已认证的用户。别忘了,类型提示的类会被自动注入到你的控制器方法中:
~~~php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ProfileController extends Controller
{
/**
* 更新用户简介
*
* @param Request $request
* @return Response
*/
public function update(Request $request)
{
// $request->user() 返回已认证的用户的实例...
}
}
~~~
#### 确认当前用户是否被认证
你可以使用`Auth`facade 的`check`方法来检查用户是否已认证。如果已认证,将会返回`true`:
~~~php
use Illuminate\Support\Facades\Auth;
if (Auth::check()) {
// 用户已认证...
}
~~~
> {提示} 虽然可以使用`check`方法确认用户是否被认证,但是在允许用户访问的某些路由 / 控制器之前,通常还是会使用中间件来验证用户是否进行过身份验证。想要了解更多信息,请查看有关[路由保护](https://laravel-china.org/docs/laravel/5.7/authentication#protecting-routes)的文档。
### 保护路由
[路由中间件](https://laravel-china.org/docs/laravel/5.7/middleware)可以用于只允许通过认证的用户访问给定的路由。Laravel 自带了一个`auth`中间件, 它定义于`Illuminate\Auth\Middleware\Authenticate`。由于这个中间件已经在HTTP 内核中注册,你所有需要做的只是把这个中间件附加到路由定义中:
~~~php
Route::get('profile', function () {
// 只有认证过的用户可以进入...
})->middleware('auth');
~~~
当然,如果你使用[控制器](https://laravel-china.org/docs/laravel/5.7/controllers),你可以在控制器的构造函数中调用`middleware`方法来直接将其附加到路由定义中:
~~~php
public function __construct()
{
$this->middleware('auth');
}
~~~
#### 重定向未认证的用户
当`auth`中间件检测到一个未认证用户时,它将返回一个`401`的JSON响应,或者,如果不是 AJAX 请求, 重定向用户到名为`login`的[命名路由](https://laravel-china.org/docs/laravel/5.7/routing#named-routes).
你可以在`app/Exceptions/Handler.php`文件中定义一个`unauthenticated`方法来修改这个行为:
~~~php
use Illuminate\Auth\AuthenticationException;
protected function unauthenticated($request, AuthenticationException $exception)
{
return $request->expectsJson()
? response()->json(['message' => $exception->getMessage()], 401)
: redirect()->guest(route('login'));
}
~~~
#### 指定看守器
当你把`auth`中间件添加到路由中时,同时也能指定使用哪个看守器进行用户认证。指定的看守器应该对应你的`auth.php`中`guards`数组的一个键 :
~~~php
public function __construct()
{
$this->middleware('auth:api');
}
~~~
### 登录限制
如果你使用 Laravel 内置的`LoginController`类,`Illuminate\Foundation\Auth\ThrottlesLogins`trait 已经包含在控制器中。默认的,如果用户经过多次的登录尝试,依然无法提供正确的登录凭据,那么这个用户在一分钟内将不能再次尝试登录。这基于用户的用户名/邮箱地址和他的 IP 地址形成的特定限制。
## 手动认证用户
当然,你不一定需要使用 Laravel 内置的认证控制器。如果你选择删除这些控制器,你将需要使用 Laravel 的认证类直接进行用户认证的管理。别担心,这很简单 !
我们将使用`Auth`[facade](https://laravel-china.org/docs/laravel/5.7/facades)来访问 Laravel 的认证服务,所以我们需要确保在类的顶部引入`Auth`facade。接下来,让我们看看`attempt`方法:
~~~php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
/**
* 处理认证尝试
*
* @param \Illuminate\Http\Request $request
*
* @return Response
*/
public function authenticate(Request $request)
{
$credentials = $request->only('email', 'password');
if (Auth::attempt($credentials)) {
// Authentication passed...
return redirect()->intended('dashboard');
}
}
}
~~~
`attempt`方法接受一个键值对数组作为其第一个参数。数组中的值将被用于在数据表中查找用户。所以,在上例中,用户将通过`email`字段的值进行检索。如果找到了这个用户,数据库中保存的哈希密码将被用来与传递给方法的数组中`password`的哈希值进行比较。你不应该将指定为密码的`password`的值进行哈希操作,因为框架将在比较前,自动对其进行hash操作。如果这两个哈希密码匹配,就会为用户开启一个已认证的会话。
如果认证成功那么`attempt`方法将返回`true`。反之,会返回`false`。
重定向器上的`intended`方法将重定向用户到他们曾经希望访问的 URL,这个 URL 之前被用户认证中间件拦截了。可以给这个方法传递一个回退 URI,用于预期的地址不可用的情况。
#### 指定额外条件
如果您愿意,除了用户的电子邮件和密码之外,您还可以为身份认证查询添加额外的条件。例如,我们可以验证用户是否标记为「激活」状态:
~~~php
if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) {
// 用户是激活状态,没有被暂停,而且存在
}
~~~
> {note} 在这些例子中`email`不是必需的选项,它仅用作示例。你应该使用数据库中任何与「用户名」对应的字段的名称。
#### 访问指定的看守器实例
您可以使用`Auth`facade 上的`guard`方法指定要使用的看守器实例。这允许你使用完全独立的可认证模型或用户表来管理应用程序的单独部分的身份验证。
传递给`guard`方法的的看守器名称,应该与你的`auth.php`配置文件中配置的某个值对应:
~~~php
if (Auth::guard('admin')->attempt($credentials)) {
//
}
~~~
#### 注销登录
要将用户从应用中注销,你可以使用`Auth`facade 上的`logout`方法。这将清除用户会话中的认证信息:
~~~php
Auth::logout();
~~~
### 记住用户
如果你想在应用中提供「记住我」的功能 ,你可以给`attempt`方法的第二个参数传入一个布尔值,这将永久保持用户的认证状态,或者直到他们手动注销登录。当然,你的`users`表必须包含名为`remember_token`的字符串字段,它将被用于保存「记住我」的令牌。
~~~php
if (Auth::attempt(['email' => $email, 'password' => $password], $remember)) {
// 这个用户被记住了...
}
~~~
> {tip} 如果你使用的是 Laravel 附带的内置`LoginController`,「记住」用户的恰当逻辑已经由控制器中的 trait 实现。
如果你启用了「记住」用户,你可以使用`viaRemember`方法确定用户是否正使用「记住我」的cookie令牌进行的认证 :
~~~php
if (Auth::viaRemember()) {
//
}
~~~
### 其它认证方式
#### 验证用户实例
如果你需要把现存的用户实例登入应用, 你可以调用`login`方法并附带用户实例作为参数。给定的这个对象必须实现`Illuminate\Contracts\Auth\Authenticatable`[契约](https://laravel-china.org/docs/laravel/5.7/contracts)。 当然,Laravel自带的`App\User`模型已经实现了这个接口:
~~~php
Auth::login($user);
// 登录并且「记住」给定的用户...
Auth::login($user, true);
~~~
当然,你可以指定想要使用的看守器实例:
~~~php
Auth::guard('admin')->login($user);
~~~
#### 通过 ID 验证用户
要使用用户的 ID 登录应用,你可以使用`loginUsingId`方法。这个方法接受需要认证的用户主键:
~~~php
Auth::loginUsingId(1);
// 登录并且「记住」给定的用户...
Auth::loginUsingId(1, true);
~~~
#### 仅验证用户一次
你可以在单次请求中使用`once`方法将用户登录到应用中。这将不会使用任何 Session 或者 Cookie,这意味着在构建无状态API时,此方法可能会有所帮助:
~~~php
if (Auth::once($credentials)) {
//
}
~~~
## HTTP 基础认证
[HTTP 基础认证](https://en.wikipedia.org/wiki/Basic_access_authentication)提供了一种快速方法来验证你应用程序中的用户,而无需设置专用的「登录」页面。 开始之前, 先把`auth.basic`[中间件](https://laravel-china.org/docs/laravel/5.7/middleware)附加到你的路由中。`auth.basic`中间件已包含在 Laravel 框架中,所以你不需要定义它:
~~~php
Route::get('profile', function () {
// 只有认证过的用户可以进入...
})->middleware('auth.basic');
~~~
将中间件附加到路由后,在浏览器中访问此路由时将自动提示您输入凭据。默认的,`auth.basic`中间件把用户记录上的`email`字段 作为「用户名」。
#### FastCGI 的注意事项
如果你正使用 PHP FastCGI 模式,HTTP 基础认证可能无法正常工作。需要把下面几行添加到你的`.htaccess`文件中:
~~~php
RewriteCond %{HTTP:Authorization} ^(.+)$
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
~~~
### 无状态 HTTP 基础认证
你也可以使用 HTTP 基础身份验证,而无需在会话中设置用户标识符cookie,这对 API 的身份验证特别有用。为此 ,请[定义一个中间件](https://laravel-china.org/docs/laravel/5.7/middleware)它将调用`onceBasic`方法。如果`onceBasic`方法没有返回任何响应,那么请求就可以进一步传递到应用程序中:
~~~php
<?php
namespace App\Http\Middleware;
use Illuminate\Support\Facades\Auth;
class AuthenticateOnceWithBasicAuth
{
/**
* 处理传入的请求
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, $next)
{
return Auth::onceBasic() ?: $next($request);
}
}
~~~
接着,[注册路由中间件](https://laravel-china.org/docs/laravel/5.7/middleware#registering-middleware)并将它附加到路由:
~~~php
Route::get('api/user', function () {
// 只有认证过的用户可以进入...
})->middleware('auth.basic.once');
~~~
## 退出
要手动把用户从应用中退出登录,你可以使用`Auth`facade 上的`logout`方法。这将清除用户会话中的身份认证信息:
~~~php
use Illuminate\Support\Facades\Auth;
Auth::logout();
~~~
### 让其它设备上的 Session 失效
Laravel 还提供了一种机制,用于将其它设备上的用户 Session 失效和「注销」,而不会使其当前设备上的 Session 失效。首先,你需要保证`Illuminate\Session\Middleware\AuthenticateSession`中间件在你的`app/Http/Kernel.php`类中的`web`中间件组中,并且没有被注释掉:
~~~php
'web' => [
// ...
\Illuminate\Session\Middleware\AuthenticateSession::class,
// ...
],
~~~
然后, 你就可以使用`Auth`facade 上的`logoutOtherDevices`方法。此方法要求用户提供其当前密码,你的应用程序应通过输入表单接受该密码:
~~~php
use Illuminate\Support\Facades\Auth;
Auth::logoutOtherDevices($password);
~~~
> {note} 当调用`logoutOtherDevices`方法后,用户的其它 Session 将完全失效,这意味着他们将「退出」他们之前通过身份认证的所有看守器。
## 添加自定义的看守器
你可以使用`Auth`facade 的`extend`方法来定义自己的身份验证看守器。你应该在[服务提供器](https://laravel-china.org/docs/laravel/5.7/providers)中调用`extend`方法。由于 Laravel 已经附带了`AuthServiceProvider`,我们可以将代码放在该提供器中:
~~~php
<?php
namespace App\Providers;
use App\Services\Auth\JwtGuard;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* 注册任意应用认证/授权服务。
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Auth::extend('jwt', function ($app, $name, array $config) {
// 返回一个 Illuminate\Contracts\Auth\Guard 实例...
return new JwtGuard(Auth::createUserProvider($config['provider']));
});
}
}
~~~
正如你在上面的示例中所看到的,传递给`extend`方法的回调应该返回一个实现`Illuminate\Contracts\Auth\Guard`接口的实例。这个接口包含了一些你需要在自定义的看守器中实现的方法。当你的自定义看守器定义完成之后,你可以在`auth.php`配置文件的`guards`配置中使用这个看守器:
~~~php
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
~~~
### 请求闭包看守器
实现基于HTTP请求的自定义身份验证系统的最简单方法,是使用`Auth::viaRequest`方法。此方法允许您使用单个闭包来快速定义身份验证过程。
首先,在`AuthServiceProvider`的`boot`方法中调用`Auth::viaRequest`方法。`viaRequest`方法接受一个看守器名称作为其第一个参数。此名称可以是描述你自定义看守器的任何字符串。传递给该方法的第二个参数应该是一个闭包函数,它接收传入的HTTP请求并返回一个用户实例,或者,如果验证失败,则为`null`:
~~~php
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
/**
* 注册任意应用认证/授权服务。
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Auth::viaRequest('custom-token', function ($request) {
return User::where('token', $request->token)->first();
});
}
~~~
当你完成了自定义看守器后,就可以在`auth.php`配置文件的`guards`配置中使用这个看守器:
~~~php
'guards' => [
'api' => [
'driver' => 'custom-token',
],
],
~~~
## 添加自定义用户提供器
如果您没有使用传统的关系型数据库来存储用户,则需要使用自己的身份验证用户提供器来扩展 Laravel。我们将在`Auth`facade上使用`provider`方法来自定义一个用户提供器:
~~~php
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Auth;
use App\Extensions\RiakUserProvider;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* 注册任何应用认证/授权服务。
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Auth::provider('riak', function ($app, array $config) {
// Return an instance of Illuminate\Contracts\Auth\UserProvider...
return new RiakUserProvider($app->make('riak.connection'));
});
}
}
~~~
使用`provider`方法注册提供器后,可以在`auth.php`配置文件中切换到新的用户提供器。首先,定义一个使用新驱动的`provider`:
~~~php
'providers' => [
'users' => [
'driver' => 'riak',
],
],
~~~
最后,你可以在`guards`配置中使用此提供器:
~~~php
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
~~~
### 用户提供器契约
`Illuminate\Contracts\Auth\UserProvider`的实现只负责从永久存储系统(如 MySQL、Riak 等)中获取`Illuminate\Contracts\Auth\Authenticatable`的实现实例。无论用户数据的存储方式或用于表示用户数据的类是什么,这两个接口都允许 Laravel 身份验证机制继续运行。
让我们来看看`Illuminate\Contracts\Auth\UserProvider`契约:
~~~php
<?php
namespace Illuminate\Contracts\Auth;
interface UserProvider {
public function retrieveById($identifier);
public function retrieveByToken($identifier, $token);
public function updateRememberToken(Authenticatable $user, $token);
public function retrieveByCredentials(array $credentials);
public function validateCredentials(Authenticatable $user, array $credentials);
}
~~~
`retrieveById`函数通常接收代表用户的键,例如来自 MySQL 数据库的自动递增 ID。同时,应该检索并返回与 ID 匹配的`Authenticatable`实现。
`retrieveByToken`函数通过用户唯一的`$identifier`和「记住我」`$token`来检索用户,后者存储在字段`remember_token`中。与前面的方法一样,它应该返回`Authenticatable`的实现实例。
`updateRememberToken`方法用新的`$token`更新`$user`的`remember_token`字段。在成功使用「记住我」登录后或用户注销时分配新令牌。
在尝试登录应用程序时,`retrieveByCredentials`方法接收`Auth::attempt`方法传递的凭证数组。然后该方法应该「查询」底层持久存储中匹配这些凭据的用户。 通常,此方法将使用`$credentials['username']`执行一个「where」查询。这个方法应该返回一个`Authenticatable`的实现实例。**此方法不应尝试进行任何密码验证或身份验证。**
`validateCredentials`方法应该将给定的`$user`与`$credentials`进行比较,来验证用户。例如,这个方法可能应该使用`Hash::check`来比较`$user->getAuthPassword()`和`$credentials['password']`的值。这个方法应该返回`true`或者`false`来指示密码是否有效。
### 认证契约
现在我们已经探讨了`UserProvider`上的每个方法,让我们来看看`Authenticatable`契约。记住,提供器中`retrieveById`,`retrieveByToken`和`retrieveByCredentials`方法应该返回此接口的实例:
~~~php
<?php
namespace Illuminate\Contracts\Auth;
interface Authenticatable {
public function getAuthIdentifierName();
public function getAuthIdentifier();
public function getAuthPassword();
public function getRememberToken();
public function setRememberToken($value);
public function getRememberTokenName();
}
~~~
这个接口很简单。`getAuthIdentifierName`方法应该返回用户「主键」字段的名称,而`getAuthIdentifier`应该返回用户主键的值 。重申一次,在 MySQL 后台,这个主键是指自增的主键。`getAuthPassword`应该返回用户的哈希密码。 此接口允许身份验证系统与任何的 User 类一起使用,无论你使用的是什么ORM或存储抽象层。默认情况下,Laravel 的`app`目录中包含一个`User`类,它实现了这个接口,所以你可以参考这个类来获得一个实现例子。
## 事件
Laravel 在身份认证过程中引发了各种[事件](https://laravel-china.org/docs/laravel/5.7/events)。你可以在`EventServiceProvider`中为这些事件添加监听器:
~~~php
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
'Illuminate\Auth\Events\Registered' => [
'App\Listeners\LogRegisteredUser',
],
'Illuminate\Auth\Events\Attempting' => [
'App\Listeners\LogAuthenticationAttempt',
],
'Illuminate\Auth\Events\Authenticated' => [
'App\Listeners\LogAuthenticated',
],
'Illuminate\Auth\Events\Login' => [
'App\Listeners\LogSuccessfulLogin',
],
'Illuminate\Auth\Events\Failed' => [
'App\Listeners\LogFailedLogin',
],
'Illuminate\Auth\Events\Logout' => [
'App\Listeners\LogSuccessfulLogout',
],
'Illuminate\Auth\Events\Lockout' => [
'App\Listeners\LogLockout',
],
'Illuminate\Auth\Events\PasswordReset' => [
'App\Listeners\LogPasswordReset',
],
];
~~~