前言
Laraval
自带的用户认证系统Auth
非常强大易用,不过在Laravel
的用户认证系统中用户注册、登录、找回密码这些模块中用到密码加密和认证算法时使用的都是bcrypt
,而很多之前做的项目用户表里都是采用存储salt + password加密字符串的方式来记录用户的密码的,这就给使用Laravel
框架来重构之前的项目带来了很大的阻力,不过最近自己通过在网上找资料、看社区论坛、看源码等方式完成了对Laravel Auth的修改,在这里分享出来希望能对其他人有所帮助。 开篇之前需要再说明下如果是新项目应用Laravel框架,那么不需要对Auth进行任何修改,默认的bcrypt
加密算法是比salt + password更安全更高效的加密算法。
修改用户注册
首先,在laravel 里启用验证是用的artisan命令
php artisan make:auth
执行完命令后在routes文件(位置:app/Http/routes.php)会多一条静态方法调用
Route::auth();
这个Route是Laravel
的一个Facade
(位于Illuminate\Support\Facades\Route), 调用的auth方法定义在Illuminate\Routing\Router类里, 如下可以看到auth方法里就是定义了一些Auth
相关的路由规则
/**
* Register the typical authentication routes for an application.
*
* @return void
*/
public function auth()
{
// Authentication Routes...
$this->get('login', 'Auth\AuthController@showLoginForm');
$this->post('login', 'Auth\AuthController@login');
$this->get('logout', 'Auth\AuthController@logout');
// Registration Routes...
$this->get('register', 'Auth\AuthController@showRegistrationForm');
$this->post('register', 'Auth\AuthController@register');
// Password Reset Routes...
$this->get('password/reset/{token?}', 'Auth\PasswordController@showResetForm');
$this->post('password/email', 'Auth\PasswordController@sendResetLinkEmail');
$this->post('password/reset', 'Auth\PasswordController@reset');
}
通过路由规则可以看到注册时请求的控制器方法是AuthController
的register
方法, 该方法定义在\Illuminate\Foundation\Auth\RegistersUsers这个traits
里,AuthController在类定义里引入了这个traits
.
/**
* Handle a registration request for the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function register(Request $request)
{
$validator = $this->validator($request->all());
if ($validator->fails()) {
$this->throwValidationException(
$request, $validator
);
}
Auth::guard($this->getGuard())->login($this->create($request->all()));
return redirect($this->redirectPath());
}
在register方法里首先会对request里的用户输入数据进行验证,你只需要在AuthController
的validator
方法里定义自己的每个输入字段的验证规则就可以
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:user',
'password' => 'required|size:40|confirmed',
]);
}
接着往下看验证通过后,Laravel
会掉用AuthController
的create
方法来生成新用户,然后拿着新用户的数据去登录Auth::guard($this->getGuard())->login($this->create($request->all()));
所以我们要自定义用户注册时生成用户密码的加密方式只需要修改AuthController
的create
方法即可。
比如:
/**
* 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([
'nickname' => $data['name'],
'email' => $data['email'],
'password' => sha1($salt . $data['password']),
'register_time' => time(),
'register_ip' => ip2long(request()->ip()),
'salt' => $salt
]);
}
修改用户登录
修改登录前我们需要先通过路由规则看一下登录请求的具体控制器和方法,在上文提到的auth方法定义里可以看到
$this->get('login', 'Auth\AuthController@showLoginForm');
$this->post('login', 'Auth\AuthController@login');
$this->get('logout', 'Auth\AuthController@logout');
验证登录的操作是在\App\Http\Controllers\Auth\AuthController类的login方法里。打开AuthController发现Auth相关的方法都是通过性状(traits
)引入到类内的,在类内use
要引入的traits
,在编译时PHP就会把traits
里的代码copy到类中,这是PHP5.5引入的特性具体适用场景和用途这里不细讲。 所以AuthController@login
方法实际是定义在
\Illuminate\Foundation\Auth\AuthenticatesUsers这个traits
里的
/**
* Handle a login request to the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function login(Request $request)
{
$this->validateLogin($request);
$throttles = $this->isUsingThrottlesLoginsTrait();
if ($throttles && $lockedOut = $this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return $this->sendLockoutResponse($request);
}
$credentials = $this->getCredentials($request);
if (Auth::guard($this->getGuard())->attempt($credentials, $request->has('remember'))) {
return $this->handleUserWasAuthenticated($request, $throttles);
}
if ($throttles && ! $lockedOut) {
$this->incrementLoginAttempts($request);
}
return $this->sendFailedLoginResponse($request);
}
登录验证的主要操作是在Auth::guard($this->getGuard())->attempt($credentials, $request->has('remember'));
这个方法调用中来进行的,Auth::guard($this->getGuard())
获取到的是\Illuminate\Auth\SessionGuard (具体如何获取的看Auth这个Facade \Illuminate\Auth\AuthManager里的源码)
看一下SessionGuard里attempt 方法是如何实现的:
public function attempt(array $credentials = [], $remember = false, $login = true)
{
$this->fireAttemptEvent($credentials, $remember, $login);
$this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
if ($this->hasValidCredentials($user, $credentials)) {
if ($login) {
$this->login($user, $remember);
}
return true;
}
if ($login) {
$this->fireFailedEvent($user, $credentials);
}
return false;
}
/**
* Determine if the user matches the credentials.
*
* @param mixed $user
* @param array $credentials
* @return bool
*/
protected function hasValidCredentials($user, $credentials)
{
return ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
}
retrieveByCredentials
是用传递进来的字段从数据库中取出用户数据的,validateCredentials
是用来验证密码是否正确的实际过程。
这里需要注意的是$this->provider
这个provider
是一个实现了\Illuminate\Contracts\Auth\UserProvider类的provider
, 我们看到目录Illuminate\Auth下面有两个UserProvider的实现,分别为DatabaseUserProvider
和EloquentUserProvider
, 但是我们验证密码的时候是通过那个来验证的呢,看一下auth的配置文件
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class, //这个是driver用的Model
],
],
这里配置的是driver => eloquent
, 那么就是通过EloquentUserProvider
的retrieveByCredentials
来验证的, 这个EloquentUserProvider
是在SessionGuard
实例化时被注入进来的, (具体是怎么通过读取auth配置文件, 实例化相应的provider注入到SessionGuard里的请查阅\Illuminate\Auth\AuthManager 里createSessionDriver
方法的源代码)
接下来我们继续查看EloquentUserProvider
中retrieveByCredentials
和validateCredentials
方法的实现:
/**
* Retrieve a user by the given credentials.
*
* @param array $credentials
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
if (empty($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());
}
上面两个方法retrieveByCredentials
用除了密码以外的字段从数据库用户表里取出用户记录,比如用email查询出用户记录,然后validateCredentials
方法就是通过$this->haser->check
来将输入的密码和哈希的密码进行比较来验证密码是否正确。
好了, 看到这里就很明显了, 我们需要改成自己的密码验证就是自己实现一下validateCredentials
就可以了, 修改$this->hasher->check
为我们自己的密码验证规则就可以了。
首先我们修改$user->getAuthPassword()
把数据库中用户表的salt和password传递到validateCredentials
中
修改App\\User.php
添加如下代码
/**
* The table associated to this model
*/
protected $table = 'user’;//用户表名不是laravel约定的这里要指定一下
/**
* 禁用Laravel自动管理timestamp列
*/
public $timestamps = false;
/**
* 覆盖Laravel中默认的getAuthPassword方法, 返回用户的password和salt字段
* @return type
*/
public function getAuthPassword()
{
return ['password' => $this->attributes['password'], 'salt' => $this->attributes['salt']];
}
然后我们在建立一个自己的UserProvider接口的实现,放到自定义的目录中:
新建app/Foundation/Auth/AdminEloquentUserProvider.php
namespace App\Foundation\Auth;
use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Str;
class AdminEloquentUserProvider 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'];
}
}
最后我们修改auth配置文件让Laravel在做Auth验证时使用我们刚定义的Provider,
修改config/auth.php:
'providers' => [
'users' => [
'driver' => 'admin-eloquent',
'model' => App\User::class,
]
]
修改app/Provider/AuthServiceProvider.php
public function boot(GateContract $gate)
{
$this->registerPolicies($gate);
\Auth::provider('admin-eloquent', function ($app, $config) {
return New \App\Foundation\Auth\AdminEloquentUserProvider($app['hash'], $config['model']);
});
}
Auth::provider
方法是用来注册Provider构造器的,这个构造器是一个Closure,provider方法的具体代码实现在AuthManager
文件里
public function provider($name, Closure $callback)
{
$this->customProviderCreators[$name] = $callback;
return $this;
}
闭包返回了AdminEloquentUserProvider
对象供Laravel Auth使用,好了做完这些修改后Laravel的Auth在做用户登录验证的时候采用的就是自定义的salt + password的方式了。
修改重置密码
Laravel 的重置密码的工作流程是:
- 向需要重置密码的用户的邮箱发送一封带有重置密码链接的邮件,链接中会包含用户的email地址和token。
- 用户点击邮件中的链接在重置密码页面输入新的密码,Laravel通过验证email和token确认用户就是发起重置密码请求的用户后将新密码更新到用户在数据表的记录里。
第一步需要配置Laravel的email功能,此外还需要在数据库中创建一个新表password_resets
来存储用户的email和对应的token
CREATE TABLE `password_resets` (
`email` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`token` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`created_at` timestamp NOT NULL,
KEY `password_resets_email_index` (`email`),
KEY `password_resets_token_index` (`token`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
通过重置密码表单的提交地址可以看到,表单把新的密码用post提交给了/password/reset,我们先来看一下auth相关的路由,确定/password/reset对应的控制器方法。
$this->post('password/reset', 'Auth\PasswordController@reset’);
可以看到对应的控制器方法是\App\Http\Controllers\Auth\PasswordController类的reset
方法,这个方法实际是定义在\Illuminate\Foundation\Auth\ResetsPasswords 这个traits
里,PasswordController引入了这个traits
/**
* Reset the given user's password.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function reset(Request $request)
{
$this->validate(
$request,
$this->getResetValidationRules(),
$this->getResetValidationMessages(),
$this->getResetValidationCustomAttributes()
);
$credentials = $this->getResetCredentials($request);
$broker = $this->getBroker();
$response = Password::broker($broker)->reset($credentials, function ($user, $password) {
$this->resetPassword($user, $password);
});
switch ($response) {
case Password::PASSWORD_RESET:
return $this->getResetSuccessResponse($response);
default:
return $this->getResetFailureResponse($request, $response);
}
}
方法开头先通过validator对输入进行验证,接下来在程序里传递把新密码和一个闭包对象传递给Password::broker($broker)->reset();
方法,这个方法定义在\Illuminate\Auth\Passwords\PasswordBroker类里.
/**
* Reset the password for the given token.
*
* @param array $credentials
* @param \Closure $callback
* @return mixed
*/
public function reset(array $credentials, Closure $callback)
{
// If the responses from the validate method is not a user instance, we will
// assume that it is a redirect and simply return it from this method and
// the user is properly redirected having an error message on the post.
$user = $this->validateReset($credentials);
if (! $user instanceof CanResetPasswordContract) {
return $user;
}
$pass = $credentials['password'];
// Once we have called this callback, we will remove this token row from the
// table and return the response from this callback so the user gets sent
// to the destination given by the developers from the callback return.
call_user_func($callback, $user, $pass);
$this->tokens->delete($credentials['token']);
return static::PASSWORD_RESET;
}
在PasswordBroker的reset方法里,程序会先对用户提交的数据做再一次的认证,然后把密码和用户实例传递给传递进来的闭包,在闭包调用里完成了将新密码更新到用户表的操作, 在闭包里程序调用了的PasswrodController类的resetPassword方法
function ($user, $password) {
$this->resetPassword($user, $password);
});
PasswrodController类resetPassword方法的定义
protected function resetPassword($user, $password)
{
$user->forceFill([
'password' => bcrypt($password),
'remember_token' => Str::random(60),
])->save();
Auth::guard($this->getGuard())->login($user);
}
在这个方法里Laravel
用的是bcrypt
加密了密码, 那么要改成我们需要的salt + password的方式,我们在PasswordController类里重写resetPassword
方法覆盖掉traits
里的该方法就可以了。
/**
* 覆盖ResetsPasswords traits里的resetPassword方法,改为用sha1(salt + password)的加密方式
* Reset the given user's password.
*
* @param \Illuminate\Contracts\Auth\CanResetPassword $user
* @param string $password
* @return void
*/
protected function resetPassword($user, $password)
{
$salt = Str::random(6);
$user->forceFill([
'password' => sha1($salt . $password),
'salt' => $salt,
'remember_token' => Str::random(60),
])->save();
\Auth::guard($this->getGuard())->login($user);
}
结语
到这里对Laravel
Auth
的自定义就完成了,注册、登录和重置密码都改成了sha1(salt + password)的密码加密方式, 所有自定义代码都是通过定义Laravel
相关类的子类和重写方法来完成没有修改Laravel
的源码,这样既保持了良好的可扩展性也保证了项目能够自由迁移。
注:使用的Laravel版本为5.2