总结Laravel用户认证

用了挺久的 laravel,因为它实在是太方便了,很多东西都是开箱即用的。
现在系统越来越大了,准备开始拆分模块,首先先拆将用户认证拆出来,之前专注于业务逻辑,没有自己研究过laravel用户认证这块,这里自我总结下,也感谢各位群里大大佬的指点:

Laravel的用户认证模块:

  • Guard:守卫者,主要提供认证的机制,检查用户认证情况,laravel默认提供三种Guard
    • TokenGuard:主要用于无状态的API认证
    • SessionGuard:默认的web认证方式
    • RequestGuard(自定义会调函数的认证方式)
      我这里主要总结下SessionGuard
  • Provider:数据提供者,返回用户数据,提供给Guard用于登录、登出及认证等请求,主要有两种Provider:
    • EloquentUserProvider 通过ORM模型获取认证用户,底层其实就是{Model}::find($id)
    • DatabaseUserProvider 通过数据库表,底层实际是:DB::table({table})...方式
      两者本质上都是通过查询数据库方式获取获取合法的用户数据

那么问题来了,如果是基于API的web服务,服务本身没有专用的数据库时候这个时候就会显得很麻烦了,这也不符合微服务的逻辑 ,这也是我这次主要要解决的问题
好在Laravel太强大了,很多东西可以自定义的,所以我们可以继续使用SessionGuard(因为够强大了), 通过自定义UserProvider从接口获取用户数据,这样就可以与数据库解耦。

Laravel实现方式分析

Laravel中用户认证的模块的接口规范定义在:namespace Illuminate\Contracts\Auth,
框架实现的认证类位于:namespace Illuminate\Auth

基础的GuardAPI:Illuminate\Contracts\Auth\Guard;所有的Guard都应直接或简介实现当前Guar的接口,而Illuminate\Auth\GuardHelpers就是已经为我们实现好,具体实现方式可以研究下GuardHelpers,接口主要定义了如下方法,:

    //检查用户属否登录
    public function check();
    //游客检测
    public function guest();
    //获取当前认证的登录用户
    public function user();
    //获取当前认证的用户主键(id)
    public function id();
    //根据提供的凭证来验证登录
    public function validate(array $credentials = []);
    //设置用户,登录成功、check()认证成后都将user保存至当前请求生命周期对象,提高效率
    public function setUser(Authenticatable $user);

Statefuluard继承自Guard,在其基础上增加了额外接口,如loginloginUsingId等开箱即用的方法,SessionGuard中实现:

// 尝试登录的凭证是否合法
public function attempt(array $credentials = [], $remember = false);
//登录一次成功后不做任何处理
public function once(array $credentials = []);
// 登录用户,成功后记录登录数据 
public function login(Authenticatable $user, $remember = false);
// 使用用户ID登录
public function loginUsingId($id, $remember = false);
// 使用用户ID登录,同once
public function onceUsingId($id);
// 通过remember token 自动登录
public function viaRemember();
// 登出
public function logout();

接下去翻一下SessionGuard的代码,个人觉得它已经很强大了,对于web模块应用来说大部分场景都已经足够用了

class SessionGuard implements StatefulGuard, SupportsBasicAuth
{
    //GuardHelpers 实现接口
    use GuardHelpers, Macroable;
    ...
    public function __construct($name,
        UserProvider $provider,
        Session $session,
        Request $request = null)
    {
        $this->name = $name;
        $this->session = $session;
        $this->request = $request;
        $this->provider = $provider;
    }
    ...
    public function login(AuthenticatableContract $user, $remember = false)
    {
        .....
        $this->updateSession($user->getAuthIdentifier());
        ......
        $this->setUser($user);
    }
    ...
}

可以看到要使用其登录的话必须要传入一个实现了的\Illuminate\Contracts\Auth\Authenticatable接口的用户对象,其实主要设计用于自定义及获取主键,要实现的模型接口如下:

// 唯一标识,数据库的话对应的默认是主键
public function getAuthIdentifierName();
// 获取唯一标识的值
public function getAuthIdentifier();
// 获取认证的密码
public function getAuthPassword();
// 获取remember token
public function getRememberToken();
// 设置 remember token
public function setRememberToken($value);
// 获取 remember token 对应的字段名,比如默认的 'remember_token'
public function getRememberTokenName();

所以我们应该在模型中定义一个用于设置及获取认证的模型实现上述接口,别慌Laravel安装完也为我们准备了对应的模型,这里是我将模型移动到Models下
总结Laravel用户认证

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    ...
}

追踪Illuminate\Foundation\Auth\User

<?php

namespace Illuminate\Foundation\Auth;

use Illuminate\Auth\Authenticatable;
...

class User extends Model implements
    AuthenticatableContract,
    AuthorizableContract,
    CanResetPasswordContract
{
    use Authenticatable, Authorizable, CanResetPassword, MustVerifyEmail;
}

可以发现在Illuminate\Auth\Authenticatable中实现了上述唯一标识相关的方法,至此已完成整个Auth::login流程

接下来再我们请求须要认证的路由时候,例如:

Route::group(['middleware' => ['auth']], function () {
    Route::get("/", "HomeController@home");
});

当前路由须要经过中间件的认证,而中间件的定义namespace App\Http\Kernel

    ...
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
    ];
    ...

可以看到我们在route中定义的auth中间件\App\Http\Middleware\Authenticate::class,实际上为:
Illuminate\Auth\Middleware\Authenticate,查看代码:

......
public function handle($request, Closure $next, ...$guards)
{
      $this->authenticate($request, $guards);

     return $next($request);
}

......
protected function authenticate($request, array $guards)
{
    if (empty($guards)) {
        $guards = [null];
    }
    foreach ($guards as $guard) {
        if ($this->auth->guard($guard)->check()) {
            return $this->auth->shouldUse($guard);
        }
    }
    $this->unauthenticated($request, $guards);
}

看到整个认证的核心:获得须要认证的guard并调用check方法,check方法体位于GuardHelpers, 也就是我们如果须要自定义Guard的须实现Illuminate\Contracts\Auth\Guard中定义的接口
check中的代码很简单:return ! is_null($this->user());, 主要的逻辑还是根据官方默认或自定义的Guard中实现的user()逻辑

先贴上Illuminate\Auth\SessionGuard::user()核心代码

public function user()
{
    ...
    //获取唯一标识
    $id = $this->session->get($this->getName());
    ...
    //根据标识读取用户数据
    if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {
        $this->fireAuthenticatedEvent($this->user);
    }
    ......
    //从remember me中读取自动登录
    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);
        }
    }
...
}

$this->user = $this->provider->retrieveById($id)),可以看出,这时候用到了从provider中获取认证用户数据
laravel默认的ProviderIlluminate\Auth\EloquentUserProvider,它实现Illuminate\Contracts\Auth\UserProvider中申明的接口

namespace Illuminate\Contracts\Auth;

interface UserProvider
{
    //根据唯一标识符获取用户
    public function retrieveById($identifier);
    //根据token读取用户唯一标识符,无状态API适用
    public function retrieveByToken($identifier, $token);
    //基于认证模型更新token
    public function updateRememberToken(Authenticatable $user, $token);
    //基于认证模型获取用户
    public function retrieveByCredentials(array $credentials);
    //根据用户的给定的凭证认证用户
    public function validateCredentials(Authenticatable $user, array $credentials);
}

有了以上接口就可以自定义Provider来处理不通的逻辑,义EloquentUserProvider::retrieveById()为例子

public function retrieveById($identifier)
{
    $model = $this->createModel();

    return $this->newModelQuery($model)
        ->where($model->getAuthIdentifierName(), $identifier)
        ->first();
}

很简单就是获取唯一标识字段名以及值从数据库中获取一条数据,这就是我们平常经常用的Model::query()->where('id', {$id})->first()
返回ORM对象->SessionGuard::user()->check()方法至此认证完成,那么我们可以开始配置使用它了。
打开config/auth.php

return [

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

        'api' => [
            'driver' => 'session',
            'provider' => 'token',
            'hash' => false,
        ],
    ],
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\User::class,
        ],

        'token' => [
            'driver' => 'token',
            'model' => App\Models\User::class,
        ]
];

default中默认使用了webguard,而提供了webapi两种方式guard,及userstoken两种方式provider,我这里这样就可以根据自己的需求来定制Guard级Provider从而实现定制Auth认证方式。
因为SessionGuard,功能实在是太强大了所以,我这里只是自定义了UserProvider并实现了对应的接口,将从用户中心account.my-domain.com获取认证用户数据,从而将应用于数据库解耦合。

大家可以根据自己的需求从而定制Guard及Provider。

代码实现

其实都说的差不多了,也就是在app/Auth/下建立UserProvider实现接口写自己的逻辑。

Thanks

闲话一句:本人比较擅长写api文档,清单式的流程设计,教程文档比较少些,写的不足的地方请多包涵,多指教谢谢。

也感谢@JeffreyBool这篇文章给予启发:基于 Laravel Auth 实现自定义接口 API 用户认证详解

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
最佳答案

文档的内容实现一遍基本就清晰了:
用户认证《Laravel 8 中文文档》

用户认证里有两个概念:Guard负责认证逻辑,Provider负责提供用户信息。
所以
如果觉得官方Guard和第三方Guard不适用,自己写一个GuardGuard需要实现 Illuminate\Contracts\Auth\Guard,只要实现check方法就能完成身份校验。如果觉得每次都差用户信息没必要,check里就不要去获取用户信息好了。
如果觉得现有的Guard还不错,只是每次都查数据库太影响性能,自己写一个ProviderIlluminate\Contracts\Auth\UserProvider,自定义获取用户信息的方式,比如从缓存取,没几行代码。

Auth::user()只要自定义的Guard实现了user方法就可以用了,有user信息直接返回,没有去proivder取一下,也没几行代码
:flushed:

3年前 评论
recminy (楼主) 3年前
讨论数量: 5

你用jwt 不也是通过auth()->user()么

3年前 评论

@lovnie 可能是我想歪了,这两天在把源码扒出来看他的实现原理,还没捋到烟开云散

3年前 评论

文档的内容实现一遍基本就清晰了:
用户认证《Laravel 8 中文文档》

用户认证里有两个概念:Guard负责认证逻辑,Provider负责提供用户信息。
所以
如果觉得官方Guard和第三方Guard不适用,自己写一个GuardGuard需要实现 Illuminate\Contracts\Auth\Guard,只要实现check方法就能完成身份校验。如果觉得每次都差用户信息没必要,check里就不要去获取用户信息好了。
如果觉得现有的Guard还不错,只是每次都查数据库太影响性能,自己写一个ProviderIlluminate\Contracts\Auth\UserProvider,自定义获取用户信息的方式,比如从缓存取,没几行代码。

Auth::user()只要自定义的Guard实现了user方法就可以用了,有user信息直接返回,没有去proivder取一下,也没几行代码
:flushed:

3年前 评论
recminy (楼主) 3年前

关于每次查询数据库,我有一个扩展出来放缓存的,你可以参考。参照我以前发的文章

3年前 评论
任飘渺

用户认证本来就是每次执行需要认证的操作带有认证信息发送给后台每次验证。 不是API方式的不也是每次认证SESSION吗?

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
1
粉丝
0
喜欢
5
收藏
6
排名:2572
访问:393
私信
所有博文
社区赞助商