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

问题描述

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

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

然后,我在 example2.dev 上使用了第二个用于课堂的 symfony 应用程序,但还没有用户/身份验证方法

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

解决方法

绝对有可能。让我们来看看一个可能的实现,希望它是设计一个适合您的需求的解决方案的一个很好的起点。在第一个 Symfony 应用程序(我们称之为用户服务)中,我们将有登录功能来将凭据交换为 JWT 令牌、刷新 JWT 令牌等。在获得 JWT 令牌后,用户可以调用其他服务并使用JWT 令牌。在其他服务上,我们需要解码 JWT 令牌(它会检查它是否有效且未过期)。为此,我们应该在所有服务中都有 LexikJWTAuthenticationBundle 依赖项,但具有不同的配置。对于用户服务,我们将同时拥有公钥和私钥以生成 JWT 令牌并对其进行验证,而其他服务仅需要公钥来验证 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}