如何让 Symfony 应用程序使用来自其他 Symfony 应用程序的身份验证和授权?

How to make a Symfony apps use the authentication and authorization from other Symfony apps?

有什么方法可以从不同域上的另一个 symfony 应用程序对 symfony 中的用户进行身份验证和授权吗?

这是我的场景,我有一个 symfony 应用程序,主要用作域 example1.dev 上的用户相关服务(用户、角色、学生姓名等),该站点提供 api 端点使用 JWTLexicBundle 登录(在 example1.dev/api/authentication 上),并提供端点以在 eample1.dev/api/whoami 上获取学生数据(角色等),方法是向该端点发送有效的 JWT 令牌 Header。来自 example1.dev 的 JWT 令牌被用作各种前端站点上的身份验证令牌(example3.dev、example4.dev 构建在 top react 上)

然后,我在 example2.dev.

上使用了第二个用于 class 且没有 user/authentication 方法的 symfony 应用程序

如何使用要在 example2.dev 实施的 example1.dev 用户数据和角色?我的意思是在 example2.dev 上使用 example1.dev api 服务进行身份验证和授权,例如检查请求是否提供 JWT Token,如果是,请检查 example1.dev token 是否有效,如果有效,从 example1.dev 获取用户数据。这可能吗?

绝对有可能。让我们看一下可能的实现,希望它能成为设计解决方案的良好起点,根据您的要求量身定制。在第一个 Symfony 应用程序(我们称之为用户服务)中,我们将具有登录功能以将凭据交换为 JWT 令牌、刷新 JWT 令牌等。在获得 JWT 令牌后,用户能够调用其他服务并使用智威汤逊令牌。在其他服务上,我们需要解码 JWT 令牌(它将检查它是否有效且未过期)。为此,我们应该在所有服务中都依赖 LexikJWTAuthenticationBundle,但配置不同。对于用户服务,我们将同时拥有 public 和密钥来生成 JWT 令牌并验证它,而其他服务只需要 public 密钥来验证 JWT 令牌并将其解码为读取有效负载。

用户服务config.yml配置。

# JWT Configuration
lexik_jwt_authentication:
    secret_key:          '%jwt_private_key%'
    public_key:          '%jwt_public_key%'
    pass_phrase:         '%jwt_key_pass_phrase%'
    token_ttl:           '%jwt_token_ttl%'
    user_identity_field: email

其他服务config.yml配置。

# JWT Configuration
lexik_jwt_authentication:
    public_key:          '%jwt_public_key%'
    token_ttl:           '%jwt_token_ttl%'
    user_identity_field: email

之后,我们可能想创建一个小型共享库来共享可能的角色。或者只是为所有服务复制角色。角色只是字符串,所以任何方法都行得通。我们也可能希望有一个共享的用户提供程序和 UserInterface 实现,但它完全是可选的。 在 JWT 令牌有效负载中,我们可以传递用户的可用角色,当用户通过身份验证并生成 JWT 令牌时,用户服务将填充这些角色。这种方法使其他服务能够读取 JWT 令牌负载并获取用户角色以根据请求的资源检查用户授权。

示例 security.yml 用户服务配置。

security:
    encoders:
        SharedAuthLibrary\Security\User:
            algorithm: bcrypt
        App\Entity\User:
            algorithm: bcrypt

    role_hierarchy:
        ROLE_ADMIN: ROLE_USER

    providers:
        service:
            id: shared_auth_library_jwt_user_provider
        login:
            id: app.user_provider

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        refresh:
            pattern:  ^/api/token/refresh
            stateless: true
            anonymous: true

        login:
            pattern:  ^/api/login$
            stateless: true
            anonymous: true
            provider: login
            json_login:
                check_path: /api/login
                username_path: email
                password_path: password
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure

        api:
            pattern: ^/api
            stateless: true
            anonymous: true
            provider: service
            guard:
              entry_point: lexik_jwt_authentication.jwt_token_authenticator
              authenticators:
                - lexik_jwt_authentication.jwt_token_authenticator

    access_control:
        - { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api/token/refresh, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }

示例 security.yml 其他服务的配置。

security:
    encoders:
        SharedAuthLibrary\Security\User:
            algorithm: bcrypt

    role_hierarchy:
        ROLE_ADMIN: ROLE_USER

    providers:
        service:
            id: shared_auth_library_jwt_user_provider

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        api:
            pattern: ^/api
            stateless: true
            anonymous: true
            provider: service
            guard:
              entry_point: lexik_jwt_authentication.jwt_token_authenticator
              authenticators:
                - lexik_jwt_authentication.jwt_token_authenticator

    access_control:
        - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }

为了获得对其他服务进行授权所需的数据,我们需要使用 ID、电子邮件和角色来丰富 JWT 负载。让我们在我们的用户服务中创建一个 JWT 创建的事件监听器。

<?php

declare(strict_types=1);

namespace App\EventListener;

use SharedAuthLibrary\Security\User;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class SecurityEventSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            Events::JWT_CREATED => 'onJwtCreated',
        ];
    }

    public function onJwtCreated(JWTCreatedEvent $event): void
    {
        /** @var User $user */
        $user = $event->getUser();

        $payload          = $event->getData();
        $payload['id']    = $user->getId();
        $payload['roles'] = $user->getRoles();
        $payload['email'] = $user->getUsername();
        $payload['exp']   = (new \DateTimeImmutable())->getTimestamp() + 86400;

        $event->setData($payload);
    }
}

我们来看看共享库。我们需要一个有效载荷容器来将有效载荷传递给我们的用户提供者,以便创建一个具有来自有效载荷的所有字段的经过身份验证的用户,我们需要检查对资源的授权等等。

<?php

declare(strict_types=1);

namespace SharedAuthLibrary\Security;

class JwtPayloadContainer
{
    private array $payload = [];

    public function setPayload(array $payload): void
    {
        if (empty($this->payload)) {
            $this->payload = $payload;
        }
    }

    public function getPayload(): array
    {
        return $this->payload;
    }
}

以及实际使用负载容器的侦听器。

<?php

declare(strict_types=1);

namespace SharedAuthLibrary\Listener;

use SharedAuthLibrary\Security\JwtPayloadContainer;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent;

class JwtPayloadListener
{
    private JwtPayloadContainer $jwtPayloadContainer;

    public function __construct(JwtPayloadContainer $jwtPayloadContainer)
    {
        $this->jwtPayloadContainer = $jwtPayloadContainer;
    }

    public function onJWTDecoded(JWTDecodedEvent $event): void
    {
        $payload = $event->getPayload();
        $this->jwtPayloadContainer->setPayload($payload);
    }
}

我们的用户提供者可能看起来像这样。

<?php

declare(strict_types=1);

namespace SharedAuthLibrary\Security;

use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class JwtUserProvider implements UserProviderInterface
{
    private JwtPayloadContainer $jwtPayloadContainer;

    public function __construct(JwtPayloadContainer $jwtPayloadContainer)
    {
        $this->jwtPayloadContainer = $jwtPayloadContainer;
    }

    public function loadUserByUsername($username): User
    {
        $payload = $this->jwtPayloadContainer->getPayload();

        return new User($payload['id'], $payload['email'], $payload['roles']);
    }

    public function refreshUser(UserInterface $user): User
    {
        if (!$user instanceof User) {
            throw new UnsupportedUserException(
                sprintf('Instances of "%s" are not supported.', \get_class($user))
            );
        }

        return $this->loadUserByUsername($user->getUsername());
    }

    public function supportsClass($class): bool
    {
        return User::class === $class;
    }
}

示例共享用户模型。

<?php

declare(strict_types=1);

namespace SharedAuthLibrary\Security;

use Symfony\Component\Security\Core\User\UserInterface;

class User implements UserInterface
{
    private string $id;

    private string $username;

    private array $roles;

    private string $password;

    private string $salt;

    public function __construct(
        string $id,
        string $email,
        array $roles,
        string $password = '',
        string $salt = '',
    ) {
        $this->id         = $id;
        $this->roles      = $roles;
        $this->username   = $email;
        $this->password   = $password;
        $this->salt       = $salt;
    }

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

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

    public function getRoles(): array
    {
        return $this->roles;
    }

    public function getPassword(): string
    {
        return $this->password;
    }

    public function getSalt(): string
    {
        return $this->salt;
    }

    public function eraseCredentials()
    {
        // TODO: Implement eraseCredentials() method.
    }
}

最后,将订阅者和监听器注册到service.yml:

    shared_auth_library_jwt_user_provider:
        class: App\SharedAuthLibrary\Security\JwtUserProvider

    App\EventListener\SecurityEventSubscriber:
        tags:
            - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_created, method: onJWTCreated}

    App\SharedAuthLibrary\Listener\JwtPayloadListener:
        tags:
            - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_decoded, method: onJWTDecoded}