Аутентификация SF4 работает, но токен не сохранен (сериализация никогда не вызывается) - PullRequest
0 голосов
/ 31 мая 2018

У меня проблемы с аутентификацией с помощью symfony 4 на моем первом REST API.

Дело в том, что моя аутентификация прошла успешно, и затем мой URL перенаправления вызывается, но маркер аутентификации теряется во время этого перенаправления.Я также заметил, что мой метод сериализации никогда не вызывается на моем пользовательском объекте.

Что я хочу, так это: Когда моя аутентификация прошла успешно, вызывается страница моего профиля.Но с этим кодом все, что я получаю, это перенаправление 302 из профиля, что означает, что моя аутентификация работает, но токен был потерян (если он существует, его никогда не видели)

Мои единственные подсказки:

  • Метод сериализации в User никогда не вызывался (это важно?) РЕДАКТИРОВАТЬ: нет, потому что мне нужно быть без сохранения состояния, поэтому удалите эти методы.
  • Моя аутентификация работает, потому что если я сделаю ошибку в учетных данных Iполучил правильную ошибку.

Вот код:

Мой провайдер

<?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');
    }
}

Моя безопасность.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, мой метод сериализации был вызван, но у меня возникла ошибка доступа к странице профиля.

Мне нужно остаться "без состояния"«но это может помочь вам найти решение.

1 Ответ

0 голосов
/ 31 мая 2018

Брандмауэр без сохранения состояния никогда не будет хранить токен в сеансе, поэтому вы должны передавать учетные данные для каждого запроса в API.

В настоящее время ваш класс защиты возвращает перенаправление, поэтому ваша аутентификация теряетсяиз-за того, что symfony не хранит токен для брандмауэров без сохранения состояния.Чтобы решить эту проблему, вы должны вернуть null в методе onAuthenticationSuccess вместо перенаправления.Это также означает, что вы должны создать отдельный класс защиты для брандмауэра API.

Вы также можете найти хороший пример защиты для API в документации Symfony: https://symfony.com/doc/current/security/guard_authentication.html#step-1-create-the-authenticator-class

Edit:

Я немного неправильно понял, чего вы пытаетесь достичь.Таким образом, кажется, что вы хотите иметь чистое REST-приложение с symfony и один раз аутентифицировать пользователя, где вы затем получите токен, который можно использовать для будущих запросов.

Некоторое время назад у меня возникла та же проблема, и янаткнулся на очень хороший комплект под названием LexikJWTAuthenticationBundle .Этот комплект предоставляет вам необходимые функции из коробки.

Если вы установите его, следуя документации Начало работы , у вас должны быть для этого основы.

Ваша конфигурация должна выглядеть примерно так:

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 вместо.

Для получения дополнительной информации об этом взгляните на ссылку на конфигурацию этого комплекта

Надеюсь, это немного поможетнемного, чтобы вы начали с.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...