如何让 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}
有什么方法可以从不同域上的另一个 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}