用户认证


用户认证

引言

{tip} 想要快速开始吗? 只要在一个新的 Laravel 应用程序中运行 php artisan make:authphp artisan migrate 就可以了。 然后,把你的浏览器导航到 http://your-app.test/register 或者其他任意一个分配给你的程序的 URL。这两个命令将负责构建整个认证系统!

Laravel 使得实现身份验证非常简单。 事实上,几乎所有的配置都是现成的。 身份验证配置文件位于 config/auth.php, 其中包含几个有良好文档记录的选项,用于调整身份验证服务的行为。

在其核心,Laravel 的认证设施由 “警卫” 和 “提供者” 组成。守卫决定如何对每个请求的用户进行身份验证。比如,Laravel 带有一个 session 保护,它使用会话存储和 Cookies 来维护状态。

提供者决定如何从持久储存中检索用户。 Laravel 支持使用 Eloquent 和数据库查询生成器检索用户。但是,你可以根据应用程序的需要来自由定义其他提供者。

如果这些听起来让你很困惑,别担心!许多应用程序永远不需要修改默认的身份验证配置。

数据库注意事项

默认情况下, Laravel 包含一个 AppUser Eloquent model 在你的 app 目录下。 这个模型可与默认的 Eloquent 身份验证驱动程序一起使用。如果你的应用程序没有使用 Eloquent,你可以用 database 身份验证驱动程序,它用的是 Laravel 查询生成器。

当为 AppUser 模型生成数据库架构时,确保密码的长度至少为 60 个字符。保持默认的字符串长度为 255 个字符是一个不错的选择。

另外,你应该验证 “users”(或等效)表是否包含一个可空的,含有 100 个字符的 remember_token 字符串。此列将用于存储用户登录应用程序时选择 “记住我” 选项的令牌。

用户认证快速指南

Laravel 附带了几个预构建的身份验证控制器,它们位于 AppHttpControllersAuth 命名空间中。RegisterController 处理新的用户注册,LoginController 处理身份验证,ForgotPasswordController 处理用于重置密码的电子邮件链接,ResetPasswordController 包含重置密码的逻辑。这些控制器中的每一个都使用一个特性来包含它们的必要方法。对于许多应用程序,您根本不需要修改这些控制器。

路由

Laravel 提供了一种快速的方法,可以使用一个简单的命令来搭建认证所需的所有路由和视图:

php artisan make:auth

此命令应该用于新应用程序,并将安装布局视图、注册和登录视图以及所有身份验证端点的路由。还将生成一个 HomeController 来处理应用程序仪表板的登录后请求。

{tip} 如果应用程序不需要注册,可以通过删除新创建的 RegisterController 并修改路由声明来禁用它: Auth::routes(['register' => false]);

视图

如前一节所述, php artisan make:auth 命令将创建认证所需的所有视图,并将它们放在 resources/views/auth 目录中。

make:auth 命令还将创建一个包含应用程序基本布局的 resources/views/layouts 目录。所有这些视图都使用了 Bootstrap CSS 框架,但是你可以自由地定制它们。

认证

现在已经给认证的控制器设置好了路由和视图,你可以在应用中注册和认证新用户了!因为控制器已经默认包含了验证用户是否存在和保存用户到数据库中的认证逻辑(通过 traits 实现的),现在你已经可以在浏览器中访问应用了。

自定义路径

当用户认证成功,他们会被重定向到 /home 这个 URI 下。你可以在 LoginControllerRegisterControllerResetPasswordController,还有 VerificationController 控制器中定义 redirectTo 属性来自定义验证后的重定向位置:

protected $redirectTo = '/';

接下,你应该修改 RedirectIfAuthenticated 中间件中的 handle 方法,以便在重定向用户时重定向到新的 URI。

如果重定向路径需要自定义生成逻辑,你可以定义 redirectTo 方法替代 redirectTo 属性:

protected function redirectTo(){ 
   return '/path';
 }

{提示}  redirectTo 方法优先于 redirectTo 属性。

自定义用户名

Laravel 默认使用 email 字段来认证。如果你想使用其他的字段,可以在 LoginController 控制器里面定义一个 username 方法:

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

自定义看守器

你还可以自定义用户认证和注册的 「看守器」。要实现这一功能,需要在 LoginController,RegisterControllerResetPasswordController 中定义 guard 方法。该方法会返回一个看守器实例:

use Illuminate\Support\Facades\Auth;protected function guard(){ 
   return Auth::guard('guard-name');
 }

自定义验证 / 存储

为了修改新用户在注册时所需要填写的表单字段,或者自定义如何将新用户存储到数据库中,你可以修改  RegisterController 类。该类负责验证和创建新用户。

RegisterController 类的 validator 方法包含了验证新用户的规则,你可以随心所欲地自定义该方法。

RegisterControllercreate 方法负责使用  Eloquent ORM 在数据库中创建新的 AppUser 记录。你可以根据数据库的需要自定义该方法。

检索认证用户

你可以通过 Auth facade 来访问已认证的用户:

use Illuminate\Support\Facades\Auth;
// 获取当前通过认证的用户...
$user = Auth::user();
// 获取当前通过认证的用户 ID...
$id = Auth::id();

或者,你可以通过 IlluminateHttpRequest 实例来访问已认证的用户。别忘了,类型提示的类会被自动注入到你的控制器方法中:

<?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

use Illuminate\Support\Facades\Auth;
if (Auth::check()) {   
 // 用户已经登录了...
}

{提示} 虽然可以使用 check 方法确认用户是否被认证,但是在允许用户访问的某些路由 / 控制器之前,通常还是会使用中间件来验证用户是否进行过身份验证。想要了解更多信息,请查看有关 保护路由 的文档。

保护路由

路由中间件 可以用于只允许通过认证的用户访问给定的路由。Laravel 自带了一个 auth 中间件,它定义在 IlluminateAuthMiddlewareAuthenticate 中。由于这个中间件已经在 HTTP 内核中注册,你只需把这个中间件附加到路由定义中:

Route::get('profile', function () {  
  // 只有认证过的用户可以进入...
 })->middleware('auth');

当然,如果你使用 控制器,你可以在控制器的构造函数中调用  middleware 方法来直接将其附加到路由定义中:

public function __construct(){  
  $this->middleware('auth');
}

重定向未认证的用户

auth 中间件检测到一个未认证用户时,它会把用户重定向到名为 login 的 命名路由上。
您可以通过修改 app/Http/Middleware/Authenticate.php 文件中的 redirectTo 函数来修改此行为:

/**
 * Get the path the user should be redirected to.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return string
 */
 protected function redirectTo($request){ 
    return route('login');
 }

指定看守器

当你把 auth 中间件添加到路由中时,同时也能指定使用哪个看守器进行用户认证。指定的看守器应该对应 auth.php 配置文件中 guards 数组中的的一个键:

public function __construct(){ 
   $this->middleware('auth:api');
}

登录限流

如果你使用 Laravel 内置的 LoginController 类,IlluminateFoundationAuthThrottlesLogins trait 已经包含在该控制器中了。默认情况下,如果用户多次尝试却无法提供正确的登录凭据,那么该用户在一分钟内将不能再次尝试登录。这种限流策略基于用户的用户名 / 邮箱地址及其 IP 地址的唯一性。

手动验证用户

不一定非要在 Lavarel 中使用验证控制器。如果选择删除这些控制器,就需要直接使用 Lavarel 验证类。别担心,很容易!

可以借助 Auth facade 访问 Laravel 服务,因此需要在类的开头导入  Auth 。下面来看看 attempt 方法:

<?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)) {        
            // 身份验证通过...           
           return redirect()->intended('dashboard');    
             }   
           }
        }

attempt 方法的每个参数是一个关联数组。数组值用于在数据库中查找用户。在上面的例子中,将通过 email 列的值查找用户。如果找到该用户,将用存储在数据库中的哈希密码与数组中的 password 值做比较。不需要对 password 做哈希运算,框架在与数据库中的哈希密码做比较前自动对此值做哈希运算。如果两个哈希值匹配,将为该用户建立验证通过的 session。

如果验证成功, attempt 方法返回  true ,否则返回 false

重定向中的 intended 方法将经由身份验证中间件将用户重定向到身份验证前截获的 URL 。如果预期目标不存在,可以为此方法指定一个回退 URI 。

指定额外条件

除了用户的电子邮件和密码之外,还可以向身份验证查询添加其他条件。例如, 我们可以验证用户是不是已经被标记为 「激活」:

if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) {  
  // 用户存在,已激活且未被禁用。
 }

{note} 在这些例子中, email 不是必须的选项,它只用来做示范。你应该使用与你的数据库中 「用户名」 对应的列名。

访问指定的看守器实例

可以使用 Auth facade 的 guard 方法指定想要使用的看守器实例。这允许你使用完全独立的可验证模型或用户表来管理应用程序各个部分的验证。

传递给 guard 方法的看守器名称需要与 auth.php 配置中的配置项之一相匹配:

if (Auth::guard('admin')->attempt($credentials)) { 
   //
 }

登出

用户登出需要使用 Auth facade 的 logout 方法。它会清除用户会话(session)中的用户验证信息:

Auth::logout();

记住用户

如果想在应用中提供 「记住我」功能,可以给 attempt 方法传递一个布尔值作为其第二个参数,这会无限期保持用户身份验证,直到用户手动登出。用户表需要包含字符串类型的 remember_token 列用于存储令牌。

if (Auth::attempt(['email' => $email, 'password' => $password], $remember)) { 
   // 用户被记住...
}

{tip} 如果使用了 Laravel 内置的 LoginController,「记住」用户的正确逻辑已经由控制器所用的 traits 实现。

如果启用了「记住用户」,可以使用 viaRemember 方法判断是否使用了「记住我」cookie 对用户做身份验证:

if (Auth::viaRemember()) { 
   //
}

其它身份验证方法

验证用户实例

如果要将已经存在的用户登入应用,可以调用 login 方法,并以用户实例作为其参数 。该对象必须实现 IlluminateContractsAuthAuthenticatable 契约 。Laravel 自带的 AppUser 模型已经实现了这个接口:

Auth::login($user);
// 登录并「记住」给定的用户...
Auth::login($user, true);

使用如下方式指定想要的看守器实例:

Auth::guard('admin')->login($user);

通过 ID 验证用户身份

可以使用 loginUsingId 方法通过 ID 将用户登录到应用。这个方法接受希望验证身份用户的主键:

Auth::loginUsingId(1);
// 登录并「记住」给定用户...
Auth::loginUsingId(1, true);

仅验证一次用户身份

可以使用 once 方法在单次请求中将用户登录到应用中。这样做将不使用 session 或 cookies,这意味着此方法有助于构建一个无状态 API:

if (Auth::once($credentials)) {
    //
  }

HTTP 基础认证

HTTP 基础认证 提供了一种快速方法来验证你应用程序中的用户,而无需设置专用的「登录」页面。 开始之前, 先把 auth.basic 中间件 附加到你的路由中。auth.basic 中间件已包含在 Laravel 框架中,所以你不需要定义它:

Route::get('profile', function () { 
   // 只有认证过的用户可以进入...
 })->middleware('auth.basic');

将中间件附加到路由后,在浏览器中访问此路由时将自动提示您输入凭据。默认的,auth.basic 中间件把用户记录上的 email 字段 作为「用户名」。

FastCGI 的注意事项

如果你正使用 PHP FastCGI 模式,HTTP 基础认证可能无法正常工作。需要把下面几行添加到你的 .htaccess 文件中:

RewriteCond %{HTTP:Authorization} ^(.+)$
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

无状态 HTTP 基础认证

你也可以使用 HTTP 基础身份验证,而无需在会话中设置用户标识符 cookie,这对 API 的身份验证特别有用。为此 ,请定义一个中间件 它将调用 onceBasic 方法。如果 onceBasic 方法没有返回任何响应,那么请求就可以进一步传递到应用程序中:

<?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);  
     }
   }

接着, 注册路由中间件 并将它附加到路由:

Route::get('api/user', function () {  
  // 只有认证过的用户可以进入...
})->middleware('auth.basic.once');

退出

要手动把用户从应用中退出登录,你可以使用 Auth facade 上的 logout 方法。这将清除用户会话中的身份认证信息:

use Illuminate\Support\Facades\Auth;Auth::logout();

让其它设备上的 Session 失效

Laravel 还提供了一种机制,用于将其它设备上的用户 Session 失效和「注销」,而不会使其当前设备上的 Session 失效。首先,你需要保证 IlluminateSessionMiddlewareAuthenticateSession 中间件在你的 app/Http/Kernel.php 类中的 web 中间件组中,并且没有被注释掉:

'web' => [   
    // ...
    \Illuminate\Session\Middleware\AuthenticateSession::class,  
    // ...
  ],

然后, 你就可以使用 Auth facade 上的 logoutOtherDevices 方法。此方法要求用户提供其当前密码,你的应用程序应通过输入表单接受该密码:

use Illuminate\Support\Facades\Auth;
Auth::logoutOtherDevices($password);

{note} 当调用 logoutOtherDevices 方法后,用户的其它 Session 将完全失效,这意味着他们将「退出」他们之前通过身份认证的所有看守器。

添加自定义的看守器

你可以使用 Auth facade 的 extend 方法来定义自己的身份验证看守器。你应该在 服务提供器 中调用  extend 方法。由于 Laravel 已经附带了 AuthServiceProvider,我们可以将代码放在该提供器中:

<?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 方法的回调应该返回一个实现 IlluminateContractsAuthGuard 接口的实例。这个接口包含了一些你需要在自定义的看守器中实现的方法。当你的自定义看守器定义完成之后,你可以在 auth.php 配置文件的 guards 配置中使用这个看守器:

'guards' => [  
  'api' => [     
     'driver' => 'jwt',        
     'provider' => 'users',    
     ],
  ],

请求闭包看守器

实现基于 HTTP 请求的自定义身份验证系统的最简单方法,是使用 Auth::viaRequest 方法。此方法允许您使用单个闭包来快速定义身份验证过程。

首先,在 AuthServiceProviderboot 方法中调用 Auth::viaRequest 方法。viaRequest 方法接受一个看守器名称作为其第一个参数。此名称可以是描述你自定义看守器的任何字符串。传递给该方法的第二个参数应该是一个闭包函数,它接收传入的 HTTP 请求并返回一个用户实例,或者,如果验证失败,则为 null

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 配置中使用这个看守器:

'guards' => [   
 'api' => [      
   'driver' => 'custom-token',  
   ],
 ],

添加自定义用户提供器

如果不使用传统的关系数据库存储用户,就需要使用自己的身份验证用户提供器扩展 Lavarel。可以使用 Auth facade 的 provider 方法自定义用户提供器:

<?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{   
        /**
     * 注册任意应用身份验证 / 授权服务Register any application authentication / authorization services.
     *
     * @return void
     */   
      public function boot()   
       {      
         $this->registerPolicies();       
         Auth::provider('riak', function ($app, array $config) {         
            // 返回 Illuminate\Contracts\Auth\UserProvider 实例...            
            return new RiakUserProvider($app->make('riak.connection'));       
           });   
        }}

一旦使用 provider 方法注册完毕,就可以在 auth.php 配置文件中切换到新的用户提供器。先定义一个使用新驱动的 provider

'providers' => [  
  'users' => [      
    'driver' => 'riak',  
      ],
  ],

随后就可以在 guards 配置中使用这个提供器:

'guards' => [   
    'web' => [     
       'driver' => 'session',        
       'provider' => 'users',    
     ],
  ],

用户提供器契约

IlluminateContractsAuthUserProvider 实现仅负责从 MySQL、Riak 等持久化存储系统中提取 IlluminateContractsAuthAuthenticatable 实现。无论用户如何存储及用于表示它的类是什么类型,这两个接口都允许 Laravel 身份验证机制继续运行:

我们来看看 IlluminateContractsAuthUserProvider 契约:

<?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 函数通常接受用于表示类的 key(如 MySQL 数据库中自动递增的 ID)作为参数,并获取和返回与这个 ID 匹配的 Authenticatable 实现。

retrieveByToken 函数通过用户的唯一 $identifier 和存储在 remember_token 列的 「记住我」 令牌获取用户。与前一方法相同,它返回 Authenticatable 实现。

updateRememberToken 方法用新  $token 更新  $userremember_token 列。在「记住我」登录校验成功或者用户登出时分配「刷新令牌」。

在尝试登录到应用时,retrieveByCredentials 方法接受凭证数组传递给 Auth::attempt 方法。此方法在底层持久化存储中「查询」与这些凭证匹配的用户。通常,此方法运行一个基于 $credentials['username'] 的 「where」 条件,它应该返回一个 Authenticatable 实现。此方法不就尝试进行任何密码校验或身份验证。

validateCredentials 方法应该比较给定的 $user$credentials 来验证用户身份。例如,此方法或许应该使用 Hash::check 来比较 $user->getAuthPassword() 的值与 $credentials['password'] 的值。它应该返回 truefalse ,以表明用户密码是否有效。

身份验证契约

我们已经剖析了 UserProvider 的每个方法。下面再来看看 Authenticatable 契约。切记,用户提供器的 retrieveByIdretrieveByTokenretrieveByCredentials 方法将返回此接口的实例:

<?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 引发了多种 事件 。 可以在  EventServiceProvider 中附着这些事件的监听器:

/**
 * 应用的事件监听器映射。
 *
 * @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',   
         ],
      ];
本文章首发在 LearnKu.com 网站上。