Laravel 认证详解


管理器
管理多个拦截器,通过给定拦截器的名称,可以返回或生产拦截器并返回,那么管理器必定是一个工厂(生产拦截器)。管理器管理多个拦截器,那么必定就有一个默认拦截器的概念。

拦截器
拦截器要认证用户(用户登录),首先客户端需要提供凭证(比如客户端提供的token或对应的用户session中的用户id),如果没有凭证,就直接跳转到登录页(或返回认证失败信息),然后是拿到凭证后(如果是非一次性认证,这个时候还会到持久介质中检查是否已经登录,登录就直接通过 – 持久介质一般就是指session了),还需要知道到什么地方进行比对,这时需要有一个数据源(比如用户表)。这个数据源就是提供者,它向拦截器按照一致的方法提供用户实例。拦截器对于已经通过认证的用户,可能还需要保存认证凭证(下次请求直接通过),有保存认证凭证,当然还要包括注销凭证(用户登出)。

提供者
拦截器面对提供者,通过向提供者获取用户实例,那么提供者必须按照一致的方式编写(这样不同的拦截器就可以调用不同的提供者)。提供者是数据源的抽象,对外提供统一接口,比如根据某个一致的方法返回用户实例。

用户实例
提供者提供用户实例,拦截器使用这个用户实例来比对,很明显,如果用户实例没有一定的规范,也是不行的,比如要比对什么字段不能定死,但是可以定死方法,这个就是接口规范。

总结:
管理器是一个工厂,可以管理和生成拦截器。拦截器需要一个提供者,依赖提供者提供的用户来认证用户。提供者被拦截器调用,它需要提供一致的接口,以使得它可以插入到不同的拦截器中。提供者提供的用户实例也是面向拦截器的,为了能适应不同的拦截器,用户实例也需要按照一致的接口来实现。

由于有一致的接口,所以一个拦截器,可以更换提供者(比如使用不同的数据)。一个提供者,可以插入到不同的拦截器(比如拦截器都使用同一个提供者)。

可扩展:
对于一些常用的拦截器,比如token和session,可以预先实现,同理,对于提供者,也可以预先实现。如果需要插入自定义的拦截器或提供者,只需要按照预定实现相关方法就可以了。

以上就是用户认证的理论知识。以下就是针对这些理论知识进行展开。

管理器 – Illuminate\Auth\AuthManager:

这个类提供了怎么创建默认的Guard的方法(createSessionDriver、createTokenDriver),和怎么创建Provider的方法(createDatabaseProvider),还提供了如何插入自定义的Guard的方法(extend()方法)和插入自定义Provider的方法(provider()方法)。Guard实例的生成,对应的Provider实例必须先生成。

AuthManager中管理不同的Guard,Guard实例的生成需要知道Driver(就是怎么生成Guard实例,比如session,api),还需要知道Provider(数据源),Provider内置支持Database和Eloquent这两种类型,根据类型产生数据源对象(需要实现对应接口)。另外,还有一个默认Guard的概念。有了Guard实例后,需要从Guard实例中取回用户实例,AuthManager中记录默认Guard的用户实例(userResolver)

用户登录认证,首先需要拦截所有的请求,判断是否登录,如果没有登录,跳转到登录页,登录成功后,记录登录信息到会话(下次就知道已登录);如果登录了,跳转到首页, 还可以取到用户实例。

这里的拦截器就是Guard, 登录成功后保存信息,取回用户实例,是Guard需要实现的,另外,如何认证用户需要依赖传递给Guard的Provider,Guard收集认证信息,到Provider中查找比对做出判断。

提供者 – 用户提供者
考虑这样的应用场景:有多个系统公用一套管理员信息,即管理员A用同一套信息可以登录系统1,也可以登录系统2。

这里的管理员需要单独出来,并对外提供API,系统1和系统2以统一的方式调用这些API进行用户认证。

首先需要定义一个UserProriver,需要实现Illuminate\Contracts\Auth\UserProvider接口:

// 通过唯一标示符获取认证模型
public function retrieveById($identifier);
// 通过唯一标示符remember token获取模型
public function retrieveByToken($identifier, $token);
// 通过给定的认证模型更新remember token
public function updateRememberToken(Authenticatable $user, $token);
// 通过给定的凭证获取用户,比如 email 或用户名等等
public function retrieveByCredentials(array $credentials);
// 认证给定的用户和给定的凭证是否符合
public function validateCredentials(Authenticatable $user, array $credentials);

自定义的UserProriver需要实现这些方法(在其中调用外部API),从这个Provider返回的用户实例必须实现了Illuminate\Contracts\Auth\Authenticatable接口的实例(可以认证的):

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

注:App\User实现了该接口(可认证),实际是使用了Illuminate\Auth\Authenticatable特性。

Illuminate\Auth\GenericUser也实现了这个接口,可以直接套用(DatabaseUserProvider就用它来包装用户实例),否则需要自己实现(比如字段名称不一样),具体实现可以参考Illuminate\Auth\DatabaseUserProvider。

图中MyUser是一个自定义实现,除非需要自定义各种字段,否则没有必要,直接使用GenericUser来对应用户实例即可。

接下来是调用AuthManager的provider()方法来定义新的Provider(数据源):

// App\Providers\AuthServiceProvider
public function boot()
    {
        $this->registerPolicies();

        \Auth::provider('my-user', function() {
            return new MyUserProvider();    // 返回自定义的 user provider
        });
    }

然后修改配置,让Guard使用my-user作为Provider:

// app/config/auth.php
'guards' => [
        'web2' => [
            'driver' => 'session',
            'provider' => 'my-user'  //名字可以随意,只要在providers中找到
        ],
],
'providers' => [
        'my-user' => [
            'driver' => 'my-user'  //名字必须和在\Auth::provider()定义时的一样
        ]
],

这里的’providers’中的’my-user’是否需要其它的参数(driver参数必须,并且和定义时的值一样),是由自定义的Provider决定的,可以根据情况添加。

设置 config\auth.php 的 defaults.guard 为web2,就可以使用自定义的Provider进行认证了。

拦截器 – Guard
考虑这样的应用场景:如果需要修改登录认证逻辑,比如对认证做一些额外的处理,默认提供的Guard不能满足的情况。

Illuminate\Contracts\Auth\StatefulGuard接口在Illuminate\Contracts\Auth\Guard
上提供了额外方法,比如:login() attempt() logout()等(Session认证必须)。

每个Guard实际都注入了Illuminate\Auth\GuardHelpers, 这是一些辅助方法,比如:check() 、guest()通用的方式实现 。Guard中进行拦截是实际是跟对应的UserProvider进行交互,取回用户实例(必须实现Illuminate\Contracts\Auth\Authenticatable接口),

Illuminate\Contracts\Auth\Guard接口:

// 判断当前用户是否登录
public function check();
// 判断当前用户是否是游客(未登录)
public function guest();
// 获取当前认证的用户
public function user();
// 获取当前认证用户的 id,严格来说不一定是 id,应该是上个模型中定义的唯一的字段名
public function id();
// 根据提供的消息认证用户
public function validate(array $credentials = []);
// 设置当前用户
public function setUser(Authenticatable $user);

Illuminate\Contracts\Auth\StatefulGuard继承Guard接口,还提供了:

// 尝试根据提供的凭证验证用户是否合法
public function attempt(array $credentials = [], $remember = false);
// 一次性登录,不记录session or cookie
public function once(array $credentials = []);
// 登录用户,通常在验证成功后记录 session 和 cookie 
public function login(Authenticatable $user, $remember = false);
// 使用用户 id 登录
public function loginUsingId($id, $remember = false);
// 使用用户 ID 登录,但是不记录 session 和 cookie
public function onceUsingId($id);
// 通过 cookie 中的 remember token 自动登录
public function viaRemember();
// 登出
public function logout();

预定义的拦截器:
1 RequestGuard
Illuminate\Auth\RequestGuard
RequestGuard是一个非常简单的Guard。RequestGuard 是通过传入一个闭包来认证的。可以通过调用 Auth::viaRequest 添加一个自定义的 RequestGuard.(每个请求,都执行这个闭包,这个闭包负责怎么认证用户,当然,数据来源,怎么比对数据,都需要在这个闭包中完成)

2 SessionGuard
Illuminate\Auth\SessionGuard
SessionGuard 是 Laravel web 认证默认的 guard(用户认证后,记录到session,第二次请求就不会再次认证).

3 TokenGuard
Illuminate\Auth\TokenGuard
TokenGuard 适用于无状态 api 认证,通过 token 认证(请求中携带token,用token认证用户,每次请求都会进行认证).

要实现自定义的Guard,可以参考以上的实现,然后调用AuthManager的extend()方法来定义新的Guard(Driver):

// App\Providers\AuthServiceProvider
public function boot()
    {
        $this->registerPolicies();

        \Auth::extend('web2', function(){
              return new MyGuard();
        });
    }

Guard需要使用到Provider(构造函数中注入),完成验证,检查等操作。

另外
Laravel支持在认证过程中触发多种事件,可以在的EventServiceProvider中监听这些事件:

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',
    ],
];