我无法从 JWT 获取用户

I can't get the user from JWT

我有一个使用 Lexik JWT v2.8.0、gesdinet jwt 刷新令牌 v0.9.1 和我自己的实体用户的 Symfony 5.1 项目。我可以使用 JWT 登录并获取令牌,将其保存在 HttpOnly cookie 中并成功将其与受保护的 APIs 一起使用。

我的网络应用程序有一些 API 休息,但它也有页面要浏览。所以我的意图是从 API 完成这一切,但当用户获得令牌时也在网络浏览器中登录。

但由于 api 登录是无状态的,在成功登录后,网络分析器仍然显示登录为匿名。而且我无法从令牌中获取用户。我做了一个服务来从令牌中获取用户,在控制器中调用它并将一些登录的用户数据发送到前端。

我得到了 的服务。我实现的获取用户的服务是:

use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use UserBundle\Domain\Entity\User;

class UserService
{
    private TokenStorageInterface $tokenStorage;
    private JWTTokenManagerInterface $jwtManager;

    public function __construct( TokenStorageInterface $storage, JWTTokenManagerInterface $jwtManager )
    {
        $this->tokenStorage = $storage;
        $this->jwtManager = $jwtManager;
    }

    public function getCurrentUser() : ?User
    {
        $decodedJwtToken = $this->jwtManager->decode($this->tokenStorage->getToken());
        if ($decodedJwtToken instanceof TokenInterface) {

            $user = $decodedJwtToken->getUser();
            return $user;

        } else {
            return null;
        }
    }
}

并将其声明为服务:

get.token.user.service:
    class: UserBundle\Domain\Service\UserService
    arguments: [ '@security.token_storage' ]

但我从 $this->tokenStorage->getToken() 得到的只是一个带有“anon”的网络令牌。用户,所以它不是 JWT,jwtManager 无法对其进行解码。

UserService.php on line 25:
Symfony\Component\Security\Core\Authentication\Token\AnonymousToken {#16 ▼
  -secret: "RXVaVx3"
  -user: "anon."
  -roleNames: []
  -authenticated: true
  -attributes: []
}

我还尝试从控制器中的 cookie 中获取 jwt 并将其作为参数发送到服务,但是我从解码中得到一个错误,我正在传递一个字符串并且它需要一个 TokenInterface 对象,所以它也没有用。

如何从服务中的令牌中获取用户?是否有通过 api 登录网络而不是从 jwt 获取用户并将其发送到渲染器的最佳实践?

编辑:添加代码以使用 lexik jwt 并将令牌保存在 cookie 中:

# /config/packages/lexik_jwt_authentication.yaml
lexik_jwt_authentication:
    secret_key: '%env(resolve:JWT_SECRET_KEY)%'
    public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
    pass_phrase: '%env(JWT_PASSPHRASE)%'
    token_ttl: 3600
    user_identity_field: email

token_extractors:
    cookie:
        enabled: true
        name: BEARER

安全文件代码

# /config/packages/security.yaml
security:
    encoders:
        UserBundle\Domain\Entity\User:
            algorithm: auto

    providers:
        app_user_provider:
            entity:
                class: UserBundle\Domain\Entity\User
                property: email
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        login:
            pattern: ^/api/login
            stateless: true
            anonymous: true
            json_login:
                provider: app_user_provider
                check_path: /api/login_check
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
        refresh:
            pattern:  ^/api/token/refresh
            stateless: true
            anonymous: true
        register:
            pattern: ^/api/register
            stateless: true
            anonymous: true
        api:
            pattern: ^/api
            stateless: true
            anonymous: true
            provider: app_user_provider
            guard:
                authenticators:
                    - lexik_jwt_authentication.jwt_token_authenticator
        main:
            anonymous: true
            lazy: true
            provider: app_user_provider

    access_control:
        - { path: ^/api/user, roles: IS_AUTHENTICATED_ANONYMOUSLY, methods: [GET] }
        - { path: ^/api/linkpage, roles: IS_AUTHENTICATED_ANONYMOUSLY, methods: [GET] }
        - { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api/token/refresh, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }

侦听器将 jwt 保存在 cookie 中并避免令牌出现在响应中:

use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent;
use Symfony\Component\HttpFoundation\Cookie;

class AuthenticationSuccessListener
{
    private bool $secure = false;
    private int $tokenTtl;

    public function __construct(int $tokenTtl)
    {
        $this->tokenTtl = $tokenTtl;
    }

    public function onAuthenticationSuccess( AuthenticationSuccessEvent $event)
    {
        $response = $event->getResponse();
        $data = $event->getData();

        $token = $data['token'];
        unset($data['token']);  // remove token from response. It works.
        unset($data['refresh_token']); // remove token from refresh token, even though I still get this token in the response
        $event->setData($data);

        $response->headers->setCookie(
            new Cookie(
                'BEARER',
                $token,
                (new \DateTime())->add(new \DateInterval('PT'. $this->tokenTtl . 'S')),
                '/',
                null,
                $this->secure
            )
        );
    }
}


#services.yaml
app.listener.authenticationsuccesslistener:
    class: UserBundle\Application\Listeners\AuthenticationSuccessListener
    arguments: ['%lexik_jwt_authentication.token_ttl%']
    tags:
      - { name: kernel.event_listener, event: lexik_jwt_authentication.on_authentication_success, method: onAuthenticationSuccess }

我仍然为刷新令牌编写了另外 2 个侦听器,但我认为不需要它们。

其余的身份验证是来自 lexik jwt bundle 的默认身份验证代码。

在 BEARER cookie 中存储了正确格式的 jwt,如下所示:

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1OTgzNTE0ODYsImV4cCI6MTU5ODM1NTA4Niwicm9sZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJlbWFpbCI6ImFkbWluQGVtYWlsLmNvbSJ9.p5WYCxEE-VSxp09Eo7CxXoxi6zy1ZcnLJiBe1YGsrk3iFm7T-6JAWbvyb9ZW_-1jtpYcWQlFOjOf7uET4wRHvlvygnPOeZck7tZM8TUlSqMXIllMeVARz8mEUvXVwhDWEk5T7Ibw9c3VgfvyiLUgSb_cmSyK3DwtgPCd9vOYarkag9XKxkxNI9M1OHGL61v1NoQHdkXloC72xdUUMcj5Y8yCJWZFdOp8-Xtfq8ZChAHzwCjXUhC3VnBqZcMT0tAvtilwTGDYDykppNKK1vbNoyOex47wQH_ILEFuX5Eh1p2xfbc0lWm3Ip21z3EQ2M_eOQgZvHR65T3b2dv9g5GPiFp3CNo8AuW8m6rXjWK6NZXJO8qYodxI5cUYYyFooCfVXXU8JXzGQfCZIdOPw-iBGzQEfFuLL50_sAOVcxjklAYCFZYRHwFKWmwl1BwJF4mAw4jnNIAdMmc66Z17ul2Jep9apOO90C1dZzGuVKxWqglc9GZo7-teHt0dMsg0ADrvaOKNJUwDBGJ4lZpWx6_stcl7DkCdc5k1kePGdLa7YXRO3umPwa_FVYVgnT_Z9x7RtfnGioa2TZJCIdbJnuj0L90vkgFBjHqFdVydDaaBu3Y0mKoQ2v3Sf1so4-uwJm8z1vQVZleMQgFibMiyyk3YyDidhPSxxyp4u-4xPNOSDNo

如果我没有登录并且没有 jwt 或者我删除了 BEARER cookie,我将无法访问受保护的 APIs ({"code": 401, "message": "JWT找不到令牌"}),当我分配了 jwt 时,我可以正确请求 APIs。

问题出在 security.yaml 配置上,特别是在 firewalls.main 部分,我必须在其中添加 jwt_token_authenticator 作为守卫。最后,我的 security.yaml 是:

security:
    encoders:
        UserBundle\Domain\Entity\User:
            algorithm: auto

    providers:
        app_user_provider:
            entity:
                class: UserBundle\Domain\Entity\User
                property: email
        jwt:
            lexik_jwt: ~
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        login:
            pattern: ^/api/login
            stateless: true
            anonymous: true
            json_login:
                provider: app_user_provider
                check_path: /api/login_check
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
                require_previous_session: false
        refresh:
            pattern:  ^/api/token/refresh
            stateless: true
            anonymous: true
        register:
            pattern: ^/api/register
            stateless: true
            anonymous: true
        api:
            pattern: ^/api
            stateless: true
            anonymous: true
            provider: jwt
            guard:
                authenticators:
                    - lexik_jwt_authentication.jwt_token_authenticator
        main:
            anonymous: true
            lazy: true
            provider: jwt
            guard:
                authenticators:
                    - lexik_jwt_authentication.jwt_token_authenticator
            logout:
                path:   app_logout
                target: /

    access_control:
        - { path: ^/api/login_check, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api/token/refresh, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/logout, roles: IS_AUTHENTICATED_FULLY }
        - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }

然后我在注销时遇到了一些问题,因为注销没有删除 jwt cookie 而注销让我保持登录状态。所以我为 LogoutEvent 创建了一个监听器并让监听器删除了 cookies。

use Symfony\Component\Security\Http\Event\LogoutEvent;

class LogoutListener
{

    public function onSymfonyComponentSecurityHttpEventLogoutEvent(LogoutEvent $event)
    {
        $response = $event->getResponse();
        $response->headers->clearCookie('BEARER');
        $response->headers->clearCookie('REFRESH_TOKEN');

        $event->setResponse($response);
    }

}

并将其声明为服务:

app.listener.logout:
    class: UserBundle\Application\Listeners\LogoutListener
    tags:
        - name: 'kernel.event_listener'
        event: 'Symfony\Component\Security\Http\Event\LogoutEvent'
        dispatcher: security.event_dispatcher.main

现在登录在网络上工作,无需使用我正在使用的服务解码 jwt 并将用户传递到前端。虽然解码 jwt 的服务现在工作正常。

我已经在这个问题上浪费了将近一周的时间,但最终我找到了解决方案并且运行良好。因此,如果有人遇到类似问题,我会 post 在这里节省时间。