Laravel Authentication 认证系统

Authentication 认证系统使用

Authentication
学习笔记《Laravel Auth 代码阅读》

花了两天研究 Laravel,昨天是通宵达旦搞到将近凌晨5点,基本掌握系统架构,可以说是博大精深!今天继续,主要心思放到整个 Laravel Auth 子系统中来了。

基本流程解读

HTTP本身是无状态,通常在系统交互的过程中,使用账号或者Token标识来确定认证用户,Token 一般会以 Cookie 方式保存到客户端,客户端发送请求时一并将 Token 发回服务器。如果客服端没有 Cookie,通常做法时时在 URL 中携带 Token。也可以 HTTP 头信息方式携带 Token 到服务器。Laravel 提供 Session 和 URL 中携带 token 两种方式做认证。对应配置文件中的 guards 配置的 web、api。

web 认证基于 Session 根据 SessionId 获取用户,provider 查询关联用户;api认证是基于token值交互,也采用users这个provider;

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\User::class,
    ],

    // 'users' => [
    //     'driver' => 'database',
    //     'table' => 'users',
    // ],
],

配置文件 /config/auth.php 中的 provider 是提供用户数据的接口,要标注驱动对象和目标对象,默认值 users是一套 provider 的名字,采用 eloquent 驱动,对应模类是 App\User。也可以使用 database 的方式。

Laravel 内置了认证模块,也可以手动安装,命令会生成相关的视图文件:

artisan make:auth

app
+-- Http
|   +-- Middleware
|   |   +-- RedirectIfAuthenticated.php
|   +-- Controllers
|       +-- Auth
|           +-- ForgotPasswordController.php
|           +-- LoginController.php
|           +-- RegisterController.php
|           +-- ResetPasswordController.php
+-- Providers
|   +-- AuthServiceProvider.php
+-- User.php

整个认证个模块包括相应的视图文件,一个 RedirectIfAuthenticated 中间件和四个 Controller 文件,一个 User 模型文件,它是 Authenticatable 接口类。另外还有一个 AuthServiceProvider 它是服务容器,为注入认证用户的数据提供者 UserProvider 接口准备的。密码数据是从 Authenticatable 流向 UserProvider 的,前者读取数据,后者负责校验。列如可以尝试这样覆盖 getAuthPassword() 方法来测试数据逻辑,而不是从数据库中读入数据:

public function getAuthPassword(){
    return bcrypt("userpass");
}

此方法原本是在 Illuminate\Auth\Authenticatable 定义的,它是 trait Authenticatable 类,是 PHP 多继承的规范。这个方法就是简单地返回 $this->password,即模型关联的数据表字段 password 的值,数据是通过依赖注入的。

app/Http/Kernel.php 注册中间件:

protected $routeMiddleware = [
    // ...
    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
];

Auth模块从功能上分为用户认证和权限管理两个部分;

Illuminate\Auth是负责用户认证和权限管理的模块;
Illuminate\Foundation\Auth 提供了登录、修改密码、重置密码等一系统列具体逻辑实现;
Illuminate\Foundation\Auth\AuthenticatesUsers 负责登录视图逻辑
Illuminate\Auth\Passwords 目录下是密码重置或忘记密码处理的小模块;
config\auth.php认证相关配置文件

RedirectIfAuthenticated 只有 handle() 处理逻辑,但 Auth 类中并没有定义 Auth::guard(),这是经过 Facades 编程模式,通过 __callStatic() 关联到了 AuthManager,代码注解中有提示。Facade 是一套配合 Service Container 的静态方法解决方案,是一套设计得非常优雅的机制,是 Laravel 的核心机制之一。

public function handle($request, Closure $next, $guard = null)
{
    if (Auth::guard($guard)->check()) {
        return redirect('/home');
    }

    return $next($request);
}

通过获取配置文件的 guards 设置,不同的依赖就会加载进来:

HTTP Basic: /src/Illuminate/Auth/RequestGuard.php
Session: /src/Illuminate/Auth/SessionGuard.php
Token: /src/Illuminate/Auth/TokenGuard.php

这些按标准接口定义的类都有 user() 方法,这个方法就是从数据库获取匹配的授权用户,用户数据是通过 UserProvider 接口提供的,即 /app/Providers/AuthServiceProvider.php。通过实现此接口可以定制自己的认证逻辑,UserProvider::validateCredentials() 就是检验密码的方法。自带的 /app/User.php 模型就是实现了 Authenticatable 接口的类。

public function user()
{
    if ($this->loggedOut) {
        return;
    }

    if (! is_null($this->user)) {
        return $this->user;
    }

    $id = $this->session->get($this->getName());

    if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {
        $this->fireAuthenticatedEvent($this->user);
    }

    if (is_null($this->user) && ! is_null($recaller = $this->recaller())) {
        $this->user = $this->userFromRecaller($recaller);

        if ($this->user) {
            $this->updateSession($this->user->getAuthIdentifier());

            $this->fireLoginEvent($this->user, true);
        }
    }

    return $this->user;
}

/Illuminate/Routing/Router.php 中已经定义好和认证相关的一组路由,只需在 /routes/web.php 中添加一句 Auth::routes(); 就可以注册这些路由,这些路由连接的控制器就前面安装认证模块生成的。

public function auth(array $options = [])
{
    // Authentication Routes...
    $this->get('login', 'Auth\LoginController@showLoginForm')->name('login');
    $this->post('login', 'Auth\LoginController@login');
    $this->post('logout', 'Auth\LoginController@logout')->name('logout');

    // Registration Routes...
    if ($options['register'] ?? true) {
        $this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
        $this->post('register', 'Auth\RegisterController@register');
    }

    // Password Reset Routes...
    if ($options['reset'] ?? true) $this->resetPassword();

    // Email Verification Routes...
    if ($options['verify'] ?? false) $this->emailVerification();
}

public function resetPassword()
{
    $this->get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
    $this->post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
    $this->get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
    $this->post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update');
}

public function emailVerification()
{
    $this->get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
    $this->get('email/verify/{id}', 'Auth\VerificationController@verify')->name('verification.verify');
    $this->get('email/resend', 'Auth\VerificationController@resend')->name('verification.resend');
}

来看第一条路由,是一条命名路由 name(),执行控制器的方法是 LoginController->showLoginForm() ,这个方法会调出视图 view('auth.login'),需要自己建立视图文件 \resources\views\auth\login.blade.php,可以使用命令 php artisan make:auth 来生成。登录表单大概是这样,@csrf 这是默认需要添加的参数,除非在 VerifyCsrfToken.php中间关闭了校验。

<style>
    .frame { width:50%; margin:auto; padding:32px; background: #484848; border-radius: 4px; color:white; }
    .filed { padding:16px; border:1px solid #4E6DB0; border-radius: 16px; background: #282828; margin:16px;}
    .center { text-align: center; }
</style>

<div class="frame">
    <form method="post">
        {{ csrf_field() }}
        <!-- @csrf -->
        <div class="filed">{{ __('E-Mail Address') }}: <input type="text" name="email"></div>
        <div class="filed">{{ __('Password') }}: <input type="password" name="password"></div>
        <div class="filed center">
        <button type="submit">{{ __('Submit') }}</button>
            <input type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>
            <label class="form-check-label" for="remember">{{ __('Remember Me') }}</label>
        </div>
    </form>
</div>

所有post请求中必须包含一个 @crsf 的字段用以防止跨域攻击,只有通过验证才认为是安全的提交动作,否则会得到 419 Page Expired。打开 app\Http\Middleware\VerifyCsrfToken.php 添加排除规则即可关闭:

protected $except = [
    'login',
];

只是,LoginController 类并没有定义这个方法,这个方法定义在 AuthenticatesUsers 中定义的,通过 use 关键字引入 trait Authenticatable,这相当于 PHP 的多继承用法。

use AuthenticatesUsers;

emailpassword 两个变量用于匹配用户,密码生成使用的是 bcrypt() 方法,自带的 DatabaseSeeder.php 中含有初始化数据,按需要去执行命令填充到数据库 artisan db:seed

还有一些其他的认证方法:

Auth::check() 判断当前用户是否已认证(是否已登录)
Auth::user() 获取当前的认证用户
Auth::id() 获取当前的认证用户的 ID(未登录情况下会报错)
Auth::attempt(['email' => $email, 'password' => $password], $remember)尝试对用户进行认证
Auth::attempt($credentials, true) 通过传入 true 值来开启 '记住我' 功能
Auth::once($credentials) 只针对一次的请求来认证用户
Auth::login($user)
Auth::login($user, true) // Login and "remember" the given user...
Auth::login(User::find(1), $remember) 登录一个指定用户到应用上
Auth::guard('admin')->login($user)
Auth::loginUsingId(1) 登录指定用户 ID 的用户到应用上
Auth::loginUsingId(1, true) // Login and "remember" the given user...
Auth::logout() 使用户退出登录(清除会话)
Auth::validate($credentials) 验证用户凭证
Auth::viaRemember() 是否通过记住我登录
Auth::basic('username') 使用 HTTP Basic Auth 的基本认证方式来认证
Auth::onceBasic() 执行「HTTP Basic」登录尝试
Password::remind($credentials, function($message, $user){}) 发送密码重置提示给用户

Adding Custom Guards

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
{
    /**
     * Register any application authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Auth::extend('jwt', function ($app, $name, array $config) {
            // Return an instance of Illuminate\Contracts\Auth\Guard...

            return new JwtGuard(Auth::createUserProvider($config['provider']));
        });
    }
}

/config/auth.php 中配置自定义的 Guards:

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

Closure Request Guards

use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

/**
 * Register any application authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Auth::viaRequest('custom-token', function ($request) {
        return User::where('token', $request->token)->first();
    });
}

/config/auth.php 中配置自定义的 Guards:

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

Adding Custom User Providers

如果不使用传统的关系数据库,可以自定义 Provider 可以实现自己的认证逻辑,如实现一个 riak Provider:

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) {
            // Return an instance of Illuminate\Contracts\Auth\UserProvider...

            return new RiakUserProvider($app->make('riak.connection'));
        });
    }
}

/config/auth.php 中配置自定义的 riak:

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

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

The User Provider Contract

Illuminate\Contracts\Auth\UserProvider contract 接口定义:

namespace Illuminate\Contracts\Auth;

interface UserProvider {

    public function retrieveById($identifier);
    public function retrieveByToken($identifier, $token);
    public function retrieveByCredentials(array $credentials);
    public function updateRememberToken(Authenticatable $user, $token);
    public function validateCredentials(Authenticatable $user, array $credentials);

}

retrieveById(), retrieveByToken(), and retrieveByCredentials() 方法返回的对象需要实现 Authenticatable 接口。

The Authenticatable Contract

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

}

Events 认证事件

认证过程中(包括注册、忘记密码),定义了相关事件,实现自己的 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', ], 
];
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,681评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,710评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,623评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,202评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,232评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,368评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,795评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,461评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,647评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,476评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,525评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,226评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,785评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,857评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,090评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,647评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,215评论 2 341

推荐阅读更多精彩内容