本文最先於發表本人博客: Laravel 自帶 Auth 密碼重置源碼解析及擴展實現手機號密碼找回php
Larval 自帶 Auth 密碼重置源碼解析及擴展實現手機號密碼找回laravel
Larval技術羣小夥伴問密碼重置時PasswordController中須要設置的$broker
是幹嗎用的,正好來寫一下Laravel 中Auth的ResetsPasswords
,以及實踐一下擴展,因此大致這篇博客寫寫:web
首先來看一下PasswordController 中的 ResetsPasswords traitredis
trait ResetsPasswords { use RedirectsUsers; public function getEmail() { return $this->showLinkRequestForm(); } /** * 這裏就是設置密碼重置郵件內容的 * * @return \Illuminate\Http\Response */ public function showLinkRequestForm() { //因此咱們能夠在PoasswrodController 中設置 protected $linkRequestView 來定義密碼重置郵件模板 if (property_exists($this, 'linkRequestView')) { return view($this->linkRequestView); } if (view()->exists('auth.passwords.email')) { return view('auth.passwords.email'); } return view('auth.password'); } /** * 發送密碼重置郵件 * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function postEmail(Request $request) { return $this->sendResetLinkEmail($request); } /** * 給重置密碼的用戶發送郵件 * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function sendResetLinkEmail(Request $request) { $this->validate($request, ['email' => 'required|email']); $broker = $this->getBroker(); //獲取broker,下面會講 $response = Password::broker($broker)->sendResetLink($request->only('email'), function (Message $message) { $message->subject($this->getEmailSubject()); }); //根據 broker 來發送密碼重置郵件,下面會詳細講 switch ($response) { case Password::RESET_LINK_SENT: //狀態,下面會講 return $this->getSendResetLinkEmailSuccessResponse($response); case Password::INVALID_USER: default: return $this->getSendResetLinkEmailFailureResponse($response); } } /** * 郵件標題 * * @return string */ protected function getEmailSubject() { return property_exists($this, 'subject') ? $this->subject : 'Your Password Reset Link'; } /** * 郵件成功發送過之後返回 * * @param string $response * @return \Symfony\Component\HttpFoundation\Response */ protected function getSendResetLinkEmailSuccessResponse($response) { return redirect()->back()->with('status', trans($response)); } /** * 郵件發送時候返回 * * @param string $response * @return \Symfony\Component\HttpFoundation\Response */ protected function getSendResetLinkEmailFailureResponse($response) { return redirect()->back()->withErrors(['email' => trans($response)]); } /** * 用戶點擊郵箱裏面重置鏈接後跳轉的頁面,就是重置密碼頁面 * @param \Illuminate\Http\Request $request * @param string|null $token * @return \Illuminate\Http\Response */ public function getReset(Request $request, $token = null) { return $this->showResetForm($request, $token); } /** * 用戶點擊郵箱裏面重置鏈接後跳轉的頁面,就是重置密碼頁面 * * @param \Illuminate\Http\Request $request * @param string|null $token * @return \Illuminate\Http\Response */ public function showResetForm(Request $request, $token = null) { if (is_null($token)) { return $this->getEmail(); } $email = $request->input('email'); //因此咱們能夠在PoasswrodController 中設置 protected $resetView 來定義密碼重置的頁面 if (property_exists($this, 'resetView')) { return view($this->resetView)->with(compact('token', 'email')); } if (view()->exists('auth.passwords.reset')) { return view('auth.passwords.reset')->with(compact('token', 'email')); } return view('auth.reset')->with(compact('token', 'email')); } /** * 重置密碼 * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function postReset(Request $request) { return $this->reset($request); } /** * 重置密碼實現 * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function reset(Request $request) { $this->validate($request, [ 'token' => 'required', 'email' => 'required|email', 'password' => 'required|confirmed|min:6', ]); $credentials = $request->only( 'email', 'password', 'password_confirmation', 'token' ); $broker = $this->getBroker(); $response = Password::broker($broker)->reset($credentials, function ($user, $password) { //注意這個回調 $this->resetPassword($user, $password); }); //根據 broker重置密碼,下面會詳細講 switch ($response) { case Password::PASSWORD_RESET: return $this->getResetSuccessResponse($response); default: return $this->getResetFailureResponse($request, $response); } } /** * 重置密碼,而且從新登錄 * * @param \Illuminate\Contracts\Auth\CanResetPassword $user * @param string $password * @return void */ protected function resetPassword($user, $password) { $user->password = bcrypt($password); $user->save(); Auth::guard($this->getGuard())->login($user); } //下面的代碼略 }
上面其實就是路由的實現方法,主要路由以下:json
Method | URI | Action |
---|---|---|
POST | password/email | AppHttpControllersAuthPasswordController@sendResetLinkEmail |
POST | password/reset | AppHttpControllersAuthPasswordController@reset |
GET | HEAD | password/reset/{token?} |
首先來主要看下sendResetLinkEmail
方法,這個方法主要實現根據用戶填入的郵箱地址來發送重置郵件的緩存
$response = Password::broker($broker)->sendResetLink($request->only('email'), function (Message $message) { $message->subject($this->getEmailSubject()); }); //根據 broker 來發送密碼重置郵件,下面會詳細講 switch ($response) { case Password::RESET_LINK_SENT: //狀態,下面會講 return $this->getSendResetLinkEmailSuccessResponse($response); case Password::INVALID_USER: default: return $this->getSendResetLinkEmailFailureResponse($response); }
上面的Password
就是Facade,咱們看一下這個Facade:併發
Illuminate/Support/Facades/Password.phpapp
<?php namespace Illuminate\Support\Facades; /** * @see \Illuminate\Auth\Passwords\PasswordBroker */ class Password extends Facade { /** * Constant representing a successfully sent reminder. * * @var string */ const RESET_LINK_SENT = 'passwords.sent'; /** * Constant representing a successfully reset password. * * @var string */ const PASSWORD_RESET = 'passwords.reset'; /** * Constant representing the user not found response. * * @var string */ const INVALID_USER = 'passwords.user'; /** * Constant representing an invalid password. * * @var string */ const INVALID_PASSWORD = 'passwords.password'; /** * Constant representing an invalid token. * * @var string */ const INVALID_TOKEN = 'passwords.token'; protected static function getFacadeAccessor() { return 'auth.password'; } }
能夠看到上面郵件發送後等狀態的判斷也是在這個Facade中定義的,那麼auth.password
的這個是綁定到哪一個類中實現的?繼續查看對應的ServiceProvider的registerdom
Illuminate/Auth/Passwords/PasswordResetServiceProvider.phpide
<?php namespace Illuminate\Auth\Passwords; use Illuminate\Support\ServiceProvider; class PasswordResetServiceProvider extends ServiceProvider { protected $defer = true; public function register() { $this->registerPasswordBroker(); } protected function registerPasswordBroker() { $this->app->singleton('auth.password', function ($app) { return new PasswordBrokerManager($app); }); $this->app->bind('auth.password.broker', function ($app) { return $app->make('auth.password')->broker(); }); } public function provides() { return ['auth.password', 'auth.password.broker']; } }
看到了PasswordBrokerManager($app);
,那麼咱們就知道了上面Passwrod::broker
的實如今PasswordBrokerManager
中,那咱們先來看下是如何發送這個重置密碼郵件的
Illuminate/Auth/Passwords/PasswordBrokerManager.php
<?php namespace Illuminate\Auth\Passwords; use InvalidArgumentException; use Illuminate\Contracts\Auth\PasswordBrokerFactory as FactoryContract; class PasswordBrokerManager implements FactoryContract { // 略 /** * 獲取broker * * @param string $name * @return \Illuminate\Contracts\Auth\PasswordBroker */ public function broker($name = null) { $name = $name ?: $this->getDefaultDriver(); return isset($this->brokers[$name]) ? $this->brokers[$name] : $this->brokers[$name] = $this->resolve($name); } /** * Resolve the given broker. * * @param string $name * @return \Illuminate\Contracts\Auth\PasswordBroker * * @throws \InvalidArgumentException */ protected function resolve($name) { $config = $this->getConfig($name); //獲取auth.php配置中的passwords broker if (is_null($config)) { throw new InvalidArgumentException("Password resetter [{$name}] is not defined."); } //這裏很重要,就是實例一個PasswordBroker return new PasswordBroker( $this->createTokenRepository($config), $this->app['auth']->createUserProvider($config['provider']), $this->app['mailer'], $config['email'] ); } /** * 根據配置建立一個token實例 * * @param array $config * @return \Illuminate\Auth\Passwords\TokenRepositoryInterface */ protected function createTokenRepository(array $config) { return new DatabaseTokenRepository( $this->app['db']->connection(), $config['table'], $this->app['config']['app.key'], $config['expire'] ); } //下面略 public function __call($method, $parameters) { return call_user_func_array([$this->broker(), $method], $parameters); } }
上面的resolve
返回了new PasswordBroker
,這裏的PasswordBroker
其實才是密碼重置的核心實現,裏面主要作了實現一下幾件事情:
Illuminate/Auth/Passwords/PasswordBroker.php
<?php namespace Illuminate\Auth\Passwords; use Closure; use Illuminate\Support\Arr; use UnexpectedValueException; use Illuminate\Contracts\Auth\UserProvider; use Illuminate\Contracts\Mail\Mailer as MailerContract; use Illuminate\Contracts\Auth\PasswordBroker as PasswordBrokerContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; class PasswordBroker implements PasswordBrokerContract { //上面的略 public function __construct(TokenRepositoryInterface $tokens, UserProvider $users, MailerContract $mailer, $emailView) { $this->users = $users; $this->mailer = $mailer; $this->tokens = $tokens; $this->emailView = $emailView; } /** * 給用戶發送包含重置連接的郵件 * * @param array $credentials * @param \Closure|null $callback * @return string */ public function sendResetLink(array $credentials, Closure $callback = null) { // 驗證用戶 $user = $this->getUser($credentials); if (is_null($user)) { return PasswordBrokerContract::INVALID_USER; } // 生成token $token = $this->tokens->create($user); //發送郵件 $this->emailResetLink($user, $token, $callback); return PasswordBrokerContract::RESET_LINK_SENT; } /** * 發送郵件的實現 * * @param \Illuminate\Contracts\Auth\CanResetPassword $user * @param string $token * @param \Closure|null $callback * @return int */ public function emailResetLink(CanResetPasswordContract $user, $token, Closure $callback = null) { //把token和user變量傳遞到郵件模板中,併發送郵件 $view = $this->emailView; return $this->mailer->send($view, compact('token', 'user'), function ($m) use ($user, $token, $callback) { $m->to($user->getEmailForPasswordReset()); if (! is_null($callback)) { call_user_func($callback, $m, $user, $token); } }); } /** * 根據token重置密碼 * * @param array $credentials * @param \Closure $callback * @return mixed */ public function reset(array $credentials, Closure $callback) { //實現根據$credentials來驗證用戶是否能夠更改更改密碼 $user = $this->validateReset($credentials); if (! $user instanceof CanResetPasswordContract) { return $user; } $pass = $credentials['password']; // 下面這個就是產生新的密碼的實現 call_user_func($callback, $user, $pass); $this->tokens->delete($credentials['token']); return PasswordBrokerContract::PASSWORD_RESET; } //下面的是一些驗證的方法,略 }
上面的reset
中的call_user_func
就是調用了重置新密碼的邏輯,$callback
其實就是最上面的trait ResetsPasswords
中的resetPassword($user, $password)
來保存新密碼。
到這裏Laravel 自帶Auth的密碼重置的源碼解讀部分就完成了,下面咱們就經過擴展一下實現手機號密碼找回和自定義郵件發送方式找回密碼,根據上面的代碼解析若是你看懂的話應該瞭解,其實咱們只要擴展PasswordBroker.php
和PasswordBrokerManager.php
就能夠了。
自定義郵件發送和手機號發送驗證碼邏輯類請自行實現,如下代碼的EmailService和SmsService分別表示發送郵件和發送短信的類,本身按照需求進行封裝,好比SendCloud發送郵件,雲通信發送手機短信驗證碼的具體實現
自定義郵件重置密碼的邏輯基本都同樣的,不變,手機號重置密碼的過程應該是這樣的:
路由以下:
Route::post('password/email', 'Auth\PasswordController@sendResetLinkEmail'); //經過郵件重置密碼 Route::post('password/reset-mail', 'Auth\PasswordController@resetBymail'); //發送手機短信驗證碼 Route::post('password/phone', 'Auth\PasswordController@sendResetCodePhone'); //經過手機驗證碼找回密碼 Route::post('password/reset-phone', 'Auth\PasswordController@resetByPhone');
在app目錄下創建入以下目錄和文件(根據我的習慣):
Foundation/ ├── Auth ├── Passwords ├── RyanPasswordBroker.php ├── RyanPasswordBrokerManager.php └── Facade └── RyanPassword.php
新建ServiceProvider,將auth.password綁定到咱們本身的RyanPasswordBroker
app/Providers/RyanPasswordResetServiceProvider.php
<?php namespace App\Providers; use App\Foundation\Auth\Passwords\RyanPasswordBrokerManager; use Illuminate\Support\ServiceProvider; class RyanPasswordResetServiceProvider extends ServiceProvider { protected $defer = true; public function register() { $this->registerPasswordBroker(); } protected function registerPasswordBroker() { $this->app->singleton('auth.password', function ($app) { return new RyanPasswordBrokerManager($app); }); $this->app->bind('auth.password.broker', function ($app) { return $app->make('auth.password')->broker(); }); } public function provides() { return ['auth.password', 'auth.password.broker']; } }
修改config/app.php
'providers' => [ ...... App\Providers\RyanPasswordResetServiceProvider::class, ], 'aliases' => [ ...... 'RyanPassword' => App\Foundation\Auth\Passwords\Facade\RyanPassword::class, ],
app/Foundation/Auth/Passwords/Facade/RyanPassword.php
<?php namespace App\Foundation\Auth\Passwords\Facade; use Illuminate\Support\Facades\Password; /** * @see \Illuminate\Auth\Passwords\PasswordBroker */ class RyanPassword extends Password { }
app/Foundation/Auth/Passwords/RyanPasswordBroker.php
<?php namespace App\Foundation\Auth\Passwords; use App\Services\SmsService; use Closure; use Illuminate\Auth\Passwords\PasswordBroker; use Illuminate\Support\Arr; use UnexpectedValueException; use Illuminate\Contracts\Auth\UserProvider; use Illuminate\Contracts\Mail\Mailer as MailerContract; use Illuminate\Contracts\Auth\PasswordBroker as PasswordBrokerContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use Illuminate\Auth\Passwords\TokenRepositoryInterface; use App\Services\EmailService; use Illuminate\Contracts\View\Factory as ViewFactory; use Illuminate\Support\Facades\Redis; class RyanPasswordBroker extends PasswordBroker { //這裏注意下,EmailService是自定義發送郵件的方式,本身實現 public function __construct(TokenRepositoryInterface $tokens, UserProvider $users, EmailService $mailer, $emailView) { $this->users = $users; $this->mailer = $mailer; $this->tokens = $tokens; $this->emailView = $emailView; } public function emailResetLink(CanResetPasswordContract $user, $token, Closure $callback = null) { $body = app('view')->make($this->emailView, compact('token', 'user'))->render(); return $this->mailer->send($user->getEmailForPasswordReset(), 'xxx帳號密碼重置', $body, $fromName = 'xxxx'); } protected function validateReset(array $credentials) { if (is_null($user = $this->getUser($credentials))) { return PasswordBrokerContract::INVALID_USER; } if (!$this->validateNewPassword($credentials)) { return PasswordBrokerContract::INVALID_PASSWORD; } if (isset($credentials['verify_code'])) { //若是提交的字段含有verify_code表示是手機驗證碼方式重置密碼,須要驗證用戶提交的驗證碼是否是剛纔發送給他手機號的,驗證碼發送之後能夠保持在緩存中 if (Redis::get('password:telephone:' . $credentials['telephone']) != $credentials['verify_code']) { return PasswordBrokerContract::INVALID_TOKEN; } } elseif (!$this->tokens->exists($user, $credentials['token'])) { //郵件重置方式 return PasswordBrokerContract::INVALID_TOKEN; } return $user; } /** * Get the user for the given credentials. * * @param array $credentials * @return \Illuminate\Contracts\Auth\CanResetPassword * * @throws \UnexpectedValueException */ public function getUser(array $credentials) { $credentials = Arr::except($credentials, ['token', 'verify_code']);//這裏注意,若是是手機驗證碼方式找回密碼須要吧verify_code字段排除,以避免users表中沒有verify_code字段查不到用戶 $user = $this->users->retrieveByCredentials($credentials); if ($user && !$user instanceof CanResetPasswordContract) { throw new UnexpectedValueException('User must implement CanResetPassword interface.'); } return $user; } /** * 發送重置密碼手機驗證碼 * * @param array $credentials * @param \Closure|null $callback * @return string */ public function sendResetCode(array $credentials, Closure $callback = null) { $user = $this->getUser($credentials); if (is_null($user)) { return PasswordBrokerContract::INVALID_USER; } //我是將手機驗證碼發送後保持在Redis中,驗證的時候也是去redis取 $telephone = $credentials['telephone']; $code = random_int(100000, 999999); $result = with(new SmsService())->sendTemplateSms($telephone, config('sms.template_ids.password_verify_code'), [$code]); $result = json_decode($result, true); if ($result['status']) { Redis::setEx('password:telephone:' . $telephone, 3000, $code); return PasswordBrokerContract::RESET_LINK_SENT; } } /** * 經過手機驗證碼重置密碼 * @param array $credentials * @param Closure $callback * @return CanResetPasswordContract|string */ public function resetByPhone(array $credentials, Closure $callback) { $user = $this->validateReset($credentials); if (!$user instanceof CanResetPasswordContract) { return $user; } $pass = $credentials['password']; call_user_func($callback, $user, $pass); //若是是手機號重置密碼的話新密碼保存後須要刪除緩存的驗證碼 Redis::del('password:telephone:' . $credentials['telephone']); return PasswordBrokerContract::PASSWORD_RESET; } }
app/Foundation/Auth/Passwords/RyanPasswordBrokerManager.php
<?php namespace App\Foundation\Auth\Passwords; use App\Services\EmailService; use InvalidArgumentException; use Illuminate\Contracts\Auth\PasswordBrokerFactory as FactoryContract; use Illuminate\Auth\Passwords\PasswordBrokerManager as PasswordBrokerManager; use Illuminate\Auth\Passwords\DatabaseTokenRepository; class RyanPasswordBrokerManager extends PasswordBrokerManager { /** * The application instance. * * @var \Illuminate\Foundation\Application */ protected $app; /** * The array of created "drivers". * * @var array */ protected $brokers = []; protected $mailer; /** * Create a new PasswordBroker manager instance. * * @param \Illuminate\Foundation\Application $app * @return void */ public function __construct($app) { parent::__construct($app); $this->mailer = new EmailService(); } /** * Attempt to get the broker from the local cache. * * @param string $name * @return \Illuminate\Contracts\Auth\PasswordBroker */ public function broker($name = null) { $name = $name ?: $this->getDefaultDriver(); return isset($this->brokers[$name]) ? $this->brokers[$name] : $this->brokers[$name] = $this->resolve($name); } /** * Resolve the given broker. * * @param string $name * @return \Illuminate\Contracts\Auth\PasswordBroker * * @throws \InvalidArgumentException */ protected function resolve($name) { $config = $this->getConfig($name); if(is_null($config)) { throw new InvalidArgumentException("Password resetter [{$name}] is not defined."); } //這裏實例化咱們自定義的RyanPasswordBroker來完成密碼重置邏輯 return new RyanPasswordBroker($this->createTokenRepository($config), $this->app['auth']->createUserProvider($config['provider']), $this->mailer, $config['email']); } }
修改PasswordController.php
<?php namespace App\Controllers\Auth; use App\Controllers\Controller; use App\Requests\Auth\EmailResetPasswordRequest; use App\Requests\Auth\EmailResetPasswordSendRequest; use App\Requests\Auth\PhoneResetPasswordRequest; use App\Requests\Auth\PhoneResetPasswordSendRequest; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; use Illuminate\Foundation\Auth\ResetsPasswords; use RyanPassword, Auth; class PasswordController extends Controller { use ResetsPasswords; protected $broker = 'users'; public function __construct() { $this->middleware('guest'); } /** * 發送重置密碼郵件 * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function sendResetLinkEmail(EmailResetPasswordSendRequest $request) { $broker = $this->getBroker(); $response = RyanPassword::broker($broker)->sendResetLink($request->only('email')); switch ($response) { case RyanPassword::RESET_LINK_SENT: return ['status_code' => '200', 'message' => '密碼重置郵件已發送']; case RyanPassword::INVALID_USER: default: throw new \UnauthorizedHttpException(401, '該郵箱未註冊'); } } /** * 經過郵件重置密碼 * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function resetBymail(EmailResetPasswordRequest $request) { $credentials = $request->only('email', 'password', 'password_confirmation', 'token'); $broker = $this->getBroker(); $response = RyanPassword::broker($broker)->reset($credentials, function ($user, $password) { $this->resetPassword($user, $password); }); switch ($response) { case RyanPassword::PASSWORD_RESET: unset($credentials['token']); unset($credentials['password_confirmation']); return [ 'status_code' => '200', 'message' => '密碼重置成功' ]; case RyanPassword::INVALID_TOKEN: //返回'Token 已經失效' default: //返回'密碼重置失敗' } } /** * 發送重置密碼短信驗證碼 * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function sendResetCodePhone(PhoneResetPasswordSendRequest $request) { $broker = $this->getBroker(); $response = RyanPassword::broker($broker)->sendResetCode($request->only('telephone')); switch ($response) { case RyanPassword::RESET_LINK_SENT: return ['status_code' => '200', 'message' => '密碼重置驗證碼已發送']; case RyanPassword::INVALID_USER: default: //返回'該手機號未註冊' } } /** * 經過短信驗證碼重置密碼 * @param PhoneResetPasswordRequest $request * @return array */ public function resetByPhone(PhoneResetPasswordRequest $request) { $credentials = $request->only('telephone', 'password', 'password_confirmation', 'verify_code'); $broker = $this->getBroker(); $response = RyanPassword::broker($broker)->resetByPhone($credentials, function ($user, $password) { $this->resetPassword($user, $password); }); switch ($response) { case RyanPassword::PASSWORD_RESET: unset($credentials['verify_code']); unset($credentials['password_confirmation']); return [ 'status_code' => '200', 'message' => '密碼重置成功', ]; case RyanPassword::INVALID_TOKEN: //返回'手機驗證碼已失效' default: //返回'密碼重置失敗' } } }
結束!!
轉載請註明: 轉載自Ryan是菜鳥 | LNMP技術棧筆記
若是以爲本篇文章對您十分有益,何不 打賞一下