Как обрабатывать вызов Ajax на страницах, требующих аутентификации и / или авторизации? - PullRequest
0 голосов
/ 23 сентября 2018

Если страница требует аутентификации и пользователь не найден, Symfony просто перенаправляет или показывает страницу входа.Это настолько просто, что я все заработал.

Далее я хотел бы отправить пользовательское сообщение (или html), если пользователь делает Ajax-вызов внутри страницы, требующей аутентификации, но сеанс, например, прервался (Пользователь больше не аутентифицирован).

security.yml

security:
    encoders:
        AppBundle\Entity\User:
            algorithm: bcrypt

    role_hierarchy:
        ROLE_ADMIN: ROLE_USER

    providers:
        db_provider:
            entity:
                class: AppBundle:User

    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        main:
            anonymous: ~

            pattern: ^/

            form_login:
                login_path: security_login
                check_path: security_login
                use_forward: false
                failure_handler: AppBundle\Security\AuthenticationHandler

            logout:
                path: /logout
                target: /

            access_denied_handler: AppBundle\Security\AccessDeniedHandler

    access_control:
        - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, role: ROLE_ADMIN }

Я попытался перехватить ошибку события, используя access_denied_handler или failure_handler.

AppBundle \ Security \ AccessDeniedHandler.php

namespace AppBundle\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;

class AccessDeniedHandler implements AccessDeniedHandlerInterface {

    public function handle(Request $request, AccessDeniedException $exception) {

        return new JsonResponse([
            'success' => 0,
            'error'   => 1,
            'message' => $exception -> getMessage(),
            'from'    => 'AccessDeniedHandler'
        ]);
    }
}

AppBundle \ Security \ AuthenticationHandler.php

namespace AppBundle\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

class AuthenticationHandler implements AuthenticationFailureHandlerInterface {

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception) {
        return new JsonResponse(['error' => 1, 'from' => 'AuthenticationHandler']);
    }
}

Ни один из этих классов не доступен.Чего мне не хватает?

1 Ответ

0 голосов
/ 25 сентября 2018

Примечания

Создан для проекта Symfony 3.4, должен быть совместим с Symfony 4, но я не тестировал;

Все службы имеют автоматическую привязку, поэтому добавлять нечего вservices.yml

Я не использую FOSUserBundle;

Я не соблюдаю стандарты кодирования Symfony;

Я делал заметки здесь и там;также я поместил некоторые комментарии в самом коде;

Важная часть в конце (LoginFormAuthenticator), я публикую весь код, надеюсь, кому-то будет легче, чем мне.

Источник вдохновения:

https://symfony.com/doc/3.4/security.html

https://symfonycasts.com/screencast/symfony3-security

https://www.sitepoint.com/easier-authentication-with-guard-in-symfony-3/

Стена кода

security.yml

Конфигурация безопасности

Для пользователя «memory» имя пользователя и пароль «admin»

security:
    encoders:
        Symfony\Component\Security\Core\User\User:
            algorithm: bcrypt

        AppBundle\Entity\User:
            algorithm: bcrypt

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: ROLE_ADMIN

    providers:
        chain_provider:
            chain:
                providers: [memory_provider, db_provider]

        memory_provider:
            memory:
                users:
                    admin:
                        password: '$2y$13$21gXkzksqlR68HhAYB2WLOqcQvJZzgIrSH/KRq1aEzkkOnjI7lR9e'
                        roles: 'ROLE_SUPER_ADMIN'

        db_provider:
            entity:
                class: AppBundle:User
                property: email

    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        main:
            anonymous: ~

            pattern: ^/

            logout:
                path: /logout
                target: /

            guard:
                authenticators:
                    - AppBundle\Security\LoginFormAuthenticator

    access_control:
        - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, role: ROLE_USER }

Шаблон app / Resources / views / Security / _content.login.html.twig

{% set form_action = path('security_login') %}

<form action="{{ form_action }}" method="post" autocomplete="off" id="login_f">
    {% if error %}
        <div class="login_form_error">{{ error.messageKey }}</div>
    {% endif %}

    <div class="closed">
        <input type="hidden" name="_csrf_token" value="{{ csrf_token(login_csrf_token) }}" />
    </div>

    <div class="login_field login_field_0">
        <label for="login_username" class="login_l">
            <i class="fas fa-user"></i>
        </label>
        <input type="text" class="login_i" id="login_username" name="_username" placeholder="Username" />
    </div>

    <div class="login_field login_field_1">
        <label for="login_password" class="login_l">
            <i class="fas fa-key"></i>
        </label>
        <input type="password" class="login_i" id="login_password" name="_password" placeholder="Password" />
    </div>

    <div>
        <input type="submit" class="login_bttn" id="_submit" value="Login" />
    </div>
</form>

Template app / Resources / views / Security /login.html.twig

Нет необходимости в base.html.twig

{% extends 'base.html.twig' %}

{% block content %}
    <div id="login_c">
        {% include 'Security/_content.login.html.twig' %}
    </div>
{% endblock %}

Служба

Отображает страницу входа илисодержимое входа

Замените постоянное значение CSRF_TOKEN своим собственным

namespace AppBundle\Services\User;

use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class LoginFormService {

    private $templatingEngine;
    private $authenticationUtils;

    const CSRF_TOKEN = 'login:token:w4kSzA3v5VJyb4aWLbV7stAY92cNwgL77J6QrXpU!';

    function __construct(
        EngineInterface $templatingEngine,
        AuthenticationUtils $authenticationUtils) {

        $this -> templatingEngine    = $templatingEngine;
        $this -> authenticationUtils = $authenticationUtils;

    }

    function getHtml($contentOnly = False) {

        // last username entered by the user
        $lastUsername = $this -> authenticationUtils -> getLastUsername();

        // get the login error if there is one
        $error = $this -> authenticationUtils -> getLastAuthenticationError();

        $html_vars = array(
            'lastUsername'     => $lastUsername,
            'error'            => $error,
            'login_csrf_token' => self::CSRF_TOKEN,
        );

        $html_template = 'Security/login.html.twig';
        if ( $contentOnly ) {
            $html_template = 'Security/_content.login.html.twig';
        }

        $html = $this -> templatingEngine -> render($html_template, $html_vars);

        return $html;
    }

}

Контроллер входа

Простой контроллер буфера для отображения страницы входаесли пользователь получитess / login

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

use AppBundle\Services\User\LoginFormService;

class SecurityController extends Controller {


    /**
     @Route("/login", name="security_login")
    */
    public function loginAction(LoginFormService $loginFormService, Request $request) {
        return new Response($loginFormService -> getHtml());
    }

    /**
     @Route("/logout", name="security_logout")
    */
    public function logoutAction() {}

}

Аутентификатор Guard

Вместо "project_homepage_route" используйте любой маршрут, который вы хотите

namespace AppBundle\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Routing\RouterInterface;

use AppBundle\Services\User\LoginFormService;

class LoginFormAuthenticator extends AbstractGuardAuthenticator {

    private $router;
    private $templatingEngine;
    private $passwordEncoder;
    private $csrfTokenManager;

    private $loginService;

    protected $auth_error_csrf    = 'Invalid CSRF token!!!';
    protected $auth_error_message = 'Invalid credentials!!!';

    function __construct(
        RouterInterface $router, 
        UserPasswordEncoderInterface $passwordEncoder,
        CsrfTokenManagerInterface $csrfTokenManager,
        LoginFormService $loginService) {

        $this -> router           = $router;
        $this -> passwordEncoder  = $passwordEncoder;
        $this -> csrfTokenManager = $csrfTokenManager;
        $this -> loginService     = $loginService;

    }

    /* Methods */

    protected function loginResponse(Request $request, $forbidden = False) {

        // The javascript library must set the 'X-Requested-With' header
        // xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        if ( $request -> isXmlHttpRequest() ) {
            $response = new JsonResponse([
                'error' => 1,
                'html'  => $this -> loginService -> getHtml(True)
            ]);
        } else {
            $html = $this -> loginService -> getHtml();
            $response = new Response($html);
        }

        if ($forbidden) {
            $response -> setStatusCode(Response::HTTP_FORBIDDEN);
        }

        return $response;
    }

    /* AbstractGuardAuthenticator methods */

    public function supports(Request $request) {
        return $request -> attributes -> get('_route') === 'security_login' && $request -> isMethod('POST');
    }

    public function getCredentials(Request $request) {

        // Add csrf protection
        $csrfData  = $request -> request -> get('_csrf_token');
        $csrfToken = new CsrfToken(LoginFormService::CSRF_TOKEN, $csrfData);

        if ( !$this -> csrfTokenManager -> isTokenValid($csrfToken) ) {
            throw new InvalidCsrfTokenException( $this -> auth_error_csrf );
        }

        return array(
            'username' => $request -> request -> get('_username'),
            'password' => $request -> request -> get('_password'),
        );
    }

    public function getUser($credentials, UserProviderInterface $userProvider) {

        $username = $credentials['username'];

        try {
            return $userProvider -> loadUserByUsername($username);
        } catch (UsernameNotFoundException $e) {
            throw new CustomUserMessageAuthenticationException( $this -> auth_error_message );
        }

        return null;
    }

    public function checkCredentials($credentials, UserInterface $user) {

        $is_valid_password = $this -> passwordEncoder -> isPasswordValid($user, $credentials['password']);

        if ( !$is_valid_password ) {
            throw new CustomUserMessageAuthenticationException( $this -> auth_error_message );
            return;
        }

        return True;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $authException) {

        $session = $request -> getSession();
        $session -> set(Security::AUTHENTICATION_ERROR, $authException);
        $session -> set(Security::LAST_USERNAME, $request -> request -> get('_username'));

        // Shows the login form instead of the page content
        return $this -> loginResponse($request, True);

        // If you want redirect make sure the line below is used
        // return new RedirectResponse($this -> router -> generate('security_login'));
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) {

        if ( $request -> isXmlHttpRequest() ) {
            return new JsonResponse([
                'success' => 1,
                'message' => 'Authentication success!'
            ]);
        }

        return new RedirectResponse($this -> router -> generate('project_homepage_route'));
    }

    public function start(Request $request, AuthenticationException $authException = null) {

        // Shows the login form instead of the page content
        return $this -> loginResponse($request);

        // If you want redirect make sure the line below is used
        // return new RedirectResponse($this -> router -> generate('security_login'));
    }

    public function supportsRememberMe() {
        return false;
    }
}
...