美文网首页全栈Web开发者Symfony學習筆記
[Symfony] 在FOSUserbundle添加Google

[Symfony] 在FOSUserbundle添加Google

作者: KimWong | 来源:发表于2017-01-30 11:58 被阅读142次

    這不是一篇Syfmony新手學習的文章####

    閱讀這篇文章時,你該具備以下知識:####

    1. Symfony表單的創建
    2. Service的使用以及Container的注入
    3. 對FOSUserBundle有一定的了解

    你將會在這篇文章獲得以下知識:####

    1. 寫一個Eventer Listener
    2. 覆寫Fosuser Controller
    3. Php Trait 的使用
    4. 注入個人邏輯至Symfony登入機制

    文章開始###

    開發 [Symfony] (https://symfony.com/)項目已經有一段時間了,目前 Symfony 很多開源項目的登入系統都會選擇 FOSUserBundle,加上做項目就是要求快呀,所以就硑究起來了,剛開始用時很久結這東西:what the fuck this bundle working on?
    這東西很方便,登入、驗証、授權、查找密碼都不用開發者去處理,Fos全家桶已經幫你Handle好了。但問題來了吖吖吖,登入這東西很玄呀,帳號/郵件 +密碼這種東西不能滿足大部份需求呀。 先別說用API拿TOKEN去LOGIN、OAUTH、甚麼JWT,大哥,我只想加個驗証碼怎麼也這麼難呀。這個是基本的吖吖。。。

    問題:
    1. FOSUserBundle does not handle the login. It is done by the Symfony security component.(官方)
    2. If you want to add more field about Authenticate, the most general way is custom authentication system.(官方)
    3. 在GITHUB走了二轉,的有很多關於google recaptcha或其它Verification注入到Symfony的FORM中,但是,但是!!這些BUNDLE全家桶都不能INJECT到FOSUserBundle的登入表單呀。

    所以,如果你想在Authenticate中加入GOOGLE Reaptcha 或者其它驗証碼方法時,你要怎做?有的司機會說:Build your custom authentication system 就行了呀。
    媽的!說了等於沒說。就像Symfony官網所說的:

    Creating a custom authentication system is hard####

    那麼難道就沒有一種簡單的方法麼?SYMFONY一直標榜說自己有多屌有多屌,強調從架構上提升代碼的複用性同時降低藕合性。所以當然有方法啦,下面就分享一個比較簡單的方法把Google Recaptcha。這個例子很實用,無論你想加入任何的checking,都可以用這個方法。

    Step:
    1. Install EWZRecaptchaBundle from git and config it.
    2. 新增一個表單Service,并注射container到GoogleLoginFormListener,用來渲染Google Recaptcha表單。
    3. Override FOSUserBundle 的 LoginAction,并Inject包含了Google Recaptcha的表單(步驟2)。
    4. 增加渲染google recapcha到登入頁面。
    5. 新增一個名叫GoogleLoginFormListener的listener,它是override SYMFONY的 UsernamePasswordFormAuthenticationListener
    6. GoogleLoginFormListener裹面,用 EWZRecaptchaBundle 檢查驗証內容

    好了,說到這裹,有沒有一點想法了,如果有,就動一下手吧。如果沒有,請繼續看下去。這裹會一部一部的分享。

    Step 1: install EWZRecaptchaBundle from git and config it.

    首先,從GIT安裝EWZRecaptchaBundle,并Config。
    在這部中,你只需跟著DOCUMENT啦。但是有一點要注意的:

    ajax : true
    

    這行千萬不要加,如果加了就會默認用了GOOGLE Recapcha(下稱GR,名稱太長不想打)。

    Step 2: 新增一個表單Service,并注射container到GoogleLoginFormListener,用來渲染Google Recaptcha表單。

    <?php
    
    namespace App\BackBundle\Helper;
    
    use Symfony\Component\DependencyInjection\ContainerInterface;
    use CTM\UserBundle\Form\CustomLoginForm;
    use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
    
    class GoogleRecaptchaForm
    {
        private $container;
    
        function __construct(ContainerInterface $containerInterface) {
            $this->container = $containerInterface;
        }
    
        public function createLoginForm(){
            $form = $this->container->get('form.factory')->create(new CustomLoginForm(), null, array(
                'action' => $this->generateUrl( 'fos_user_security_check' ),
                'method' => 'POST',
            ));
            $form->add('submit', 'submit', array('label' => 'Create'));
            return $form;
        }
    
        public function generateUrl($route, $parameters = array(), $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH)
        {
            return $this->container->get('router')->generate($route, $parameters, $referenceType);
        }
    
    }
    
    

    這裹的關鍵代碼就是action是

    'fos_user_security_check'
    

    雖然有些老司機會知道,這個ACTION FOS是沒有用到啦,這是題外話。

    Step 3: Override FOSUserBundle 的 LoginAction,并Inject包含了Google Recaptcha的表單(步驟2)。
    關於如何Override FOSUserBundle 的CONTROLLER,這裹不多說,自己看文檔
    Override SecurityController 的 LoginAction:

        public function loginAction(Request $request)
        {
            /** @var $session \Symfony\Component\HttpFoundation\Session\Session */
            $session = $request->getSession();
    
            if (class_exists('\Symfony\Component\Security\Core\Security')) {
                $authErrorKey = Security::AUTHENTICATION_ERROR;
                $lastUsernameKey = Security::LAST_USERNAME;
            } else {
                // BC for SF < 2.6
                $authErrorKey = SecurityContextInterface::AUTHENTICATION_ERROR;
                $lastUsernameKey = SecurityContextInterface::LAST_USERNAME;
            }
    
            // get the error if any (works with forward and redirect -- see below)
            if ($request->attributes->has($authErrorKey)) {
                $error = $request->attributes->get($authErrorKey);
            } elseif (null !== $session && $session->has($authErrorKey)) {
                $error = $session->get($authErrorKey);
                $session->remove($authErrorKey);
            } else {
                $error = null;
            }
    
            if (!$error instanceof AuthenticationException) {
                $error = null; // The value does not come from the security component.
            }
    
            // last username entered by the user
            $lastUsername = (null === $session) ? '' : $session->get($lastUsernameKey);
    
            if ($this->has('security.csrf.token_manager')) {
                $csrfToken = $this->get('security.csrf.token_manager')->getToken('authenticate')->getValue();
            } else {
                // BC for SF < 2.4
                $csrfToken = $this->has('form.csrf_provider')
                    ? $this->get('form.csrf_provider')->generateCsrfToken('authenticate')
                    : null;
            }
    
            $form = $this->get('app.form.google.recaptcha')->createLoginForm();
    
            return $this->renderLogin(array(
                'last_username' => $lastUsername,
                'error' => $error,
                'csrf_token' => $csrfToken,
                'form' => $form->createView(),
            ));
        }
    

    這裹的關鍵代碼是:

            $form = $this->get('app.form.google.recaptcha')->createLoginForm();
    
            return $this->renderLogin(array(
                'last_username' => $lastUsername,
                'error' => $error,
                'csrf_token' => $csrfToken,
                'form' => $form->createView(),
            ));
    

    就是渲染我們上一部的表單。

    Step 4: 增加渲染google recapcha到登入頁面。

            <form class="form-horizontal" name="{{ form.vars.name }}" action="{{ path('fos_user_security_check') }}"
                  method="post">
                <fieldset>
    
                    <input type="hidden" name="_csrf_token" value="{{ csrf_token }}"/>
    
                    <div class="form-group" title="Username">
                        <div class="col-md-12">
                            <input class="form-control" name="_username" id="username" type="text"
                                   placeholder="{{ 'Type username' | trans }}" value="{{ last_username }}"/>
                        </div>
                    </div>
                    <div class="form-group" title="Password">
                        <div class="col-md-12">
                            <input class="form-control" name="_password" id="password" type="password"
                                   placeholder="{{ 'Type password' | trans }}"/>
                        </div>
                    </div>
    
                    {% if error is not null %}
                        <div class="form-group ">
                            {% if 'recaptcha' in error.message  and '|' in error.message %}
                                {% set key = error.message | split ('|') %}
                                    <p class="col-xs-12 col-lg-12 col-md-12 customWarning">{{ key.0 | trans ~ " " ~ "Error" | trans  }} </p>
                                    <p class="col-xs-12 col-lg-12 col-md-12 customWarning">{{ key.1 | trans }}</p>
                            {% else %}
                                    <p class="col-md-12 col-lg-12 col-md-12 customWarning">{{ error.messageKey|trans(error.messageData, 'security') }}</p>
                                    <p class="col-md-12 col-lg-12 col-md-12 customWarning">{{ 'form.errorLogin' | trans }}</p>
                            {% endif %}
                        </div>
                    {% endif %}
    
                    {% if form.recaptcha.vars.ewz_recaptcha_enabled == true %}
                        <div class="form-group">
                            <div class="col-xs-12 col-lg--12 col-md-12">
                                {% form_theme form.recaptcha 'EWZRecaptchaBundle:Form:ewz_recaptcha_widget.html.twig' %}
                                {{ form_widget(form.recaptcha ) }}
                            </div>
                        </div>
                    {% endif %}
    
                    <div class="form-group">
                        <div class="col-md-6">
                            <button type="submit" id="_submit" name="{{ form.submit.vars.full_name }}"
                                    class="btn btn-info btn-block">{{ 'Login' | trans }}</button>
                        </div>
    
                        <div class="col-md-6">
                            <a class="btn btn-success btn-block"
                               href="{{ path('fos_user_resetting_request') }}">{{ 'Forget password' | trans }}</a>
                        </div>
                    </div>
    
                    <div id="recaptcha"></div>
    
                </fieldset>
            </form>
    

    TWIG的HTML代碼不多說,主要是自己實現一個FORM,INJECT剛才的EWZRecaptchaBundle FORM進去,到這裹都很簡單。看下一步。關鍵。

    STEP 5: 新增一個名叫GoogleLoginFormListener的listener,它是override SYMFONY的 UsernamePasswordFormAuthenticationListener

    這部份是關鍵,因為我們希望在原有的Symfony登入驗証上增加驗証碼的驗証,但因為FOS是不作出驗証的,其實它是交給SYMFONY作驗証的,所以,我們就要作一個LISTENER,在symfony checking你的帳號/EMAIL/密碼 的ACTION 前/後 做一次驗証碼的CHECK。 這個例子很實用,無論你想加入任何的checking,都可以用這個方法。
    當然,如果你想要加入checking的是用戶的資料,當然是要實現一個的Login ACTION 後的LISTENER。

    核心部份具體是你要override一個名叫UsernamePasswordFormAuthenticationListener的Listener。這個LISTENER是監聽你帳號密碼驗証前的ACTION:

    <?php
    /**
     * Created by PhpStorm.
     * User: kimwong
     */
    
    namespace App\BackBundle\EventListener;
    use Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener as BaseListener;
    use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpKernel\Log\LoggerInterface;
    use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
    use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
    use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
    use Symfony\Component\Security\Http\HttpUtils;
    use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
    use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
    use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
    use Symfony\Component\EventDispatcher\EventDispatcherInterface;
    use Symfony\Component\Security\Core\Exception\BadCredentialsException;
    use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
    use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
    use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderAdapter;
    use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
    use Symfony\Component\Security\Http\ParameterBagUtils;
    use Symfony\Component\Security\Core\Security;
    use Symfony\Component\Security\Csrf\CsrfToken;
    use \Symfony\Component\DependencyInjection\ContainerAwareTrait;
    use \Symfony\Component\DependencyInjection\ContainerAwareInterface;
    
    
    class GoogleLoginFormListener extends BaseListener implements ContainerAwareInterface
    {
        use ContainerAwareTrait;
    
        private $csrfTokenManager;
    
        public function __construct(
            TokenStorageInterface $tokenStorage,
            AuthenticationManagerInterface $authenticationManager,
            SessionAuthenticationStrategyInterface $sessionStrategy,
            HttpUtils $httpUtils,
            $providerKey,
            AuthenticationSuccessHandlerInterface $successHandler,
            AuthenticationFailureHandlerInterface $failureHandler,
            array $options = array(),
            LoggerInterface $logger = null,
            EventDispatcherInterface $dispatcher = null,
            $csrfTokenManager = null
        )
        {
            if ($csrfTokenManager instanceof CsrfProviderInterface) {
                $csrfTokenManager = new CsrfProviderAdapter($csrfTokenManager);
            }elseif (null !== $csrfTokenManager && !$csrfTokenManager instanceof CsrfTokenManagerInterface) {
                throw new InvalidArgumentException('The CSRF token manager should be an instance of CsrfProviderInterface or CsrfTokenManagerInterface.');
            }
    
            if (isset($options['intention'])) {
                if (isset($options['csrf_token_id'])) {
                    throw new \InvalidArgumentException(sprintf('You should only define an option for one of "intention" or "csrf_token_id" for the "%s". Use the "csrf_token_id" as it replaces "intention".', __CLASS__));
                }
    
                @trigger_error('The "intention" option for the '.__CLASS__.' is deprecated since version 2.8 and will be removed in 3.0. Use the "csrf_token_id" option instead.', E_USER_DEPRECATED);
    
                $options['csrf_token_id'] = $options['intention'];
            }
    
            parent::__construct($tokenStorage, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, array_merge(array(
                'username_parameter' => '_username',
                'password_parameter' => '_password',
                'csrf_parameter' => '_csrf_token',
                'csrf_token_id' => 'authenticate',
                'captcha' => 'captcha',
                'post_only' => true,
            ), $options), $logger, $dispatcher);
    
            $this->csrfTokenManager = $csrfTokenManager;
        }
    
        /**
         * {@inheritdoc}
         */
        protected function requiresAuthentication(Request $request)
        {
            if ($this->options['post_only'] && !$request->isMethod('POST')) {
                return false;
            }
    
            return parent::requiresAuthentication($request);
        }
    
        /**
         * {@inheritdoc}
         */
        protected function attemptAuthentication(Request $request)
        {
            if (null !== $this->csrfTokenManager) {
                $csrfToken = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']);
    
                if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['csrf_token_id'], $csrfToken))) {
                    throw new InvalidCsrfTokenException('Invalid CSRF token.');
                }
            }
    
            $form = $this->container->get('app.form.google.recaptcha')->createLoginForm();
    
            if ($form->createView()->children['recaptcha']->vars['ewz_recaptcha_enabled']){
                $form->handleRequest($request);
                if (!$form->isValid()) {
                    if ( $form->get('recaptcha')->getErrors() ){
                        $error = $form->get('recaptcha')->getErrorsAsString();
                        throw new BadCredentialsException(  'recaptcha' . "|"  .  $error );
                    }
                    throw new BadCredentialsException( 'Unexpected Error!' );
                }
            }
    
            if ($this->options['post_only']) {
                $username = trim(ParameterBagUtils::getParameterBagValue($request->request, $this->options['username_parameter']));
                $password = ParameterBagUtils::getParameterBagValue($request->request, $this->options['password_parameter']);
            } else {
                $username = trim(ParameterBagUtils::getRequestParameterValue($request, $this->options['username_parameter']));
                $password = ParameterBagUtils::getRequestParameterValue($request, $this->options['password_parameter']);
            }
    
    
            if (strlen($username) > Security::MAX_USERNAME_LENGTH) {
                throw new BadCredentialsException('Invalid username.');
            }
    
            $request->getSession()->set(Security::LAST_USERNAME, $username);
    
            return $this->authenticationManager->authenticate(new UsernamePasswordToken($username, $password, $this->providerKey));
        }
    }
    

    這裹核心代碼有二個:
    1.注入ContainerAwareTrait。 用Trait的方法注入Container到這個Listener。

    不要問我這裹為甚麼要用Trait而不用傳統的在construct裹注入,并不是因為Trait很屌很方便很PHP潮流,是因為這裹如果你用construct,我很負責任100%擔保,你會爆ERROR的,詳細自己試,原因有點複雜,這裹先不說了#####

    2.覆寫原生attemptAuthentication,加入GR的checking:

            $form = $this->container->get('app.form.google.recaptcha')->createLoginForm();
    
            if ($form->createView()->children['recaptcha']->vars['ewz_recaptcha_enabled']){
                $form->handleRequest($request);
                if (!$form->isValid()) {
                    if ( $form->get('recaptcha')->getErrors() ){
                        $error = $form->get('recaptcha')->getErrorsAsString();
                        throw new BadCredentialsException(  'recaptcha' . "|"  .  $error );
                    }
                    throw new BadCredentialsException( 'Unexpected Error!' );
                }
            }
    

    因為我們用了EWZBUNDLE,所以這裹的驗証我們不需要自己實作,用handleRequest就好了。主要是做一個配對,錯的話把ERROR MESSAGE RENDER到前端。

    整個GR注入到FOSUSERBUNDLE就完成了。

    是不是很簡單呢。

    話說Angular2已經進入最後階段了,未來有機會會分享一下一些Ng2的經驗。

    謝謝觀看

    相关文章

      网友评论

        本文标题:[Symfony] 在FOSUserbundle添加Google

        本文链接:https://www.haomeiwen.com/subject/pnqhittx.html