用了挺久的 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 中用户认证的模块的接口规范定义在: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, 在其基础上增加了额外接口,如 login、loginUsingId 等开箱即用的方法,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 下
<?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 默认的 Provider 为 Illuminate\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, 而提供了 web、api 两种方式 guard,及 users、token 两种方式 provider,我这里这样就可以根据自己的需求来定制 Guard 级 Provider 从而实现定制 Auth 认证方式。
因为 SessionGuard,功能实在是太强大了所以,我这里只是自定义了 UserProvider 并实现了对应的接口,将从用户中心 account.my-domain.com 获取认证用户数据,从而将应用于数据库解耦合。
大家可以根据自己的需求从而定制 Guard 及 Provider。
666