Symfony 5 / 使用 API 编写自定义身份验证总是返回我无效的凭据

Posted

技术标签:

【中文标题】Symfony 5 / 使用 API 编写自定义身份验证总是返回我无效的凭据【英文标题】:Symfony5 / write a Custom Authenticator with API always return me Invalid credentials 【发布时间】:2021-12-22 04:58:44 【问题描述】:

很简单。

我需要通过外部和粘性会话验证用户,但目前,我想验证任何用户

我正在关注自定义身份验证器文档:https://symfony.com/doc/current/security/custom_authenticator.html

我创建了一个没有 Doctrine 的 User 和 UserProvider:

\App\Security\User
\App\Security\UserProvider

我创建了自定义身份验证器:\App\Security\ApiAuthentificator.php 并始终返回有效的 SelfValidatingPassport:

<?php
// src/Security/ApiAuthenticator.php
namespace App\Security;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;

use Symfony\Component\HttpFoundation\RedirectResponse; 


use App\Security\UserProvider;

class ApiAuthenticator extends AbstractAuthenticator

    private $userProvider;

    public function __construct(UserProvider $userProvider)
    
        $this->userProvider = $userProvider;
    

    /**
     * Called on every request to decide if this authenticator should be
     * used for the request. Returning `false` will cause this authenticator
     * to be skipped.
     */
    public function supports(Request $request): ?bool
    
        return 'login' === $request->attributes->get('_route')
            && $request->isMethod('POST');
    

    public function authenticate(Request $request): PassportInterface
    
        /*
        $apiToken = $request->headers->get('X-AUTH-TOKEN');
        if (null === $apiToken) 
            // The token header was empty, authentication fails with HTTP Status
            // Code 401 "Unauthorized"
            throw new CustomUserMessageAuthenticationException('No API token provided');
        
        
        return new SelfValidatingPassport(new UserBadge($apiToken));
        */

        //
        // TODO: call to API
        // 
        
        $credentials = [
            'username' => $request->request->get('_username'),
            'password' => $request->request->get('_password')
        ];
        
        return new SelfValidatingPassport(new UserBadge($credentials['username']));
    

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    
        // on success, let the request continue
        return null;
        // return new RedirectResponse($this->urlGenerator->generate('admin'));
    

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    
        $data = [
            // you may want to customize or obfuscate the message first
            'message' => strtr($exception->getMessageKey(), $exception->getMessageData())

            // or to translate this message
            // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
        ];

        // return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
        return new Response('<html><body>' . $data['message'] . '</body></html>');
    


但是,当我登录时,登录过程响应 Invalid Credentials,但不是 ApiAuthenticator 上的 onAuthenticationFailure 函数。

在 UserProvider 中,总是在 loadUserByIdentifier 中返回一个 User,而 refreshUser 因为防火墙是stateless: true,所以它永远不会被调用:

<?php
// src/Security/UserProvider
namespace App\Security;

use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;

use App\Security\User;
use App\Utils\HelpCurl;


class UserProvider implements UserProviderInterface, PasswordUpgraderInterface

    /**
     * Symfony calls this method if you use features like switch_user
     * or remember_me.
     *
     * If you're not using these features, you do not need to implement
     * this method.
     *
     * @throws UserNotFoundException if the user is not found
     */
    public function loadUserByIdentifier($identifier): UserInterface
    
        // Load a User object from your data source or throw UserNotFoundException.
        // The $identifier argument may not actually be a username:
        // it is whatever value is being returned by the getUserIdentifier()
        // method in your User class.

        $user = new User();
        $user->setUsername($identifier);

        return $user;

        /*
        throw new \Exception('TODO: fill in loadUserByIdentifier() inside '.__FILE__);
        */
    

    /**
     * @deprecated since Symfony 5.3, loadUserByIdentifier() is used instead
     */
    public function loadUserByUsername($username): UserInterface
    
        return $this->loadUserByIdentifier($username);
    

    /**
     * Refreshes the user after being reloaded from the session.
     *
     * When a user is logged in, at the beginning of each request, the
     * User object is loaded from the session and then this method is
     * called. Your job is to make sure the user's data is still fresh by,
     * for example, re-querying for fresh User data.
     *
     * If your firewall is "stateless: true" (for a pure API), this
     * method is not called.
     *
     * @return UserInterface
     */
    public function refreshUser(UserInterface $user)
    
        if (!$user instanceof User) 
            throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user)));
        

        throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__);

        /*
        // Return a User object after making sure its data is "fresh".
        // Or throw a UsernameNotFoundException if the user no longer exists.
        throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__);
        */
    

    /**
     * Tells Symfony to use this provider for this User class.
     */
    public function supportsClass($class)
    
        return User::class === $class || is_subclass_of($class, User::class);
    

    /**
     * Upgrades the hashed password of a user, typically for using a better hash algorithm.
     */
    public function upgradePassword(UserInterface $user, string $newHashedPassword): void
    
        // TODO: when hashed passwords are in use, this method should:
        // 1. persist the new password in the user storage
        // 2. update the $user object with $user->setPassword($newHashedPassword);
    


有什么想法吗?缺少什么?

我粘贴安全和用户:

<?php
// src/Security/User
namespace App\Security;

use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class User implements UserInterface

    private $username;

    private $roles = [];

    /**
     * @deprecated since Symfony 5.3, use getUserIdentifier instead
     */
    public function getUsername(): string
    
        return (string) $this->username;
    

    public function setUsername(string $username): self
    
        $this->username = $username;

        return $this;
    

    /**
     * A visual identifier that represents this user.
     *
     * @see UserInterface
     */
    public function getUserIdentifier(): string
    
        return (string) $this->username;
    

    /**
     * @see UserInterface
     */
    public function getRoles(): array
    
        $roles = $this->roles;
        // guarantee every user at least has ROLE_USER
        $roles[] = 'ROLE_CUSTOMER';

        return array_unique($roles);
    

    public function setRoles(array $roles): self
    
        $this->roles = $roles;

        return $this;
    

    /**
     * This method can be removed in Symfony 6.0 - is not needed for apps that do not check user passwords.
     *
     * @see PasswordAuthenticatedUserInterface
     */
    public function getPassword(): ?string
    
        return null;
    

    /**
     * This method can be removed in Symfony 6.0 - is not needed for apps that do not check user passwords.
     *
     * @see UserInterface
     */
    public function getSalt(): ?string
    
        return null;
    

    /**
     * @see UserInterface
     */
    public function eraseCredentials()
    
        // If you store any temporary, sensitive data on the user, clear it here
        // $this->plainPassword = null;
    


security:
    # https://symfony.com/doc/current/security/authenticator_manager.html
    enable_authenticator_manager: true
    # https://symfony.com/doc/current/security.html#c-hashing-passwords
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        # used to reload user from session & other features (e.g. switch_user)
        app_user_provider:
            id: App\Security\UserProvider
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            # lazy: true
            stateless: true
            # provider: users_in_memory

            # activate different ways to authenticate
            # https://symfony.com/doc/current/security.html#firewalls-authentication

            # https://symfony.com/doc/current/security/impersonating_user.html
            # switch_user: true
            custom_authenticators:
                - App\Security\ApiAuthenticator
            form_login:
                login_path: login
                check_path: login
            logout:
                path: app_logout

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
        # -  path: ^/admin, roles: ROLE_ADMIN 
        # -  path: ^/profile, roles: ROLE_USER 

【问题讨论】:

【参考方案1】:

我解决了。

首先。问题是登录遵循他自己的身份验证,而不是ApiAuthenticator 说的那个。尽管也输入了ApiAuthenticator

security.yaml 中禁用form_login,用户通过ApiAuthenticator 正确验证。

第二。要进行粘性会话,从逻辑上讲,您必须禁用 statelessstateless: false

现在我有一个用户通过粘性会话进行身份验证,我只需要使用 api 进行验证。

谢谢!

security:
    # https://symfony.com/doc/current/security/authenticator_manager.html
    enable_authenticator_manager: true
    # https://symfony.com/doc/current/security.html#c-hashing-passwords
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        # used to reload user from session & other features (e.g. switch_user)
        app_user_provider:
            id: App\Security\UserProvider
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            # lazy: true
            # stateless: false
            # provider: users_in_memory

            # activate different ways to authenticate
            # https://symfony.com/doc/current/security.html#firewalls-authentication

            # https://symfony.com/doc/current/security/impersonating_user.html
            # switch_user: true
            custom_authenticators:
                - App\Security\ApiAuthenticator
            #form_login:
                #login_path: login
                #check_path: login
            logout:
                path: app_logout

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    # access_control:
        # -  path: ^/admin, roles: ROLE_CUSTOMER 
        # -  path: ^/profile, roles: ROLE_USER 

<?php
// src/Security/ApiAuthenticator.php
namespace App\Security;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;

use Symfony\Component\HttpFoundation\RedirectResponse; 


use App\Security\UserProvider;

class ApiAuthenticator extends AbstractAuthenticator

    private $userProvider;

    public function __construct(UserProvider $userProvider)
    
        $this->userProvider = $userProvider;
    

    /**
     * Called on every request to decide if this authenticator should be
     * used for the request. Returning `false` will cause this authenticator
     * to be skipped.
     */
    public function supports(Request $request): ?bool
    
        return 'login' === $request->attributes->get('_route')
            && $request->isMethod('POST');
    

    public function authenticate(Request $request): PassportInterface
    
        /*
        $apiToken = $request->headers->get('X-AUTH-TOKEN');
        if (null === $apiToken) 
            // The token header was empty, authentication fails with HTTP Status
            // Code 401 "Unauthorized"
            throw new CustomUserMessageAuthenticationException('No API token provided');
        
        
        return new SelfValidatingPassport(new UserBadge($apiToken));
        */

        //
        // TODO: call to API
        // 
        
        $credentials = [
            'username' => $request->request->get('_username'),
            'password' => $request->request->get('_password')
        ]; 
 
        return new SelfValidatingPassport(new UserBadge($credentials['username']));
    

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    
        // on success, let the request continue
        return null;
        // return new RedirectResponse($this->urlGenerator->generate('admin'));
    

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    
        $data = [
            // you may want to customize or obfuscate the message first
            'message' => strtr($exception->getMessageKey(), $exception->getMessageData())

            // or to translate this message
            // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
        ];

        // return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
        return new Response('<html><body>' . $data['message'] . '</body></html>');
    

<?php
// src/Security/UserProvider
namespace App\Security;

use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;

use App\Security\User;
use App\Utils\HelpCurl;


class UserProvider implements UserProviderInterface, PasswordUpgraderInterface

    /**
     * Symfony calls this method if you use features like switch_user
     * or remember_me.
     *
     * If you're not using these features, you do not need to implement
     * this method.
     *
     * @throws UserNotFoundException if the user is not found
     */
    public function loadUserByIdentifier($identifier): User
    
        // Load a User object from your data source or throw UserNotFoundException.
        // The $identifier argument may not actually be a username:
        // it is whatever value is being returned by the getUserIdentifier()
        // method in your User class.

        $user = new User();
        $user->setUsername($identifier);
        $user->setRoles(array('ROLE_ADMIN','ROLE_GESTOR'));
        return $user;

        /*
        throw new \Exception('TODO: fill in loadUserByIdentifier() inside '.__FILE__);
        */
    

    /**
     * @deprecated since Symfony 5.3, loadUserByIdentifier() is used instead
     */
    public function loadUserByUsername($username): User
    
        return $this->loadUserByIdentifier($username);
    

    /**
     * Refreshes the user after being reloaded from the session.
     *
     * When a user is logged in, at the beginning of each request, the
     * User object is loaded from the session and then this method is
     * called. Your job is to make sure the user's data is still fresh by,
     * for example, re-querying for fresh User data.
     *
     * If your firewall is "stateless: true" (for a pure API), this
     * method is not called.
     *
     * @return UserInterface
     */
    public function refreshUser(UserInterface $user)
    
        if (!$user instanceof User) 
            throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user)));
        

        return $user;


        // throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__);

        /*
        // Return a User object after making sure its data is "fresh".
        // Or throw a UsernameNotFoundException if the user no longer exists.
        throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__);
        */
    

    /**
     * Tells Symfony to use this provider for this User class.
     */
    public function supportsClass($class)
    
        return User::class === $class || is_subclass_of($class, User::class);
    

    /**
     * Upgrades the hashed password of a user, typically for using a better hash algorithm.
     */
    public function upgradePassword(UserInterface $user, string $newHashedPassword): void
    
        // TODO: when hashed passwords are in use, this method should:
        // 1. persist the new password in the user storage
        // 2. update the $user object with $user->setPassword($newHashedPassword);
    


【讨论】:

以上是关于Symfony 5 / 使用 API 编写自定义身份验证总是返回我无效的凭据的主要内容,如果未能解决你的问题,请参考以下文章

VueJs 前端的无效 CSRF 令牌错误(symfony 5)

在 Symfony2 中使用自定义身份验证提供程序

如何在 symfony 5 中验证一些自定义约束

如何使用自定义函数 Twig-symfony

Symfony 5 - 带有 TextType 的自定义 ChoiceType

将自定义用户提供程序与散列密码与 http basic 结合到后端 api