Мы разрабатываем REST API, используя symfony 4.2 и API-платформу, и у нас странное поведение.Когда мы выдаем, например, запросы GET HTTP на сгенерированные конечные точки: мы получаем либо ожидаемый ответ JSON, либо ошибку 500 для того же запроса, случайным образом, как кажется.Интересно, что:
- при использовании нашего углового клиента
- случаются случайные ошибки, которые НИКОГДА не отображаются при выдаче команд curl для одних и тех же URL
Здесьэто начало возвращенной трассировки стека JSON при возникновении ошибки:
{
"type": "https://tools.ietf.org/html/rfc2616#section-10",
"title": "An error occurred",
"detail": "The class 'App\\Security\\User\\AppUser' was not found in the chain configured namespaces App\\Entity, Vich\\UploaderBundle\\Entity",
"trace": [
{
"namespace": "",
"short_class": "",
"class": "",
"type": "",
"function": "",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/persistence/lib/Doctrine/Common/Persistence/Mapping/MappingException.php",
"line": 22,
"args": []
},
{
"namespace": "Doctrine\\Common\\Persistence\\Mapping",
"short_class": "MappingException",
"class": "Doctrine\\Common\\Persistence\\Mapping\\MappingException",
"type": "::",
"function": "classNotFoundInNamespaces",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/persistence/lib/Doctrine/Common/Persistence/Mapping/Driver/MappingDriverChain.php",
"line": 87,
"args": [
[
"string",
"App\\Security\\User\\AppUser"
],
[
"array",
[
[
"string",
"App\\Entity"
],
[
"string",
"Vich\\UploaderBundle\\Entity"
]
]
]
]
},
{
"namespace": "Doctrine\\Common\\Persistence\\Mapping\\Driver",
"short_class": "MappingDriverChain",
"class": "Doctrine\\Common\\Persistence\\Mapping\\Driver\\MappingDriverChain",
"type": "->",
"function": "loadMetadataForClass",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php",
"line": 151,
"args": [
[
"string",
"App\\Security\\User\\AppUser"
],
[
"object",
"Doctrine\\ORM\\Mapping\\ClassMetadata"
]
]
},
{
"namespace": "Doctrine\\ORM\\Mapping",
"short_class": "ClassMetadataFactory",
"class": "Doctrine\\ORM\\Mapping\\ClassMetadataFactory",
"type": "->",
"function": "doLoadMetadata",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/persistence/lib/Doctrine/Common/Persistence/Mapping/AbstractClassMetadataFactory.php",
"line": 305,
"args": [
[
"object",
"Doctrine\\ORM\\Mapping\\ClassMetadata"
],
[
"null",
null
],
[
"boolean",
false
],
[
"array",
[]
]
]
},
{
"namespace": "Doctrine\\Common\\Persistence\\Mapping",
"short_class": "AbstractClassMetadataFactory",
"class": "Doctrine\\Common\\Persistence\\Mapping\\AbstractClassMetadataFactory",
"type": "->",
"function": "loadMetadata",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php",
"line": 78,
"args": [
[
"string",
"App\\Security\\User\\AppUser"
]
]
},
{
"namespace": "Doctrine\\ORM\\Mapping",
"short_class": "ClassMetadataFactory",
"class": "Doctrine\\ORM\\Mapping\\ClassMetadataFactory",
"type": "->",
"function": "loadMetadata",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/persistence/lib/Doctrine/Common/Persistence/Mapping/AbstractClassMetadataFactory.php",
"line": 183,
"args": [
[
"string",
"App\\Security\\User\\AppUser"
]
]
},
{
"namespace": "Doctrine\\Common\\Persistence\\Mapping",
"short_class": "AbstractClassMetadataFactory",
"class": "Doctrine\\Common\\Persistence\\Mapping\\AbstractClassMetadataFactory",
"type": "->",
"function": "getMetadataFor",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php",
"line": 283,
"args": [
[
"string",
"App\\Security\\User\\AppUser"
]
]
},
{
"namespace": "Doctrine\\ORM",
"short_class": "EntityManager",
"class": "Doctrine\\ORM\\EntityManager",
"type": "->",
"function": "getClassMetadata",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/doctrine-bundle/Repository/ContainerRepositoryFactory.php",
"line": 45,
"args": [
[
"string",
"App\\Security\\User\\AppUser"
]
]
},
{
"namespace": "Doctrine\\Bundle\\DoctrineBundle\\Repository",
"short_class": "ContainerRepositoryFactory",
"class": "Doctrine\\Bundle\\DoctrineBundle\\Repository\\ContainerRepositoryFactory",
"type": "->",
"function": "getRepository",
"file": "/home/beta/www/cel2-dev/cel2-services/vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php",
"line": 713,
"args": [
[
"object",
"Doctrine\\ORM\\EntityManager"
],
[
"string",
"App\\Security\\User\\AppUser"
]
]
}
...
}
Для простоты мы реализовали фиктивный объект AbstractGuardAuthenticator, который всегда возвращает пользователя с жестким кодом и демонстрирует то же поведение.Ниже следует его код, код класса пользовательского объекта вместе с интересными битами конфигурации безопасности:
<?php
namespace App\Security;
use App\Security\User\AppUser;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class YesAuthenticator extends AbstractGuardAuthenticator
{
public function supports(Request $request)
{
return true;
}
public function getCredentials(Request $request)
{
return array(
'token' => 'sdflsdklfjsdlkfjslkdfjsldk46541qsdf',
);
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$user = new AppUser(22, 'toto@wanadoo.fr', 'toto', 'litoto', 'tl', 'totoL', '', array(), null);
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$data = array(
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
);
return new JsonResponse($data, Response::HTTP_FORBIDDEN);
}
public function start(Request $request, AuthenticationException $authException = null)
{
$data = array(
'message' => 'Authentication Required'
);
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
public function supportsRememberMe()
{
return false;
}
}
Класс пользовательского объекта:
<?php
namespace App\Security\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;
/**
* Represents a user logged in the SSO authentication system.
*
* @package App\Security\User
*/
class AppUser implements UserInterface, EquatableInterface {
private $id;
private $email;
private $pseudo;
private $avatar;
private $surname;
private $lastName;
private $usePseudo;
private $administeredProjectId;
private $roles;
const ADMIN_ROLE = "administrator";
public function __construct(
$id, $email, $surname, $lastName, $pseudo, $usePseudo, $avatar,
array $roles, $administeredProjectId) {
$this->id = $id;
$this->email = $email;
$this->surname = $surname;
$this->lastName = $lastName;
$this->pseudo = $pseudo;
$this->usePseudo = $usePseudo;
$this->avatar = $avatar;
$this->administeredProjectId = $administeredProjectId;
$this->roles = $roles;
}
public function setId($idd) {
$this->id = $idd;
}
public function getId() {
return $this->id;
}
public function isAdmin() {
return in_array(AppUser::ADMIN_ROLE, $this->roles);
}
public function isProjectAdmin() {
return (!is_null($this->administeredProjectId));
}
public function isLuser() {
return (
!( $this->isAdmin() ) ||
( $this->isProjectAdmin() ) );
}
public function getRoles() {
return $this->roles;
}
public function getSurname() {
return $this->surname;
}
public function getLastName() {
return $this->lastName;
}
public function getAvatar() {
return $this->avatar;
}
public function getAdministeredProjectId() {
return $this->administeredProjectId;
}
public function getPassword() {
return null;
}
public function getSalt() {
return null;
}
public function getEmail() {
return $this->email;
}
public function getUsername() {
return $this->usePseudo ? $this->pseudo : ($this->surname . ' ' . $this->lastName);
}
public function getPseudo() {
return $this->pseudo;
}
public function eraseCredentials() {
}
public function isEqualTo(UserInterface $user) {
if (!$user instanceof AppUser) {
return false;
}
if ($this->username !== $user->getUsername()) {
return false;
}
return true;
}
}
Извлечение безопасности.yaml file:
security:
providers:
user:
entity:
class: App\Security\User\AppUser
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: true
guard:
authenticators:
- App\Security\YesAuthenticator
main:
anonymous: ~
logout: ~
guard:
authenticators:
- App\Security\YesAuthenticator
Вот пример сущности / ресурса, для которого веб-сервисы демонстрируют поведение.Ничего особенного ...
<?php
namespace App\Entity;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Annotation\ApiResource;
/**
* @ORM\Entity
* @ORM\Table(name="tb_project")
* @ApiResource(attributes={
* "normalization_context"={"groups"={"read"}},
* "formats"={"jsonld", "json"},
* "denormalization_context"={"groups"={"write"}}},
* collectionOperations={"get"},
* itemOperations={"get"}
* )
*/
class Project
{
/**
* @Groups({"read"})
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
* @ORM\Column(type="integer")
*/
private $id = null;
/**
* @Groups({"read"})
* @ORM\OneToOne(targetEntity="Project")
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id")
*/
private $parent;
/**
* @Assert\NotNull
* @Groups({"read"})
* @ORM\Column(type="string", nullable=false)
*/
private $label = null;
/**
*
* @Assert\NotNull
* @Groups({"read"})
* @ORM\Column(name="is_private", type="boolean", nullable=false)
*/
private $isPrivate = true;
public function getId(): ?int {
return $this->id;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): self
{
$this->label = $label;
return $this;
}
public function getIsPrivate(): ?bool
{
return $this->isPrivate;
}
public function setIsPrivate(bool $isPrivate): self
{
$this->isPrivate = $isPrivate;
return $this;
}
public function getParent(): ?self
{
return $this->parent;
}
public function setParent(?self $parent): self
{
$this->parent = $parent;
return $this;
}
public function __clone() {
if ($this->id) {
$this->id = null;
}
}
public function __toString()
{
return $this->getLabel();
}
}
Обратите внимание, что у нас есть другие веб-службы с настраиваемыми поставщиками данных, которые фильтруют возвращаемые данные на основе текущего пользователя.Недетерминированным способом запросы к ним также иногда терпят неудачу, потому что текущий пользователь является нулем (в то время как пользователь действительно зарегистрирован).