grzegorz-jamroz/sf-doctrine-api-auth-bundle

此包的最新版本(v6.4.0)没有提供许可证信息。

此扩展包为 Symfony Doctrine Api 包提供授权

v6.4.0 2023-12-14 17:26 UTC

This package is auto-updated.

Last update: 2024-10-01 00:11:02 UTC


README

此扩展包使用 JWT 和刷新令牌为 Symfony Doctrine Api 包提供授权

Code Coverage Code Coverage Release Version

安装

composer require grzegorz-jamroz/sf-doctrine-api-auth-bundle
  1. 更新项目中路由配置
# config/routes.yaml
controllers:
    resource: ../src/Controller/
    type: attribute

# ...

# add those lines:
ifrost_doctrine_api_controllers:
    resource: ../src/Controller/
    type: doctrine_api_attribute
    
login:
  path: /login

ifrost_doctrine_api_auth:
  resource: Ifrost\DoctrineApiAuthBundle\Routing\DoctrineApiAuthLoader
  type: service
# ...
  1. 配置 Doctrine 将 UUID 存储为二进制字符串
# config/packages/doctrine.yaml
doctrine:
    dbal:
        types:
            uuid_binary:  Ramsey\Uuid\Doctrine\UuidBinaryType
# Uncomment if using doctrine/orm <2.8
        # mapping_types:
            # uuid_binary: binary

注意:您可以将 Doctrine 配置为以不同方式存储 UUID,您可以在此处了解相关信息 这里。请注意,只有以二进制类型存储 UUID 时,此扩展包才能正常工作。

  1. 创建一个实现 ApiUserInterface 的用户实体

示例

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Ifrost\DoctrineApiAuthBundle\Entity\ApiUserInterface;
use PlainDataTransformer\Transform;
use Ramsey\Uuid\Doctrine\UuidV7Generator;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

#[ORM\Entity(readOnly: true)]
class User implements ApiUserInterface
{
    #[ORM\Id]
    #[ORM\Column(type: "uuid_binary", unique: true)]
    #[ORM\GeneratedValue(strategy: "CUSTOM")]
    #[ORM\CustomIdGenerator(class: UuidV7Generator::class)]
    private UuidInterface $uuid;

    #[ORM\Column(length: 180, unique: true)]
    private string $email;

    /**
     * @var string The hashed password
     */
    #[ORM\Column]
    private string $password;

    /**
     * @var array<int, string>
     */
    #[ORM\Column]
    private array $roles;

    public function __construct(
        UuidInterface $uuid,
        string $email,
        string $password = '',
        array $roles = [],
    ) {
        $this->uuid = $uuid;
        $this->email = $email;
        $this->password = $password;
        $this->roles = $roles;
    }

    public function getUuid(): UuidInterface
    {
        return $this->uuid;
    }

    public function getEmail(): string
    {
        return $this->email;
    }

    public function getUsername(): string
    {
        return $this->getEmail();
    }

    /**
     * A visual identifier that represents this user.
     *
     * @see UserInterface
     */
    public function getUserIdentifier(): string
    {
        return $this->email;
    }

    /**
     * @see UserInterface
     */
    public function getRoles(): array
    {
        $roles = $this->roles;
        // guarantee every user at least has ROLE_USER
        $roles[] = 'ROLE_USER';

        return array_unique($roles);
    }

    /**
     * @see PasswordAuthenticatedUserInterface
     */
    public function getPassword(): string
    {
        return $this->password;
    }

    /**
     * @see UserInterface
     */
    public function eraseCredentials(): void
    {
        // If you store any temporary, sensitive data on the user, clear it here
        // $this->plainPassword = null;
    }

    public static function getTableName(): string
    {
        return 'user';
    }

    /**
     * @return array<int, string>
     */
    public static function getFields(): array
    {
        return [
            ...array_keys(self::createFromArray([])->jsonSerialize()),
            'password',
        ];
    }

    public function jsonSerialize(): array
    {
        return [
            'uuid' => (string) $this->uuid,
            'email' => $this->email,
            'roles' => $this->getRoles(),
        ];
    }

    public function getWritableFormat(): array
    {
        return [
            ...$this->jsonSerialize(),
            'uuid' => $this->uuid->getBytes(),
            'password' => $this->password,
            'roles' => json_encode($this->getRoles()),
        ];
    }
    
        public static function createFromArray(array $data): static|self
    {
        return new self(
            $data['uuid'] ?? Uuid::uuid7(),
            Transform::toString($data['email'] ?? ''),
            Transform::toString($data['password'] ?? ''),
            Transform::toArray($data['roles'] ?? []),
        );
    }

    public static function createFromRequest(array $data): static|self
    {
        return new self(
            isset($data['uuid']) ? Uuid::fromString($data['uuid']) : Uuid::uuid7(),
            Transform::toString($data['email'] ?? ''),
            Transform::toString($data['password'] ?? ''),
            Transform::toArray($data['roles'] ?? []),
        );
    }
}
  1. 创建一个实现 TokenInterface 的令牌实体

示例

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Ifrost\DoctrineApiAuthBundle\Entity\TokenInterface;
use PlainDataTransformer\Transform;
use Ramsey\Uuid\Doctrine\UuidV7Generator;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;

#[ORM\Entity(readOnly: true)]
class Token implements TokenInterface
{
    #[ORM\Id]
    #[ORM\Column(type: "uuid_binary", unique: true)]
    #[ORM\GeneratedValue(strategy: "CUSTOM")]
    #[ORM\CustomIdGenerator(class: UuidV7Generator::class)]
    private string $uuid;

    #[ORM\ManyToOne(targetEntity: User::class)]
    #[ORM\JoinColumn(name: 'user_uuid', referencedColumnName: 'uuid', nullable: false)]
    private string $userUuid;
    
    #[ORM\Column(type: "uuid_binary", unique: true)]
    private string $refreshTokenUuid;

    #[ORM\Column]
    private int $iat;

    #[ORM\Column]
    private int $exp;

    #[ORM\Column(length: 255, nullable: true)]
    private string $device;

    public function __construct(
        UuidInterface $uuid,
        UuidInterface $userUuid,
        UuidInterface $refreshTokenUuid,
        int $iat,
        int $exp,
        string $device,
    ) {
        $this->uuid = $uuid;
        $this->userUuid = $userUuid;
        $this->refreshTokenUuid = $refreshTokenUuid;
        $this->iat = $iat;
        $this->exp = $exp;
        $this->device = $device;
    }

    public function getUuid(): UuidInterface
    {
        return $this->uuid;
    }

    public function getUserUuid(): UuidInterface
    {
        return $this->userUuid;
    }
    
    public function getRefreshTokenUuid(): UuidInterface
    {
        return $this->refreshTokenUuid;
    }

    public function getIat(): int
    {
        return $this->iat;
    }

    public function getExp(): int
    {
        return $this->exp;
    }

    public function getDevice(): string
    {
        return $this->device;
    }

    public static function getTableName(): string
    {
        return 'token';
    }

    /**
     * @return array<int, string>
     */
    public static function getFields(): array
    {
        return array_keys(self::createFromArray([])->jsonSerialize());
    }

    public function jsonSerialize(): array
    {
        return [
            'uuid' => (string) $this->uuid,
            'user_uuid' => (string) $this->userUuid,
            'refresh_token_uuid' => (string) $this->refreshTokenUuid,
            'iat' => $this->iat,
            'exp' => $this->exp,
            'device' => $this->device,
        ];
    }

    public function getWritableFormat(): array
    {
        return [
            ...$this->jsonSerialize(),
            'uuid' => $this->uuid->getBytes(),
            'user_uuid' => $this->userUuid->getBytes(),
            'refresh_token_uuid' => $this->refreshTokenUuid->getBytes(),
        ];
    }
    
    public static function createFromArray(array $data): static|self
    {
        return new self(
            $data['uuid'] ?? Uuid::uuid7(),
            $data['user_uuid'] ?? Uuid::uuid7(),
            $data['refresh_token_uuid'] ?? Uuid::uuid7(),
            Transform::toInt($data['iat'] ?? 0),
            Transform::toInt($data['exp'] ?? 0),
            Transform::toString($data['device'] ?? ''),
        );
    }

    public static function createFromRequest(array $data): static|self
    {
        return new self(
            isset($data['uuid']) ? Uuid::fromString($data['uuid']) : Uuid::uuid7(),
            isset($data['user_uuid']) ? Uuid::fromString($data['user_uuid']) : Uuid::uuid7(),
            isset($data['refresh_token_uuid']) ? Uuid::fromString($data['refresh_token_uuid']) : Uuid::uuid7(),
            Transform::toInt($data['iat'] ?? 0),
            Transform::toInt($data['exp'] ?? 0),
            Transform::toString($data['device'] ?? ''),
        );
    }
}
  1. 创建 config/packages/ifrost_doctrine_api_auth.yaml 文件并添加
# config/packages/ifrost_doctrine_api_auth.yaml
ifrost_doctrine_api_auth:
  token_entity: 'App\Entity\Token'
  user_entity: 'App\Entity\User'
  1. 生成 SSL 密钥 来源
php bin/console lexik:jwt:generate-keypair
  1. 更新安全配置 来源

示例

# config/packages/security.yaml
security:
  # ...
  # https://symfony.com.cn/doc/current/security.html#registering-the-user-hashing-passwords
  password_hashers:
    Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
  # https://symfony.com.cn/doc/current/security.html#loading-the-user-the-user-provider
  providers:
    # used to reload user from session & other features (e.g. switch_user)
    app_user_provider:
      entity:
        class: App\Entity\User
        property: email
  firewalls:
    # ...
    login:
      pattern: ^/login
      stateless: true
      json_login:
        check_path: /login
        success_handler: lexik_jwt_authentication.handler.authentication_success
        failure_handler: lexik_jwt_authentication.handler.authentication_failure
    refresh:
      pattern: ^/token/refresh
      stateless: true
    logout:
      pattern: ^/logout
      stateless: true
    api:
      pattern: ^/
      stateless: true
      jwt: ~
    main:
      lazy: true
      provider: app_user_provider
    # ...
  # 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: ^/login,          roles: PUBLIC_ACCESS }
    - { path: ^/token/refresh,  roles: PUBLIC_ACCESS }
    - { path: ^/logout,         roles: PUBLIC_ACCESS }
    - { path: ^/,               roles: IS_AUTHENTICATED_FULLY }
  # ...
  1. 针对 Apache 用户的注意事项

Apache 服务器会删除任何不在有效 HTTP BASIC AUTH 格式中的授权头。更多信息请参阅 这里。要解决这个问题,请将以下规则添加到您的虚拟主机配置中

SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
  1. 配置您的 Symfony 应用数据库 来源
  • 在您的 .env 文件中配置数据库
    # .env file
    DATABASE_URL="mysql://db_username:password@127.0.0.1:3306/db_name?serverVersion=mariadb-10.6.11&charset=utf8mb4"
    
  • 如果尚未创建,则创建数据库
    php bin/console doctrine:database:create
    
  • 如果没有安装,请安装 symfony/orm-packsymfony/maker-bundle
    composer require symfony/orm-pack
    composer require symfony/maker-bundle --dev
    
  • 创建迁移
    php bin/console make:migration
    
  • 运行迁移
    php bin/console doctrine:migrations:migrate
    
  1. 清除缓存
php bin/console cache:clear
  1. 现在您可以调试您的路由了。运行以下命令
php bin/console debug:router

您应该得到以下输出

 ------------------- -------- -------- ------ --------------------------
  Name                Method   Scheme   Host   Path
 ------------------- -------- -------- ------ --------------------------
  _preview_error      ANY      ANY      ANY    /_error/{code}.{_format}
  login               ANY      ANY      ANY    /login
  logout              POST     ANY      ANY    /logout
  refresh_token       POST     ANY      ANY    /token/refresh
 ------------------- -------- -------- ------ --------------------------
  1. 创建 UserController
<?php

declare(strict_types=1);

namespace App\Controller;

use App\Entity\User;
use Ifrost\ApiFoundation\Attribute\Api;
use Ifrost\ApiFoundation\Enum\Action;
use Ifrost\DoctrineApiBundle\Controller\DoctrineApiController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route;

#[Api(entity: User::class, path: 'users', excludedActions: [Action::CREATE])]
class UserController extends DoctrineApiController
{
    #[Route('/users', name: 'users_create', methods: ['POST'])]
    public function create(): Response
    {
        $data = $this->getApiRequest(User::getFields());
        $data['password'] = $this->getPasswordHasher()->hashPassword(
            User::createFromArray($data),
            $data['password']
        );
        $this->getApiRequestService()->setData($data);

        return $this->getApi()->create();
    }
    
    public static function getSubscribedServices(): array
    {
        return array_merge(parent::getSubscribedServices(), [
            UserPasswordHasherInterface::class => '?' . UserPasswordHasherInterface::class,
        ]);
    }
    
    protected function getPasswordHasher(): UserPasswordHasherInterface
    {
        $passwordHasher = $this->container->get(UserPasswordHasherInterface::class);
        $passwordHasher instanceof UserPasswordHasherInterface ?: throw new \RuntimeException(sprintf('Container identifier "%s" is not instance of %s', UserPasswordHasherInterface::class, UserPasswordHasherInterface::class));

        return $passwordHasher;
    }
}
  1. 现在您可以调试您的路由了。运行以下命令
php bin/console debug:router

您应该得到以下输出

 ---------------- -------- -------- ------ --------------------------
  Name             Method   Scheme   Host   Path
 ---------------- -------- -------- ------ --------------------------
  _preview_error   ANY      ANY      ANY    /_error/{code}.{_format}
  users_create     POST     ANY      ANY    /users
  users_find       GET      ANY      ANY    /users
  users_find_one   GET      ANY      ANY    /users/{uuid}
  users_update     PUT      ANY      ANY    /users/{uuid}
  users_modify     PATCH    ANY      ANY    /users/{uuid}
  users_delete     DELETE   ANY      ANY    /users/{uuid}
  login            ANY      ANY      ANY    /login
  logout           POST     ANY      ANY    /logout
  token_refresh    POST     ANY      ANY    /token/refresh
 ---------------- -------- -------- ------ --------------------------
  1. 临时将路由 users_create 设置为公开,以便测试用户
# config/packages/security.yaml
security:
  enable_authenticator_manager: true
  # ...
  firewalls:
    # ...
    user:
      pattern: ^/user
      stateless: true
    # ...
   # ...
  access_control:
    # ...
    - { route: 'users_create',  roles: PUBLIC_ACCESS }
    # ...
  # ...
  1. 创建测试用户
curl -i -X POST -d '{"email":"test_user@email.com", "password":"top-secret", "roles":["ROLE_ADMIN"]}' http://your-domain.com/users
  1. config/packages/security.yaml 重置到第 12 点之前的状态

用法

登录 / 获取令牌

curl -i -X POST -d '{"username":"test_user@email.com","password":"top-secret"}' -H "Content-Type: application/json" http://your-domain.com/login

示例响应(如果启用 return_refresh_token_in_body 配置参数)

{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE2NzI4NDgzOTQsImV4cCI6MTY3Mjg1MTk5NCwicm9sZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJ1c2VybmFtZSI6InRlc3RfdXNlckBlbWFpbC5jb20iLCJ1dWlkIjoiNDVjZjZiOGMtYmJkZi00ZDNlLWI5YWEtN2IyZTY2YTkwM2JmIiwiZGV2aWNlIjpudWxsfQ.FvE0FCRHqwuxDbw-i7mnIu2gYbHAof4mTEnKSWdAy-C9lzpSkMKNCZ01JLGASKYSDur2YJoTujxQZRdtKOyyzwl2hX2_jOstJ0lagdMHXncgAPfaYUurwczAUkjxSeTbikkOLU1afE86RaJl1jr3vB7fJRt1z3JE_enqpwAuFdNhz8JaneoRKG7onEZa6TY-asfSwnVKvTjKSNlE8-54yzgvCKRFZxyhHdI0EuO3mOq_Sx1IOnFdwjx2s3vTLQD1pQl-GMgHy3izyviWu0_VVkifZyh36GEfj2x3Gl0dUOdTXBzqFWgHiPAVFTIAiQU60ETA3WASuU-M3x9R44GqCg",
  "refreshToken": "9ddcc1e382ab8773a0da843b7b5ee3f369b672ff1d46bc5fb0add51de37e054af4024a75689947ed1055689902bc859bf9680740b8a1a954fed1066448a837ea61653866323162612d386435342d343330612d616433362d376539323735376134666363"
}

获取刷新令牌

curl -i -X POST http://127.0.0.1:8000/token/refresh -H "Content-Type: application/json" -H "Authorization: Bearer PLACE_FOR_TOKEN" -d '{"refreshToken":"PLACE_FOR_REFRESH_TOKEN"}' -b XDEBUG_SESSION=PHPSTORM

或者如果配置参数 cookie 启用

curl -i -X POST http://127.0.0.1:8000/token/refresh -H "Content-Type: application/json" -H "Authorization: Bearer PLACE_FOR_TOKEN" -b XDEBUG_SESSION=PHPSTORM -b refreshToken=PLACE_FOR_REFRESH_TOKEN

示例响应

{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE2NzI4OTkzMDMsImV4cCI6MTY3MjkwMjkwMywicm9sZXMiOlsiUk9MRV9VU0VSIl0sInVzZXJuYW1lIjoidGVzdF91c2VyQGVtYWlsLmNvbSIsInV1aWQiOiI0NDgxOGVlZi1hZjAzLTQ0OTMtOTBjOC01NzkwMzcyNmRhMDIiLCJkZXZpY2UiOm51bGx9.Gswbkzc4pCRkavs7NHPUVgqEFeESWHr7kITbhDP35YOiesTlAKUgcr2U6Dc8s5McNqmyU05bwR3brnTLu9NY1FIhxXA2MwLw-bl75SepHAkKzx9ASdzJ_peXnwbYiHk2p50GyYmTzJzfxY3g921KnJr0SXz6VVl-Xg3kO5ccWR95F3FRzyWZU_JL6Ye8APtHWxGzl6lHxKko9pUb9xdpcgkKPvospLciuH3REz5mdSAs8xxErYeRMWEZl8BBzmAkj0bnVadL3EmliGWnQkG9HgzobE2NePQZH-w5blaZfU3To8AGgwU3O1yIUCCyV8vL1etPltXysx81d0I6gKs9Dw",
  "refreshToken": "r8sYCwAz1MIMKVrScHZ2rmB4Uqul9T_32IMc9MYIEm2BbE2TTzcZ5QmdTixNJWTHbhCySt2Kzj0CTZajtrmMKNgp22i1jYPj.p2lII8MgnTJgYVTDMQGZiRMGU1UDOZiQkd1JVFjgm9MgM9Du1zTO2hzZ4MCqZbOX4eiS2Y5rOXzyMXv-TOUrMN_IbXWNcU7gD3VNzCmZS3cJmVeRZwMcAbPNCBzWDJYko0N1ZizMOSMwh0M2xicjtzDkM2T9i3yF9YMNkKS5Q2NHMMilNEtMTozUOcd9QQGFYYBOcRiLJMA4FO3p5sY0Hh1vYdXY2ygWzexKr27MgC1LgNdMU-0ZOvP2IUskN18CqUYHbVSK2HI9SDTMi2jrKExcxTxRLDZYLsxNwVgczQDPkRZ8FPO4MTHm8kllE5SjMckqGjN0MMM2S1ZF.wz1eJzVsCLzSe22AVODQwXzDjFjMD6UQ3Mu9z0Yv34X11zIZO3H1dMe8zVm2tDDqMmZA87YMszM0UelYliJMJPNR2sDUZN86fk2J3dJF0bZ2mOiA7ykdNOATDv0fXpJ35wYzEYIj_jMVdl-WZM2z8GQzIlRMREIjf3LQEMkagBDizft0OzmugQxM1QM32MITQyRoiEnjzJ4qe9i1BCMyl12ySjkEQqGSD-ghy29T7qGiTZ0l7Ic7FMcKHyFc4HWNiYAgS1j7jjwzMkzMjB"
}

配置

您可以在 此处 找到默认配置。