<h1>扩展用户认证系统</h1> <p>上一节咱们介绍了Laravel Auth系统实现的一些细节知道了Laravel是如何应用看守器和用户提供器来进行用户认证的,可是针对咱们本身开发的项目或多或少地咱们都会须要在自带的看守器和用户提供器基础之上作一些定制化来适应项目,本节我会列举一个在作项目时遇到的具体案例,在这个案例中用自定义的看守器和用户提供器来扩展了Laravel的用户认证系统让它能更适用于咱们本身开发的项目。</p> <p>在介绍用户认证系统基础的时候提到过Laravel自带的注册和登陆验证用户密码时都是去验证采用<code>bcypt</code>加密存储的密码,可是不少已经存在的老系统中用户密码都是用盐值加明文密码作哈希后存储的,若是想要在这种老系统中应用Laravel开发项目的话那么咱们就不可以再使用Laravel自带的登陆和注册方法了,下面咱们就经过实例看看应该如何扩展Laravel的用户认证系统让它可以知足咱们项目的认证需求。</p> <h3>修改用户注册</h3> <p>首先咱们将用户注册时,用户密码的加密存储的方式由<code>bcypt</code>加密后存储改成由盐值与明文密码作哈希后再存储的方式。这个很是简单,上一节已经说过Laravel自带的用户注册方法是怎么实现了,这里咱们直接将<code>\App\Http\Controllers\Auth\RegisterController</code>中的<code>create</code>方法修改成以下:</p>php
/** * Create a new user instance after a valid registration. * * @param array $data * @return User */ protected function create(array $data) { $salt = Str::random(6); return User::create([ 'email' => $data['email'], 'password' => sha1($salt . $data['password']), 'register_time' => time(), 'register_ip' => ip2long(request()->ip()), 'salt' => $salt ]); }
<p>上面改完后注册用户后就能按照咱们指定的方式来存储用户数据了,还有其余一些须要的与用户信息相关的字段也须要存储到用户表中去这里就再也不赘述了。</p> <h3>修改用户登陆</h3> <p>上节分析Laravel默认登陆的实现细节时有说登陆认证的逻辑是经过<code>SessionGuard</code>的<code>attempt</code>方法来实现的,在<code>attempt</code>方法中<code>SessionGuard</code>经过<code>EloquentUserProvider</code>的<code>retriveBycredentials</code>方法从用户表中查询出用户数据,经过 <code>validateCredentials</code>方法来验证给定的用户认证数据与从用户表中查询出来的用户数据是否吻合。</p> <p>下面直接给出以前讲这块时引用的代码块:</p>git
class SessionGuard implements StatefulGuard, SupportsBasicAuth { public function attempt(array $credentials = [], $remember = false) { $this->fireAttemptEvent($credentials, $remember); $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); //若是登陆认证经过,经过login方法将用户对象装载到应用里去 if ($this->hasValidCredentials($user, $credentials)) { $this->login($user, $remember); return true; } //登陆失败的话,能够触发事件通知用户有可疑的登陆尝试(须要本身定义listener来实现) $this->fireFailedEvent($user, $credentials); return false; } protected function hasValidCredentials($user, $credentials) { return ! is_null($user) && $this->provider->validateCredentials($user, $credentials); } } class EloquentUserProvider implements UserProvider { 从数据库中取出用户实例 public function retrieveByCredentials(array $credentials) { if (empty($credentials) || (count($credentials) === 1 && array_key_exists('password', $credentials))) { return; } $query = $this->createModel()->newQuery(); foreach ($credentials as $key => $value) { if (! Str::contains($key, 'password')) { $query->where($key, $value); } } return $query->first(); } //经过给定用户认证数据来验证用户 /** * Validate a user against the given credentials. * * @param \Illuminate\Contracts\Auth\Authenticatable $user * @param array $credentials * @return bool */ public function validateCredentials(UserContract $user, array $credentials) { $plain = $credentials['password']; return $this->hasher->check($plain, $user->getAuthPassword()); } }
<h3>自定义用户提供器</h3> <p>好了, 看到这里就很明显了, 咱们须要改为本身的密码验证就是本身实现一下<code>validateCredentials</code>就能够了, 修改<code>$this->hasher->check</code>为咱们本身的密码验证规则。</p> <p>首先咱们来重写<code>$user->getAuthPassword();</code> 在User模型中覆盖其从父类中继承来的这个方法,把数据库中用户表的<code>salt</code>和<code>password</code>传递到<code>validateCredentials</code>中来:</p>github
class user extends Authenticatable { /** * 覆盖Laravel中默认的getAuthPassword方法, 返回用户的password和salt字段 * @return array */ public function getAuthPassword() { return ['password' => $this->attributes['password'], 'salt' => $this->attributes['salt']]; } }
<p>而后咱们用一个自定义的用户提供器,经过它的<code>validateCredentials</code>来实现咱们本身系统的密码验证规则,因为用户提供器的其它方法不用改变沿用<code>EloquentUserProvider</code>里的实现就能够,因此咱们让自定义的用户提供器继承自<code>EloquentUserProvider</code>:</p>web
namespace App\Foundation\Auth; use Illuminate\Auth\EloquentUserProvider; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Support\Str; class CustomEloquentUserProvider extends EloquentUserProvider { /** * Validate a user against the given credentials. * * @param \Illuminate\Contracts\Auth\Authenticatable $user * @param array $credentials */ public function validateCredentials(Authenticatable $user, array $credentials) { $plain = $credentials['password']; $authPassword = $user->getAuthPassword(); return sha1($authPassword['salt'] . $plain) == $authPassword['password']; } }
<p>接下来经过<code>Auth::provider()</code>将<code>CustomEloquentUserProvider</code>注册到Laravel系统中,<code>Auth::provider</code>方法将一个返回用户提供器对象的闭包做为用户提供器建立器以给定名称注册到Laravel中,代码以下:</p>数据库
class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. * * @return void */ public function boot() { \Auth::provider('custom-eloquent', function ($app, $config) { return New \App\Foundation\Auth\CustomEloquentUserProvider($app['hash'], $config['model']); }); } ...... }
<p>注册完用户提供器后咱们就能够在<code>config/auth.php</code>里配置让看守器使用新注册的<code>custom-eloquent</code>做为用户提供器了:</p>json
//config/auth.php 'providers' => [ 'users' => [ 'driver' => 'coustom-eloquent', 'model' => \App\User::class, ] ]
<h3>自定义认证看守器</h3> <p>好了,如今密码认证已经修改过来了,如今用户认证使用的看守器仍是<code>SessionGuard</code>, 在系统中会有对外提供API的模块,在这种情形下咱们通常但愿用户登陆认证后会返回给客户端一个JSON WEB TOKEN,每次调用接口时候经过这个token来认证请求接口的是不是有效用户,这个需求须要咱们经过自定义的Guard扩展功能来完成,有个<code>composer</code>包<code>"tymon/jwt-auth": "dev-develop"</code>, 他的1.0beta版本带的<code>JwtGuard</code>是一个实现了<code>Illuminate\Contracts\Auth\Guard</code>的看守器彻底符合我上面说的要求,因此咱们就经过<code>Auth::extend()</code>方法将<code>JwtGuard</code>注册到系统中去:</p>segmentfault
class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. * * @return void */ public function boot() { \Auth::provider('custom-eloquent', function ($app, $config) { return New \App\Foundation\Auth\CustomEloquentUserProvider($app['hash'], $config['model']); }); \Auth::extend('jwt', function ($app, $name, array $config) { // 返回一个 Illuminate\Contracts\Auth\Guard 实例... return new \Tymon\JWTAuth\JwtGuard(\Auth::createUserProvider($config['provider'])); }); } ...... }
<p>定义完以后,将 <code>auth.php</code> 配置文件的<code>guards</code>配置修改以下:</p>api
'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'jwt', // token ==> jwt 'provider' => 'users', ], ],
<p>接下来咱们定义一个API使用的登陆认证方法, 在认证中会使用上面注册的<code>jwt</code>看守器来完成认证,认证完成后会返回一个JSON WEB TOKEN给客户端</p>session
Route::post('apilogin', 'Auth\LoginController@apiLogin');
class LoginController extends Controller { public function apiLogin(Request $request) { ... if ($token = $this->guard('api')->attempt($credentials)) { $return['status_code'] = 200; $return['message'] = '登陆成功'; $response = \Response::json($return); $response->headers->set('Authorization', 'Bearer '. $token); return $response; } ... } }
<h3>总结</h3> <p>经过上面的例子咱们讲解了如何经过自定义认证看守器和用户提供器扩展Laravel的用户认证系统,目的是让你们对Laravel的用户认证系统有一个更好的理解知道在Laravel系统默认自带的用户认证方式没法知足咱们的需求时如何经过自定义这两个组件来扩展功能完成咱们项目本身的认证需求。</p> <p>本文已经收录在系列文章<a href="https://github.com/kevinyan815/Learning_Laravel_Kernel" rel="nofollow noreferrer">Laravel源码学习</a>里,欢迎访问阅读。</p>闭包