>백엔드 개발 >PHP 튜토리얼 >Laravel自带Auth 密码重置源码解析及扩展实现手机号密码找回

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

WBOY
WBOY원래의
2016-06-20 12:30:541129검색

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

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

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

首先来看一下PasswordController 中的 ResetsPasswords trait

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);    }    //下面的代码略}

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

Method URI Action

POST

password/email

App\Http\Controllers\Auth\[email protected]

POST

password/reset

App\Http\Controllers\Auth\[email protected]

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

<?phpnamespace 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的register

Illuminate/Auth/Passwords/PasswordResetServiceProvider.php

<?phpnamespace 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

<?phpnamespace 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其实才是密码重置的核心实现,里面主要做了实现一下几件事情:

  • 创建邮件验证的token,并发送重置密码邮件
  • 用户点击重置连接以后根据token进行验证
  • 重置旧的密码成用户提交的新密码

Illuminate/Auth/Passwords/PasswordBroker.php

<?phpnamespace 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\[email protected]
'); //通过邮件重置密码 Route::post('password/reset-mail', 'Auth\[email protected]'); //发送手机短信验证码 Route::post('password/phone', 'Auth\[email protected]'); //通过手机验证码找回密码 Route::post('password/reset-phone', 'Auth\ [email protected]

');

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

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

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

app/Providers/RyanPasswordResetServiceProvider.php

<?phpnamespace 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

<?phpnamespace 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

<?phpnamespace 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

<?phpnamespace 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

<?phpnamespace 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, Password, 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 Password::RESET_LINK_SENT:                return ['status_code' => '200', 'message' => '密码重置邮件已发送'];            case Password::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 = Password::broker($broker)->reset($credentials, function ($user, $password) {            $this->resetPassword($user, $password);        });        switch ($response) {            case Password::PASSWORD_RESET:                unset($credentials['token']);                unset($credentials['password_confirmation']);                return [                    'status_code' => '200',                    'message' => '密码重置成功'                ];            case Password::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 Password::RESET_LINK_SENT:                return ['status_code' => '200', 'message' => '密码重置验证码已发送'];            case Password::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 = Password::broker($broker)->resetByPhone($credentials, function ($user, $password) {            $this->resetPassword($user, $password);        });        switch ($response) {            case Password::PASSWORD_RESET:                unset($credentials['verify_code']);                unset($credentials['password_confirmation']);                return [                    'status_code' => '200',                    'message' => '密码重置成功',                ];            case Password::INVALID_TOKEN:                //返回'手机验证码已失效'            default:                //返回'密码重置失败'        }    }}

结束!!

转载请注明:转载自 Ryan是菜鸟 | LNMP技术栈笔记

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

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.