【问题标题】:Api-platform, JWT token and endpoints sending back data owned by the identified userApi 平台、JWT 令牌和端点发回已识别用户拥有的数据
【发布时间】:2022-10-24 16:28:27
【问题描述】:

我正在使用带有 JWT 令牌(通过 LexikJWTAuthenticationBundle)的 API 平台的 PHP symfony,截至今天的最新版本。

我已经阅读了很多东西,并且我知道如何做基本的事情:

  • 创建一个公开我的实体的 API,
  • 使用 JWT 保护某些端点
  • 使用 user_roles 保护某些端点

我现在要做的是让 API 只发回属于用户的数据,而不是简单地发回数据库中包含并由实体表示的所有内容。我的工作以此为基础,但这没有考虑 JWT 令牌,我不知道如何在 UserFilter 类中使用令牌:https://api-platform.com/docs/core/filters/#using-doctrine-orm-filters

这是我的书实体:

<?php
// api/src/Entity/Book.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\GetCollection;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use App\Entity\User;
use App\Attribute\UserAware;


/** A book. */
#[ORM\Entity]
#[ApiResource(operations: [
    new Get(),
    new GetCollection(),
    new Post(),
    new Put(),
    new Patch(),
    new Delete()
])]

#[UserAware(userFieldName: "id")]
class Book
{
    /** The id of this book. */
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    private ?int $id = null;

    /** The ISBN of this book (or null if doesn't have one). */
    #[ORM\Column(nullable: true)]
    #[Assert\Isbn]
    public ?string $isbn = null;

    /** The title of this book. */
    #[ORM\Column]
    #[Assert\NotBlank]
    public string $title = '';

    /** The description of this book. */
    #[ORM\Column(type: 'text')]
    #[Assert\NotBlank]
    public string $description = '';

    /** The author of this book. */
    #[ORM\Column]
    #[Assert\NotBlank]
    public string $author = '';

    /** The publication date of this book. */
    #[ORM\Column(type: 'datetime')]
    #[Assert\NotNull]
    public ?\DateTime $publicationDate = null;

    /** @var Review[] Available reviews for this book. */
    #[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'book', cascade: ['persist', 'remove'])]
    public iterable $reviews;

    #[ORM\Column(length: 255, nullable: true)]
    private ?string $publisher = null;

    /** The book this user is about. */
    #[ORM\ManyToOne(inversedBy: 'books')]
    #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id')]
    #[Assert\NotNull]
    public ?User $user = null;

    public function __construct()
    {
        $this->reviews = new ArrayCollection();
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getPublisher(): ?string
    {
        return $this->publisher;
    }

    public function setPublisher(?string $publisher): self
    {
        $this->publisher = $publisher;

        return $this;
    }
}

这是我的 UserFilter 类:

<?php
// api/src/Filter/UserFilter.php

namespace App\Filter;

use App\Attribute\UserAware;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use App\Entity\User;

final class UserFilter extends SQLFilter
{
    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
    {
        // The Doctrine filter is called for any query on any entity
        // Check if the current entity is "user aware" (marked with an attribute)
        $userAware = $targetEntity->getReflectionClass()->getAttributes(UserAware::class)[0] ?? null;

        $fieldName = $userAware?->getArguments()['userFieldName'] ?? null;
        if ($fieldName === '' || is_null($fieldName)) {
            return '';
        }

        try {
            $userId = $this->getParameter('id');
            // Don't worry, getParameter automatically escapes parameters
        } catch (\InvalidArgumentException $e) {
            // No user id has been defined
            return '';
        }

        if (empty($fieldName) || empty($userId)) {
            return '';
        }

        return sprintf('%s.%s = %s', $targetTableAlias, $fieldName, $userId);
    }
}

这是我的 UserAware 类:

<?php
// api/Annotation/UserAware.php

namespace App\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
final class UserAware
{
    public $userFieldName;
}

我将此添加到我的 config/packages/api_platform.yaml 文件中:

doctrine:
    orm:
        filters:
            user_filter:
                class: App\Filter\UserFilter
                enabled: true

它显然不起作用,因为我没有在 JWT 令牌和过滤器之间架起一座桥梁,但我不知道该怎么做。我错过了什么? 我目前的结果是 GET /api/books 发回了存储在数据库中的所有书籍,而不是只发送属于 JWT 身份验证用户的书籍。

【问题讨论】:

    标签: symfony jwt api-platform.com


    【解决方案1】:

    您可以使用 Doctrine Extension 作为described here,而不是 Doctrine Filter。 在您的情况下,它需要:

    1. 创建学说扩展:
      <?php
      // api/src/Doctrine/CurrentUserExtension.php
      
      namespace AppDoctrine;
      
      use ApiPlatformDoctrineOrmExtensionQueryCollectionExtensionInterface;
      use ApiPlatformDoctrineOrmExtensionQueryItemExtensionInterface;
      use ApiPlatformDoctrineOrmUtilQueryNameGeneratorInterface;
      use ApiPlatformMetadataOperation;
      use AppEntityBook;
      use DoctrineORMQueryBuilder;
      use SymfonyComponentSecurityCoreSecurity;
      
      final class CurrentUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
      {
          private $security;
      
          public function __construct(Security $security)
          {
              $this->security = $security;
          }
      
          public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
          {
              $this->addWhere($queryBuilder, $resourceClass);
          }
      
          public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, Operation $operation = null, array $context = []): void
          {
              $this->addWhere($queryBuilder, $resourceClass);
          }
      
          private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void
          {
              if (Book::class !== $resourceClass || $this->security->isGranted('ROLE_ADMIN') || null === $user = $this->security->getUser()) {
                  return;
              }
      
              $rootAlias = $queryBuilder->getRootAliases()[0];
              $queryBuilder->andWhere(sprintf('%s.user = :current_user', $rootAlias));
              $queryBuilder->setParameter('current_user', $user->getId());
          }
      }
      

      主要逻辑在addWhere()方法中:

      • 仅在您处理 Book 实体时适用(但您可以将想法扩展到此处的实体列表)
      • 检查用户是否被授予管理员权限(如果是,则跳过扩展,允许管理员获取所有书籍)
      • 如果用户未通过身份验证,则跳过(您应该使用端点中的防火墙或安全权限来阻止此访问)

      然后它向 SQL 查询添加 where 条件以按 userId(或您需要的任何其他条件)进行过滤

      1. 不要忘记启用您的过滤器:
      # api/config/services.yaml
      services:
      
          # ...
      
          'AppDoctrineCurrentUserExtension':
              tags:
                  - { name: api_platform.doctrine.orm.query_extension.collection }
                  - { name: api_platform.doctrine.orm.query_extension.item }
      

    【讨论】:

    • 嘿,非常感谢它完美地工作。只有一个很小的语法错误: public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void; ===> public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void 非常清楚的信息,我可以让我的API非常灵活从现在开始。
    • 不错:) 错字已修复!
    猜你喜欢
    • 2019-05-18
    • 2016-04-06
    • 2021-10-27
    • 2021-08-12
    • 2018-07-18
    • 2018-05-14
    • 1970-01-01
    • 2023-01-19
    • 1970-01-01
    相关资源
    最近更新 更多