如何在需要身份验证和/或授权的页面上处理 Ajax 调用?

How to handle an Ajax call on pages that requires authentication and / or authorization?

如果页面需要身份验证但没有找到用户,Symfony 会简单地重定向或显示登录页面。非常简单,我可以正常工作。

接下来,如果用户在需要身份验证的页面内进行 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_handlerfailure_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']);
    }
}

None 个 类 被访问。我错过了什么?

备注

为 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: 'ygXkzksqlR68HhAYB2WLOqcQvJZzgIrSH/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>

模板 app/Resources/views/Security/login.html.twig

不需要基地。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;
    }

}

登录控制器

一个简单的缓冲区控制器,用于在用户访问 /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;
    }
}