我在第一个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;但它可以帮助您找到解决方案。
答案 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
我希望这有助于您开始使用。