【问题标题】:I can't get the user from JWT我无法从 JWT 获取用户
【发布时间】:2020-08-25 07:20:39
【问题描述】:

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

我的网络应用有一些 API Rest,但它也有可供浏览的页面。所以我的意图是通过 API 完成这一切,但在用户获得令牌时也登录 Web 浏览器。

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

我有来自this question 的服务。我为获取用户而实施的服务是:

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 P>

如果我没有登录并且没有 jwt,或者我删除了 BEARER cookie,我将无法访问受保护的 API({"code": 401, "message": "JWT Token not found"}),当我分配了 jwt 后,我​​可以正确请求 API。

【问题讨论】:

  • 由于您的工作是无状态的,您需要确保您的令牌已针对每个请求进行验证并解析为正确的用户(身份验证)。这似乎不是这里的情况。所以我假设你看错了地方。请说明您是如何实现实际用户身份验证的,而不是如何从令牌中提取用户。
  • 我在每个 api 请求中检查令牌,我认为身份验证是正确的。我需要的是将用户数据传递到网络前端。实际的用户身份验证是由 lexik jwt bundle 完成的,我只是将其更改为将令牌保存在 cookie 中。但现在我将用我的代码编辑我的问题。
  • 如果令牌没有返回进行身份验证的用户,但匿名身份验证似乎没有达到您的预期。
  • 我添加了我使用的其余代码
  • 您仍然只显示第一次身份验证。有趣的部分是您发送下一个请求的位置(添加承载的位置)以及您的后端需要针对正确的用户验证承载的位置。这是您的代码失败或您提供错误输入的地方。

标签: symfony jwt symfony5 lexikjwtauthbundle


【解决方案1】:

问题出在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 创建了一个侦听器并让侦听器删除了 cookie。

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 的服务现在工作正常。

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

【讨论】:

    猜你喜欢
    • 2023-02-14
    • 2020-10-26
    • 1970-01-01
    • 2021-07-28
    • 2020-08-05
    • 1970-01-01
    • 1970-01-01
    • 2021-06-01
    • 1970-01-01
    相关资源
    最近更新 更多