资讯专栏INFORMATION COLUMN

Laravel自带Auth 密码重置源码解析及扩展实现手机号密码找回

elliott_hu / 794人阅读

摘要:到这里自带的密码重置的源码解读部分就完成了,下面我们就通过扩展一下实现手机号密码找回和自定义邮件发送方式找回密码,根据上面的代码解析如果你看懂的话应该了解,其实我们只要扩展和就可以了。

本文最早于发表本人博客: Laravel 自带 Auth 密码重置源码解析及扩展实现手机号密码找回

Larval 自带 Auth 密码重置源码解析及扩展实现手机号密码找回

Larval技术群小伙伴问密码重置时PasswordController中需要设置的$broker是干嘛用的,正好来写一下Laravel 中Auth的ResetsPasswords,以及实践一下扩展,所以大体这篇博客写写:

密码重置源码分析

实现自定义邮件发送方式进行密码重置,比如使用第三方或者自己发送邮件方式找回

实现手机号密码重置

首先来看一下PasswordController 中的 ResetsPasswords trait

trait ResetsPasswords
{
    use RedirectsUsers;

    public function getEmail()
    {
        return $this->showLinkRequestForm();
    }

    /**
     * 这里就是设置密码重置邮件内容的
     *
     * @return IlluminateHttpResponse
     */
    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  IlluminateHttpRequest  $request
     * @return IlluminateHttpResponse
     */
    public function postEmail(Request $request)
    {
        return $this->sendResetLinkEmail($request);
    }

    /**
     * 给重置密码的用户发送邮件
     *
     * @param  IlluminateHttpRequest  $request
     * @return IlluminateHttpResponse
     */
    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 SymfonyComponentHttpFoundationResponse
     */
    protected function getSendResetLinkEmailSuccessResponse($response)
    {
        return redirect()->back()->with("status", trans($response));
    }

    /**
     * 邮件发送时候返回
     *
     * @param  string  $response
     * @return SymfonyComponentHttpFoundationResponse
     */
    protected function getSendResetLinkEmailFailureResponse($response)
    {
        return redirect()->back()->withErrors(["email" => trans($response)]);
    }

    /**
     * 用户点击邮箱里面重置连接后跳转的页面,就是重置密码页面
     * @param  IlluminateHttpRequest  $request
     * @param  string|null  $token
     * @return IlluminateHttpResponse
     */
    public function getReset(Request $request, $token = null)
    {
        return $this->showResetForm($request, $token);
    }

    /**
     * 用户点击邮箱里面重置连接后跳转的页面,就是重置密码页面
     *
     * @param  IlluminateHttpRequest  $request
     * @param  string|null  $token
     * @return IlluminateHttpResponse
     */
    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  IlluminateHttpRequest  $request
     * @return IlluminateHttpResponse
     */
    public function postReset(Request $request)
    {
        return $this->reset($request);
    }

    /**
     * 重置密码实现
     *
     * @param  IlluminateHttpRequest  $request
     * @return IlluminateHttpResponse
     */
    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  IlluminateContractsAuthCanResetPassword  $user
     * @param  string  $password
     * @return void
     */
    protected function resetPassword($user, $password)
    {
        $user->password = bcrypt($password);

        $user->save();

        Auth::guard($this->getGuard())->login($user);
    }

    //下面的代码略
}

上面其实就是路由的实现方法,主要路由如下:

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.php


可以看到上面邮件发送后等状态的判断也是在这个Facade中定义的,那么auth.password 的这个是绑定到哪个类中实现的?继续查看对应的ServiceProvider的register

Illuminate/Auth/Passwords/PasswordResetServiceProvider.php

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

getDefaultDriver();

        return isset($this->brokers[$name])
                    ? $this->brokers[$name]
                    : $this->brokers[$name] = $this->resolve($name);
    }

    /**
     * Resolve the given broker.
     *
     * @param  string  $name
     * @return IlluminateContractsAuthPasswordBroker
     *
     * @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 IlluminateAuthPasswordsTokenRepositoryInterface
     */
    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其实才是密码重置的核心实现,里面主要做了实现一下几件事情:

创建邮件验证的token,并发送重置密码邮件

用户点击重置连接以后根据token进行验证

重置旧的密码成用户提交的新密码

Illuminate/Auth/Passwords/PasswordBroker.php

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  IlluminateContractsAuthCanResetPassword  $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.phpPasswordBrokerManager.php就可以了。

自定义邮件发送和手机号发送验证码逻辑类请自行实现,以下代码的EmailService和SmsService分别表示发送邮件和发送短信的类,自己按照需求进行封装,比如SendCloud发送邮件,云通讯发送手机短信验证码的具体实现

自定义邮件重置密码的逻辑基本都一样的,不变,手机号重置密码的过程应该是这样的:

用户填入手机号,点击“发送验证码”按钮,收到验证码

将验证码填入,点击“密码找回”

后台进行验证码校验,没有问题跳转到新密码设置页面

新密码设置

路由如下:

Route::post("password/email", "AuthPasswordController@sendResetLinkEmail");
//通过邮件重置密码
Route::post("password/reset-mail", "AuthPasswordController@resetBymail");
//发送手机短信验证码
Route::post("password/phone", "AuthPasswordController@sendResetCodePhone");
//通过手机验证码找回密码
Route::post("password/reset-phone", "AuthPasswordController@resetByPhone");

在app目录下建立入如下目录和文件(根据个人习惯):

Foundation/
├── Auth
   ├── Passwords
      ├── RyanPasswordBroker.php
      ├── RyanPasswordBrokerManager.php
      └── Facade
          └── RyanPassword.php

新建ServiceProvider,将auth.password绑定到我们自己的RyanPasswordBroker

app/Providers/RyanPasswordResetServiceProvider.php

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" => [
        ......
        AppProvidersRyanPasswordResetServiceProvider::class,
    ],

    "aliases" => [
        ......
        "RyanPassword" => AppFoundationAuthPasswordsFacadeRyanPassword::class,
    ],

app/Foundation/Auth/Passwords/Facade/RyanPassword.php


app/Foundation/Auth/Passwords/RyanPasswordBroker.php

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 IlluminateContractsAuthCanResetPassword
     *
     * @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

mailer = new EmailService();
    }

    /**
     * Attempt to get the broker from the local cache.
     *
     * @param  string $name
     * @return IlluminateContractsAuthPasswordBroker
     */
    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 IlluminateContractsAuthPasswordBroker
     *
     * @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

middleware("guest");
    }


    /**
     * 发送重置密码邮件
     *
     * @param  IlluminateHttpRequest $request
     * @return IlluminateHttpResponse
     */
    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  IlluminateHttpRequest $request
     * @return IlluminateHttpResponse
     */
    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  IlluminateHttpRequest $request
     * @return IlluminateHttpResponse
     */
    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技术栈笔记

如果觉得本篇文章对您十分有益,何不 打赏一下

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/30325.html

相关文章

  • Laravel核心解读 -- 用户认证系统(基础介绍)

    摘要:系统的核心是由的认证组件的看守器和提供器组成。使用的认证系统,几乎所有东西都已经为你配置好了。其配置文件位于,其中包含了用于调整认证服务行为的注释清晰的选项配置。 用户认证系统(基础介绍) 使用过Laravel的开发者都知道,Laravel自带了一个认证系统来提供基本的用户注册、登录、认证、找回密码,如果Auth系统里提供的基础功能不满足需求还可以很方便的在这些基础功能上进行扩展。这篇...

    RebeccaZhong 评论0 收藏0
  • GitChat · 安全 | 聊聊 「密码找回

    摘要:微信任意用户密码修改漏洞漏洞描述在微信官方的首页上发现了找回密码功能。选择通过手机号码找回密码。提交成功,输入新密码。网易邮箱可直接修改其他用户密码描述这次我们看一个邮箱的找回密码漏洞,这个还真和上面的方式有点不一样。 来自 GitChat 作者:汤青松更多使用技术,尽在微信公众号:GitChat技术杂谈 进入 GitChat 阅读原文 WEB安全用户密码找回多案例安全攻防实战 这次文...

    YorkChen 评论0 收藏0
  • Laravel核心解读 -- 扩展用户认证系统

    摘要:扩展用户认证系统上一节我们介绍了系统实现的一些细节知道了是如何应用看守器和用户提供器来进行用户认证的,但是针对我们自己开发的项目或多或少地我们都会需要在自带的看守器和用户提供器基础之上做一些定制化来适应项目,本节我会列举一个在做项目时遇到的 扩展用户认证系统 上一节我们介绍了Laravel Auth系统实现的一些细节知道了Laravel是如何应用看守器和用户提供器来进行用户认证的,但是...

    王伟廷 评论0 收藏0
  • 20151103addinfo-laravel小练习-小结

    摘要:学习了一段时间的小结一下最近做的小任务写下来才知道好乱糟糟,还是以记录学习的资料为主,写的很糟糕,还需要再揣度多屡屡思路。 学习了一段时间的laravel,小结一下最近做的laravel小任务,写下来才知道好乱糟糟,还是以记录学习的资料为主,写的很糟糕,还需要再揣度多屡屡思路。20151103-16 源码地址:https://github.com/dingyiming/xc-add...

    jackzou 评论0 收藏0
  • laravel auth 认证

    摘要:如果两个经哈希运算的密码相匹配那么将会为这个用户开启一个认证。如果认证成功的话方法将会返回。重定向器上的方法将用户重定向到登录之前用户想要访问的,在目标无效的情况下回退将会传递给该方法。最后如有错误,欢迎指出交流群 Auth认证 路由 从路由开始,找到源码,再进行研究找到根目录下面的 vendor/laravel/framework/src/Illuminate/Routing/Rou...

    Lionad-Morotar 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<