SF4身份验证正在运行但令牌未保存(从不调用序列化)

时间:2018-05-31 07:28:50

标签: php symfony authentication serialization yaml

我在第一个REST API上使用symfony 4进行身份验证时出现问题。

事实是我的身份验证成功,然后调用了我的重定向URL,但在此重定向期间身份验证令牌丢失。我还注意到我的用户实体永远不会调用我的序列化方法。

我想要的是:当我的身份验证成功后,我的个人资料页面就会被调用。 但是使用该代码,我得到的只是来自配置文件的302重定向,意味着我的身份验证有效,但令牌丢失(如果它存在,从未见过它)

我的唯一提示是:

  • 用户从未调用的序列化方法(这很重要吗?)编辑:不,因为我需要无状态,所以删除那些方法。
  • 我的身份验证有效,因为如果我在凭据中出错,我会收到正确的错误。

以下是代码:

我的提供者

<?php

declare(strict_types = 1);

namespace App\Api\Auth\Provider;

use App\Api\User\Entity\User;
use App\Api\User\Repository\UserRepository;
use App\Domain\User\ValueObject\Email;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class AuthProvider implements UserProviderInterface
{
    /**
     * @var \App\Api\User\Repository\UserRepository
     */
    private $userRepository;

    /**
     * AuthProvider constructor.
     * @param \App\Api\User\Repository\UserRepository $repository
     */
    public function __construct(UserRepository $repository)
    {
        $this->userRepository = $repository;
    }

    /**
     * @param string $email
     * @return mixed
     */
    public function loadUserByUsername($email)
    {
        try {
            $user = $this->userRepository->getUser($email);
        } catch (UnsupportedUserException $e) {
            throw new UsernameNotFoundException('User not found', 1001, $e);
        }

        return $user;
    }

    /**
     * @param \Symfony\Component\Security\Core\User\UserInterface | User $user
     * @return mixed
     */
    public function refreshUser(UserInterface $user)
    {
        return $this->loadUserByUsername($user->getEmail());
    }

    /**
     * Qualify the supported class for this provider
     * @param string $class
     * @return string
     */
    public function supportsClass($class)
    {
        if (!$class instanceof User) {
            throw new UnsupportedUserException(
                sprintf('Entity given is not supported, expected User got %s', $class),
                1000
            );
        }

        return $class;
    }
}

我的警卫:

<?php

declare(strict_types = 1);

namespace App\Api\Auth\Guard;

use App\Api\User\Repository\UserRepository;
use App\Domain\User\Exception\InvalidCredentialsException;
use App\Domain\User\ValueObject\Credentials;
use App\Domain\User\ValueObject\Email;
use App\Domain\User\ValueObject\HashedPassword;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;

/**
 * Allow the authentication by giving credential, when login process achieved and valid, profile page show up
 * Class LoginAuthenticator
 * @package App\Api\Auth\Guard
 */
final class LoginAuthenticator extends AbstractFormLoginAuthenticator
{
    const LOGIN = 'login';
    const SUCCESS_REDIRECT = 'profile';

    /**
     * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
     */
    private $router;

    /**
     * @var \App\Api\User\Repository\UserRepository
     */
    private $repository;

    public function __construct(UrlGeneratorInterface $router, UserRepository $userRepository)
    {
        $this->router = $router;
        $this->repository = $userRepository;
    }

    /**
     * This method will pass the returning array to getUser and getCredential methods automatically
     * @param \Symfony\Component\HttpFoundation\Request $request
     * @return array
     */
    public function getCredentials(Request $request)
    {
        return [
            'email' => $request->get('email'),
            'password' => $request->get('password')
        ];
    }

    /**
     * In the case or the Guard and the Authenticator is the same, this method is called just after getCredentials
     * @param mixed $credentials
     * @param \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider
     * @return null|\Symfony\Component\Security\Core\User\UserInterface|void
     */
    public function getUser($credentials, UserProviderInterface $userProvider): UserInterface
    {
        try {
            $email = $credentials['email'];
            $mail = Email::fromString($email);
            $user = $userProvider->loadUserByUsername($mail->toString());

            if ($user instanceof UserInterface) {
                $this->checkCredentials($credentials, $user);
            }

        } catch (InvalidCredentialsException $exception) {
            throw new AuthenticationException();
        }

        return $user;
    }

    /**
     * The ùail has been found, because a user has been identified, we take the has password we have to compare
     * @param mixed $credentials
     * @param \Symfony\Component\Security\Core\User\UserInterface $user
     * @return bool
     */
    public function checkCredentials($credentials, UserInterface $user)
    {
        $mail = Email::fromString($credentials['email']);
        $userCredentials = new Credentials($mail, HashedPassword::fromHash($user->getPassword()));

        // Plain password compared
        $match = $userCredentials->password->match($credentials['password']);

        if (!$match) {
            throw new InvalidCredentialsException();
        }

        return true;
    }

    /**
     * Called when authentication executed and was successful!
     *
     * This should return the Response sent back to the user, like a
     * RedirectResponse to the last page they visited.
     *
     * If you return null, the current request will continue, and the user
     * will be authenticated. This makes sense, for example, with an API.
     *
     * @param \Symfony\Component\HttpFoundation\Request $request
     * @param \Symfony\Component\Security\Core\Authentication\Token\TokenInterface $token
     * @param string $providerKey
     *
     * @return RedirectResponse
     */
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        return new RedirectResponse($this->router->generate(self::SUCCESS_REDIRECT));
    }

    protected function getLoginUrl(): string
    {
        return $this->router->generate(self::LOGIN);
    }

    /**
     * Does the authenticator support the given Request?
     *
     * If this returns false, the authenticator will be skipped.
     *
     * @param Request $request
     *
     * @return bool
     */
    public function supports(Request $request)
    {
        return $request->getPathInfo() === $this->router->generate(self::LOGIN) && $request->isMethod('POST');
    }
}

My Security.yml

security:
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        users:
            id: 'App\Api\Auth\Provider\AuthProvider'
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        login:
            stateless: true
            anonymous: true
            provider: users
            guard:
              entry_point: 'App\User\Auth\Guard\LoginAuthenticator'
              authenticators:
                - 'App\Api\Auth\Guard\LoginAuthenticator'
            form_login:
              login_path: /sign-in
              check_path: sign-in
            logout:
              path: /logout
              target: /
        api:
            pattern: ^/(/user/*|/api|)
            stateless: true
            guard:
                authenticators:
                    - 'App\Api\Auth\Guard\LoginAuthenticator'


    # 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: ^/api, roles: USER }
        - { path: ^/user/*, roles: USER }
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }

我的用户实体

<?php

declare(strict_types = 1);

namespace App\Api\User\Entity;

use App\Domain\User\Repository\Interfaces\CRUDInterface;
use App\Shared\Entity\Traits\CreatedTrait;
use App\Shared\Entity\Traits\DeletedTrait;
use App\Shared\Entity\Traits\EntityNSTrait;
use App\Shared\Entity\Traits\IdTrait;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\Encoder\EncoderAwareInterface;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * @ORM\Table(name="app_users")
 * @ORM\Entity(repositoryClass="App\Api\User\Repository\UserRepository")
 */
class User implements UserInterface, CRUDInterface, \Serializable, EncoderAwareInterface
{
    use IdTrait;
    use CreatedTrait;
    use DeletedTrait;
    use EntityNSTrait;

    /**
     * @ORM\Column(type="string", length=25, unique=false, nullable=true)
     */
    private $username;

    /**
     * @ORM\Column(type="string", length=64)
     */
    private $password;

    /**
     * @ORM\Column(type="string", length=254, unique=true)
     */
    private $email;

    /**
     * @return mixed
     */
    public function getEmail()
    {
        return $this->email;
    }

    /**
     * @param mixed $email
     * @return User
     */
    public function setEmail($email)
    {
        $this->email = $email;

        return $this;
    }

    public function __construct()
    {

    }

    public function getUsername()
    {
        return $this->username;
    }

    public function getSalt()
    {
        // you *may* need a real salt depending on your encoder
        // see section on salt below
        return null;
    }

    public function getPassword()
    {
        return $this->password;
    }

    public function getRoles()
    {
        return array('USER');
    }

    /**
     * From UserInterface
     */
    public function eraseCredentials()
    {
        // Never used ?‡
    }
    /** @see \Serializable::serialize() */
    public function serialize()
    {
        var_dump('need it'); // never called
        return serialize([
            $this->id,
            $this->username,
            $this->email,
            $this->password,
            // see section on salt below
            // $this->salt,
        ]);
    }

    /** @see \Serializable::unserialize() */
    public function unserialize($serialized)
    {
        list (
            $this->id,
            $this->username,
            $this->email,
            $this->password,
            // see section on salt below
            // $this->salt
            ) = unserialize($serialized, ['allowed_classes' => false]);
    }

    /**
     * @param mixed $password
     * @return User
     */
    public function setPassword($password)
    {
        $this->password = $password;

        return $this;
    }

    /**
     * Gets the name of the encoder used to encode the password.
     *
     * If the method returns null, the standard way to retrieve the encoder
     * will be used instead.
     *
     * @return string
     */
    public function getEncoderName()
    {
        return 'bcrypt';
    }
}

这是我关于SF4的第一个项目,它可能是一个愚蠢的错误,但无法找到它。

编辑:我试图将安全配置中的无状态属性传递给false,我的serialize方法被调用但是我在配置文件页面上有一个访问被拒绝错误。

我需要留下无国籍的#34;但它可以帮助您找到解决方案。

1 个答案:

答案 0 :(得分:1)

无状态防火墙永远不会将令牌存储在会话中,因此您必须为每个对API发出的请求传递凭据。

目前,您的防护类会返回重定向,因此您的身份验证会因symfony未存储无状态防火墙令牌而丢失。要解决此问题,您应该在方法null中返回onAuthenticationSuccess,而不是进行重定向。这也意味着,您应该为API防火墙创建一个单独的防护类。

您还可以在symfony文档中找到API的良好示例:https://symfony.com/doc/current/security/guard_authentication.html#step-1-create-the-authenticator-class

编辑:

我有点误解了你想要实现的目标。因此,您似乎希望拥有一个带有symfony的纯REST应用程序,并在您获得可用于将来请求的令牌之后对用户进行身份验证。

前段时间我遇到了同样的问题,我偶然发现了一个名为LexikJWTAuthenticationBundle的非常好的捆绑包。该捆绑包为您提供开箱即用所需的功能。

如果按照Getting started文档进行安装,则应该掌握相关的基础知识。

您的配置应如下所示:

security:
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        users:
            id: 'App\Api\Auth\Provider\AuthProvider'
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        login:
            pattern:  ^/api/sign-in
            stateless: true
            anonymous: true
            form_login:
                check_path:               /api/login_check
                username_parameter: email
                password_parameter: password
                success_handler:          lexik_jwt_authentication.handler.authentication_success
                failure_handler:          lexik_jwt_authentication.handler.authentication_failure
                require_previous_session: false
        api:
            pattern: ^/(/user/*|/api|)
            stateless: true
            anonymous: true
            guard:
                authenticators:
                    - lexik_jwt_authentication.jwt_token_authenticator


    # 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: ^/api, roles: USER }
        - { path: ^/user/*, roles: USER }
        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }

但请勿忘记将login_check路线添加到routes.yml

api_login_check:
    path: /api/login_check

如果一切设置正确,您现在应该可以使用以下请求检索新令牌:

curl -X POST http://localhost/api/login_check -d _username=yourUsername -d _password=yourPassword

此调用收到的令牌应该用于将来对API的所有请求。您可以通过Authorization标题

传递它
curl -H "Authorization: Bearer $YOUR_TOKEN" http://localhost/api/some-protected-route`

如果您想以不同方式传递它(例如通过查询参数),您必须更改此捆绑包的配置:

lexik_jwt_authentication:
    token_extractors:
        query_parameter:
            enabled: true
            name: auth

现在您可以改用https://localhost/api/some-protecte-route?auth=$YOUR_TOKEN

有关此内容的详细信息,请查看此捆绑包的configuration reference

我希望这有助于您开始使用。