BeginnerEngineerBlog
中の人
中の人

ECCUBE4.1(symfony4系)でパスワードがあっているかだけ判定したい

公開: 2021-10-16 19:14
更新: 2023-04-06 14:58
1773
ECCUBE4.1 symfony4.x php ダイブ
ECCUBE4.1系(symfony4.x系)でパスワードがあっているかだけ判定したい場合の紹介になります。

こんにちは!

中の人です!

最近ECCUBE4.1系(symfony4系)での開発をしているのですが、登録済みユーザーのパスワードのみ合っているか判定する処理を実装しました。

正直symfonyについてあまり詳しくないし、調べてもヒットしないし、symfonyの情報が英語ばっかだし、ECCUBEもなんでsymfony採用しとんねんと八つ当たりしながら(ECCUBEは悪くありません。)、ひたすらソースコードを漁ってなんとか実装できたので紹介しようと思います!


実装方法


では、以下が実装方法です。
(ECCUBEのcustomizeディレクトリに適当なコントローラを作成しています。ECCUBEでのカスタマイズのやり方は
公式のEC-CUBE 4.0 開発者向けドキュメント を参照してください。)

<?php


namespace Customize\Controller\Mypage;

use Eccube\Controller\AbstractController;
use Eccube\Entity\Customer;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Eccube\Repository\CustomerRepository;


class CheckCustomerPasswordController extends AbstractController
{
    private $encoderFactory;


    private $customerRepository;


    public function __construct(
        EncoderFactoryInterface $encoderFactory,
        CustomerRepository $customerRepository
    )
    {
        $this->encoderFactory = $encoderFactory;
        $this->customerRepository = $customerRepository;
    }

    /**
     * @param Request $request
     * @Route("/check/customer/password", name="check_customer_password", methods={"POST"})
     * @Template("Hoge/huga.twig")
     */
    public function checkCustomerPassword(Request $request)
    {
        $response = false;

        //フォームに入力されたemailとpasswordをrequestから取得
        $email = $request->get('email');
        $pass = $request->get('pass');

        //requestのemailから登録済みのユーザーを検索
        $customers = $this->customerRepository->createQueryBuilder('c')
            ->where('c.email = :email')
            ->setParameter('email', $email)
            ->getQuery()
            ->getResult();

        //ユーザーがいたら
        if (!empty($customers)) {

            //CustomerEntityを取得
            $customer = $customers[0];

            //エンコーダーなるものを取得
            $encoder = $this->encoderFactory->getEncoder($customer);

            //↓以下がパスワードがあっているか判定している
            if ($encoder->isPasswordValid($customer->getPassword(), $pass, $customer->getSalt())) {
                $response = true;
            }
        }
        
                return [
            'response' => $response,
        ];
    }
}

こんな感じです。

どんな処理をしているかはコメントアウトを参考にしてください。

(2021/10/20追記
customerのゲットの記述ですが、

        $customer = $this->customerRepository->findOneBy(
            [
                'email' => $email,
            ]
        );
        if ($customer) {
            ...

の方がスマートですね。まぁ、お好きな方法でということで。
)

ということでこれで登録済みユーザーのpasswordがあっているか判定できます!

非常に簡単ですね!


DaoAuthenticationProvider.php


さぁ、ここからは、この実装をどこのファイルから見つけたかのかを紹介していきます。

そもそも、passwordがあっているか判定する必要があるのて、login機能でしかないですよね。

ということで、xdebugを駆使してログイン処理を追って、深くソースコードの海の中に潜っていったらありました。

20230406追記
ファイルのパスにAuthenticationの記述漏れがあったので追加しました

📁 /vender/symfony/security/Core/Authentication/Provider/DaoAuthenticationProvider.php

<?php


/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */


namespace Symfony\Component\Security\Core\Authentication\Provider;


use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;


/**
 * DaoAuthenticationProvider uses a UserProviderInterface to retrieve the user
 * for a UsernamePasswordToken.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 */
class DaoAuthenticationProvider extends UserAuthenticationProvider
{
    private $encoderFactory;
    private $userProvider;


    public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, string $providerKey, EncoderFactoryInterface $encoderFactory, bool $hideUserNotFoundExceptions = true)
    {
        parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions);


        $this->encoderFactory = $encoderFactory;
        $this->userProvider = $userProvider;
    }


    /**
     * {@inheritdoc}
     */
    protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token)
    {
        $currentUser = $token->getUser();
        if ($currentUser instanceof UserInterface) {
            if ($currentUser->getPassword() !== $user->getPassword()) {
                throw new BadCredentialsException('The credentials were changed from another session.');
            }
        } else {
            if ('' === ($presentedPassword = $token->getCredentials())) {
                throw new BadCredentialsException('The presented password cannot be empty.');
            }


            if (null === $user->getPassword()) {
                throw new BadCredentialsException('The presented password is invalid.');
            }


            $encoder = $this->encoderFactory->getEncoder($user);


            if (!$encoder->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) {
                throw new BadCredentialsException('The presented password is invalid.');
            }


            if ($this->userProvider instanceof PasswordUpgraderInterface && method_exists($encoder, 'needsRehash') && $encoder->needsRehash($user->getPassword())) {
                $this->userProvider->upgradePassword($user, $encoder->encodePassword($presentedPassword, $user->getSalt()));
            }
        }
    }


    /**
     * {@inheritdoc}
     */
    protected function retrieveUser($username, UsernamePasswordToken $token)
    {
        $user = $token->getUser();
        if ($user instanceof UserInterface) {
            return $user;
        }


        try {
            $user = $this->userProvider->loadUserByUsername($username);


            if (!$user instanceof UserInterface) {
                throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
            }


            return $user;
        } catch (UsernameNotFoundException $e) {
            $e->setUsername($username);
            throw $e;
        } catch (\Exception $e) {
            $e = new AuthenticationServiceException($e->getMessage(), 0, $e);
            $e->setToken($token);
            throw $e;
        }
    }
}



はい、こいつが該当のファイルです。

正直どこからどう飛んでこのファイルにたどり着いたかは覚えていません。

で、このファイルの

    /**
     * {@inheritdoc}
     */
    protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token)
    {
        $currentUser = $token->getUser();
        if ($currentUser instanceof UserInterface) {
            if ($currentUser->getPassword() !== $user->getPassword()) {
                throw new BadCredentialsException('The credentials were changed from another session.');
            }
        } else {
            if ('' === ($presentedPassword = $token->getCredentials())) {
                throw new BadCredentialsException('The presented password cannot be empty.');
            }


            if (null === $user->getPassword()) {
                throw new BadCredentialsException('The presented password is invalid.');
            }


            $encoder = $this->encoderFactory->getEncoder($user);


            if (!$encoder->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) {
                throw new BadCredentialsException('The presented password is invalid.');
            }


            if ($this->userProvider instanceof PasswordUpgraderInterface && method_exists($encoder, 'needsRehash') && $encoder->needsRehash($user->getPassword())) {
                $this->userProvider->upgradePassword($user, $encoder->encodePassword($presentedPassword, $user->getSalt()));
            }
        }
    }

このアクションの

            if (!$encoder->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) {
                throw new BadCredentialsException('The presented password is invalid.');
            }

この部分がパスワードが合っているか判定しています。

ようやく見つけたぜクソが!と思ったのですが、当初、このアクションの引数のUserInterfaceてなに!?となって、めっちゃ他のソースファイル参考にしてUserInterfaceを実装したインスタンス作ろうと頑張ったのですが、うまくいきませんでした。
で、クソが!となったのですが、そもそも

getPassword()
getSalt()

てCustomerのentityに定義されてるよねって思って、Customerのentity見たら、

class Customer extends \Eccube\Entity\AbstractEntity implements UserInterface, \Serializable

あ、インターフェース実装してんじゃん。なんだよめっちゃ頑張ってインスタンス作ろうとしたのにクソがと思って、

さらに言えば、

isPasswordValid

の引数てそもそもstringだよねってなって、UserInterface関係ねーやってなって、Customerのentityインスタンスからpasswordとsaltを引数に渡したら無事に判定ができたのでしたファァック!

(2021/10/20追記
UserInterface関係ないと書きましたが、
$encoder = $this->encoderFactory->getEncoder($user);

こいつの引数はUserInterfaceかstringみたいで、stringの場合どう書くのが正解かわからないので結局UserInterfaceは必要でした。フ◯ッキンフ◯ッキン☆
)


終わりに


今回の実装をした理由は、2段階認証の実装をしたかったからです。
ECCUBEの管理側にもデフォルトで2段階認証の実装がされていますが、デフォルトの実装方法では今回の開発にはあまり向いていなかったので、今回のような実装を行いました。

(※ 2段階認証自体の実装方法については、また別の機会に紹介できればと思います!)

passwordを判定する実装自体非常にシンプルで、実装方法がわかってしまうとなんだ簡単じゃーんと思いますが、ソースコードを追っているときは非常にイライラしながら追っていました。

  • どこで判定してんねん!
  • このインスタンスは何!?
  • そもそもloginのリクエストはどのファイルに飛んでんねん!?

みたいな感じで。

そもそもsymfonyのログイン処理がコアのファイルで一部始終やっちゃってるっぽいので、最初は意味わからん状態だったんですよねファ◯ク。

ということで、今回の記事を書いていたらその時の感情が再燃して、一部文章に現れてしまいましたが、みなさんは心を鎮めて、煩悩を取り去って、健やかにプログラミングしていきましょう!

(20211122追記
この処理を利用して2段階認証を実装する処理はこちらに書きましたので気になる方は確認してみてください。
)


ということで、今回の記事は以上になります!

ではまた!
0
0
0
0
通信エラーが発生しました。
似たような記事