オブジェクト指向とかデザインパターンとか開発プロセスとかツールとか

satoshi's ソフトウェア開発

js






当サイトはアフィリエイト広告を利用してます。

Laravel

Laravelでメールアドレスを暗号化したときにパスワード再設定用リンクを送信できるようにする

更新日:


以前、LaravelからDBに格納するデータを暗号化・復号する方法とメールアドレスのuniqueバリデーションの問題というのを書きました。

これで大丈夫かと思ったのですが、メールアドレスを暗号化したために、別のところで影響がありました。

パスワードのリセットです。

メールアドレスを入力して「パスワード再設定用リンクを送信する」ボタンを押すと、「メールアドレスに対応するユーザーが見つかりません。」とエラーになってしまいます。

メールアドレスを暗号化して保存しているために、標準の機能が動かなくなっていました。

resources/views/auth/passwords/email.blade.php を見ると、「パスワード再設定用リンクを送信する」を押すと、password/email にPOSTしてます。

ルーティングを確認します。

$ php artisan route:list | grep password/email
|        | POST      | password/email                | password.email      | App\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail  | web                       |

ForgotPasswordControllerを見てみます。

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;

class ForgotPasswordController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Password Reset Controller
    |--------------------------------------------------------------------------
    |
    | This controller is responsible for handling password reset emails and
    | includes a trait which assists in sending these notifications from
    | your application to your users. Feel free to explore this trait.
    |
    */

    use SendsPasswordResetEmails;
}

本体は vendor/laravel/ui/auth-backend/SendPasswordResetEmails でした。

メールアドレスが見つからないために、$this->sendResetLinkFailedResponse() を return しているようです。

Password::broker() に対して問い合わせをして、失敗しています。

    /**
     * Send a reset link to the given user.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
     */
    public function sendResetLinkEmail(Request $request)
    {
        $this->validateEmail($request);

        // We will send the password reset link to this user. Once we have attempted
        // to send the link, we will examine the response then see the message we
        // need to show to the user. Finally, we'll send out a proper response.
        $response = $this->broker()->sendResetLink(
            $this->credentials($request)
        );
        return $response == Password::RESET_LINK_SENT
                    ? $this->sendResetLinkResponse($request, $response)
                    : $this->sendResetLinkFailedResponse($request, $response);
    }

(略)

    /**
     * Get the broker to be used during password reset.
     *
     * @return \Illuminate\Contracts\Auth\PasswordBroker
     */
    public function broker()
    {
        return Password::broker();
    }

vendor/laravel/framework/src/Illuminate/Support/Facades/Password.php を見てみます。

これもたいしたことはしてなくて、本体は vendor/laravel/framework/src/Illuminate/Auth/Passwords/PasswordBroker にあります。

    /**
     * Send a password reset link to a user.
     *
     * @param  array  $credentials
     * @param  \Closure|null  $callback
     * @return string
     */
    public function sendResetLink(array $credentials, Closure $callback = null)
    {
        // First we will check to see if we found a user at the given credentials and
        // if we did not we will redirect back to this current URI with a piece of
        // "flash" data in the session to indicate to the developers the errors.
        $user = $this->getUser($credentials);
        if (is_null($user)) {
            return static::INVALID_USER;
        }

        if ($this->tokens->recentlyCreatedToken($user)) {
            return static::RESET_THROTTLED;
        }

        $token = $this->tokens->create($user);

        if ($callback) {
            $callback($user, $token);
        } else {
            // Once we have the reset token, we are ready to send the message out to this
            // user with a link to reset their password. We will then redirect back to
            // the current URI having nothing set in the session to indicate errors.
            $user->sendPasswordResetNotification($token);
        }

        return static::RESET_LINK_SENT;
    }

(略)

    /**
     * Get the user for the given credentials.
     *
     * @param  array  $credentials
     * @return \Illuminate\Contracts\Auth\CanResetPassword|null
     *
     * @throws \UnexpectedValueException
     */
    public function getUser(array $credentials)
    {
        $credentials = Arr::except($credentials, ['token']);

        $user = $this->users->retrieveByCredentials($credentials);

        if ($user && ! $user instanceof CanResetPasswordContract) {
            throw new UnexpectedValueException('User must implement CanResetPassword interface.');
        }

        return $user;
    }

$credentials を出力してみると、リクエストパラメータが渡されていることがわかりました。

array (
  'email' => 'hoge@example.com',
)

$this->users を調べてみたら、vendor/laravel/framework/src/Illuminate/Auth/EloquentUserProvider であることがわかりました。
メールアドレスを元にクエリをしているところは、この部分です。

    /**
     * Retrieve a user by the given credentials.
     *
     * @param  array  $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByCredentials(array $credentials)
    {
        if (empty($credentials) ||
           (count($credentials) === 1 &&
            Str::contains($this->firstCredentialKey($credentials), 'password'))) {
            return;
        }

        // First we will add each credential element to the query as a where clause.
        // Then we can execute the query and, if we found a user, return it in a
        // Eloquent User "model" that will be utilized by the Guard instances.
        $query = $this->newModelQuery();

        foreach ($credentials as $key => $value) {
            if (Str::contains($key, 'password')) {
                continue;
            }

            if (is_array($value) || $value instanceof Arrayable) {
                $query->whereIn($key, $value);
            } elseif ($value instanceof Closure) {
                $value($query);
            } else {
                $query->where($key, $value);
            }
        }

        return $query->first();
    }

要するに、この部分をオーバーライドしてやればよさそうです。

Laravelでの vendor 以下に存在するクラスのオーバーライドの方法を調べてみました。

まず、app/Providers に、オーバーライドの実装をするクラスを作成します。
メールアドレスを暗号化しましたが、md5値を格納するカラムを作ってますので、これをキーにしてクエリしてみます。

app/Providers/Md5UserProvider を作成します。
email でのクエリの部分を md5 に置き換えてやります。
同一の md5 の値に対して複数がヒットすることはあり得ないとは思いますが、クエリの結果全てに対して email が一致するかどうかで調べます。
email 以外のクエリに対しては、元のコードが動くようにしておきます。

<?php

namespace App\Providers;

use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Support\Str;

class Md5UserProvider extends EloquentUserProvider
{
    /**
     * Retrieve a user by the given credentials.
     *
     * @param  array  $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByCredentials(array $credentials)
    {
        if (empty($credentials) ||
           (count($credentials) === 1 &&
            Str::contains($this->firstCredentialKey($credentials), 'password'))) {
            return;
        }

        // First we will add each credential element to the query as a where clause.
        // Then we can execute the query and, if we found a user, return it in a
        // Eloquent User "model" that will be utilized by the Guard instances.
        $query = $this->newModelQuery();

        $email = null;
        foreach ($credentials as $key => $value) {
            if (Str::contains($key, 'password')) {
                continue;
            }

            if (is_array($value) || $value instanceof Arrayable) {
                $query->whereIn($key, $value);
            } elseif ($value instanceof Closure) {
                $value($query);
            } else {
                if ($key == 'email') {
                    $email = $value;
                    $key = 'md5';
                    $value = md5($value);
                }
                $query->where($key, $value);
            }
        }
        if (!empty($email)) {
            $results = $query->get();
            foreach ($results as $u) {
                if ($u->email == $email) return $u;
            }
            return;
        }
        return $query->first();
    }
}

では、従来は EloquentUserProvider が呼び出されていたところを、Md5UserProvier が呼び出されるように変更する必要があります。

公式ドキュメントでは、ここらへんに書かれてます。

app/Providers/AuthServiceProvider を修正します。

<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Auth;

use App\Providers\Md5UserProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        // 'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Auth::provider('md5', function($app, array $config) {
            return new Md5UserProvider($app['hash'], $config['model']);
        });
    }
}

あとは、config/auth.php を修正すれば終わりです。
標準の driver の eloquent を、上で作成した md5 に変更するだけです。

    /*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | If you have multiple user tables or models you may configure multiple
    | sources which represent each model / table. These sources may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

    'providers' => [
        'users' => [
            //'driver' => 'eloquent',
            'driver' => 'md5',
            'model' => App\User::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

これで、メールアドレスを暗号化していても、パスワード再設定用リンクの送信ができるようになりました。







-Laravel
-, , ,

Copyright© satoshi's ソフトウェア開発 , 2024 All Rights Reserved Powered by STINGER.