【问题标题】:Silex: Get Authenticated User Information on Routes Outside of FirewallSilex:获取防火墙外路由上经过身份验证的用户信息
【发布时间】:2026-01-17 11:00:01
【问题描述】:

我正在使用 Silex 2.0(我知道 - 它是开发版本,尚未完全发布)以及 CNAM 的 JWT 安全提供程序(请参阅:https://github.com/cnam/security-jwt-service-provider)为我正在编写的开源应用程序编写 API。

简而言之,我关心三种类型的用户:

  1. 拥有完全访问权限的全站管理员 (ROLE_ADMIN)
  2. 专员 (ROLE_COMMISH) 创建自己拥有的对象并可以编辑自己的对象
  3. 访问只读信息的匿名用户。

因此,与这些“角色”相关的路线分为三个部分:

  1. /admin/* 管理员可以在其中执行他们的超级操作
  2. /commish/* 专员或管理员可以在其中对其对象执行操作
  3. /*所有用户都可以阅读信息的地方

我遇到的问题是,虽然我可以设置 3 个防火墙,每个防火墙一个,但有时在第 3 个路由类别(例如GET /object/1)中需要匿名访问它,但如果用户提供了一个有效的 JWT 令牌,我需要访问该用户以便对我在响应中交回的数据执行一些额外的逻辑。

因为我目前已经设置了它(更多关于我的配置如下),它要么全有要么全无:我要么将整个防火墙限制为仅具有特定角色的经过身份验证的用户,要么我将其开放给匿名用户(因此无法查看用户信息)。

是否有可能有一条任何人都可以点击的路线,但也可以看到登录的用户?

当前的安全配置:

$app['users'] = function () use ($app) {
    return new UserProvider($app);
};

$app['security.jwt'] = [
    'secret_key' => AUTH_KEY,
    'life_time'  => 86400,
    'algorithm'  => ['HS256'],
    'options'    => [
        'header_name' => 'X-Access-Token'
    ]
];

$app['security.firewalls'] = array(
  'login' => [
    'pattern' => 'login|register|verify|lostPassword|resetPassword',
    'anonymous' => true,
  ],
  'admin' => array(
    'pattern' => '^/admin',
    'logout' => array('logout_path' => '/logout'),
    'users' => $app['users'],
    'jwt' => array(
        'use_forward' => true,
        'require_previous_session' => false,
        'stateless' => true,
    )
  ),
  'commish' => array(
    'pattern' => '^/commish',
    'logout' => array('logout_path' => '/logout'),
    'users' => $app['users'],
    'jwt' => array(
        'use_forward' => true,
        'require_previous_session' => false,
        'stateless' => true,
    )
  )
);

$app['security.role_hierarchy'] = array(
  'ROLE_ADMIN' => array('ROLE_MANAGER'),
);

$app->register(new Silex\Provider\SecurityServiceProvider());
$app->register(new Silex\Provider\SecurityJWTServiceProvider());

此外,我尝试了另一种方法,在单个防火墙下匹配所有路由,然后使用securty.access_rules 配置保护某些路由,但它不起作用。我尝试过的一个例子:

$app['security.firewalls'] = array(
  'api' => array(
    'pattern' => '^/',
    'logout' => array('logout_path' => '/logout'),
    'anonymous' => true,
    'jwt' => array(
      'use_forward' => true,
      'require_previous_session' => false,
      'stateless' => true
    )
  )
);

$app['security.access_rules'] = array(
  array('^/admin', 'ROLE_ADMIN'),
  array('^/commish', 'ROLE_MANAGER'),
  array('^/', 'IS_AUTHENTICATED_ANONYMOUSLY')
);

【问题讨论】:

    标签: php security symfony silex jwt


    【解决方案1】:

    所以:到目前为止,我还没有发现通过“正常”方式可以做到这一点,这令人失望。几天内我不会将我在下面详述的内容标记为“答案”,希望有人能插话并提供更好、更“官方”的方法来解决这个难题。

    TL;DR:我手动检查访问令牌字符串的请求标头,然后使用 JWT 类对令牌进行解码,以便在防火墙外的路由中加载用户帐户。它非常hacky,感觉非常肮脏,但这是我目前看到的问题的唯一解决方案。

    技术细节:首先,您必须从请求标头中获取令牌值。您的控制器方法将收到一个Symfony\Component\HttpFoundation\Request 对象,您可以从中访问$request->headers->get('X-Access-Token')。在大多数情况下,用户不会通过身份验证,因此这将为空,您可以返回 null。

    如果不为空,则必须使用 Silex 的 JWTEncoder 实例解码令牌内容,创建一个新的令牌实例JWTToken,将上下文设置为来自编码器的解码值,最后您可以访问来自所述令牌的用户名属性 - 然后可用于获取相应的用户记录。我想出的一个例子:

    $request_token = $request->headers->get('X-Access-Token','');
    
    if(empty($request_token)) {
      return null;
    }
    
    try {
      $decoded = $app['security.jwt.encoder']->decode($request_token);
    
      $token = new \Silex\Component\Security\Http\Token\JWTToken();
      $token->setTokenContext($decoded);
    
      $userName = $token->getTokenContext()->name;
    
      //Here, you'd use whatever "load by username" function you have at your disposal
    }catch(\Exception $ex) {
      return null;
    }
    

    显然,任何调用此函数的代码都需要知道,因为请求在防火墙之外,所以 保证会返回用户(因此,hacky try-catch通过返回 null 来消除异常。

    编辑:我已更新此处的代码以使用 Silex 的内置 DI 容器(由 Pimple 提供),因此无需手动创建 JWT 编码器的新实例。我还将 @user5117342 的答案标记为正确的答案,因为使用某种 Silex 中间件方法要强大得多。

    编辑(2016 年 4 月):使用更新的 cnam/security-jwt-service 2.1.0symfony/security 2.8,有一个小的更新使上面的代码更简单:

    $request_token = $request->headers->get('X-Access-Token','');
    
    if(empty($request_token)) {
      return null;
    }
    
    try {
      $decodedToken = $app['security.jwt.encoder']->decode($request_token);
    
      $userName = $decodedToken->name;
    
      //Here, you'd use whatever "load by username" function you have at your disposal
    }catch(\Exception $ex) {
      return null;
    }
    

    较新的依赖关系的问题是JWTToken 构造函数需要3个参数,这在大多数服务层中都很难获得,更不用说完全不合适了。当我更新我的 Composer 依赖项时,我最终发现我实际上并不需要创建 JWTToken 来获取我需要的用户名。

    当然,需要注意的是,我只在公共(匿名)API 路由上使用此方法,以便为登录的用户提供一些细节 - 我的应用程序不处理敏感数据,所以我并不过分关注防火墙之外的这条途径。在最坏的情况下,黑帽用户最终会看到他们通常不会看到的非敏感数据,但仅此而已。所以YMMV。

    【讨论】:

    【解决方案2】:

    您必须使用正则表达式,例如

    $app['security.firewalls'] = array(
       'login' => [
           'pattern' => 'login|register|oauth',
           'anonymous' => true,
       ],
       'secured' => array(
           'pattern' => '^/api|/admin|/manager',
           'logout' => array('logout_path' => '/logout'),
           'users' => $app['users'],
           'jwt' => array(
               'use_forward' => true,
               'require_previous_session' => false,
               'stateless' => true,
           )
       ),
    ); 
    

    【讨论】:

      【解决方案3】:

      您可以使用 $app['security.jwt.encoder'] 对 jwt 进行解码,并创建自定义 trait 并扩展路由对象,或者在路由级别使用 midddleware,或者更简单的方法是在应用程序级别使用中间件。我有类似的问题,这就是我解决它的方法,如下所示

      例如。

         $app->before(function (Request $request, Application $app) {                      
      
             $request->decodedJWT  = $app['security.jwt.encoder']-> 
              decode($request->headers->get('X-Access-Token'));
      
          }); 
      

      然后你可以通过这样做访问任何路由的解码 jwt

       $app->get('/object/1', function(Request $request) {
      
           $decodedJWT = $request->decodedJWT;
      
           // do whatever logic you need here 
        })
      

      【讨论】:

      • 不错!这已经感觉比我实施的 hack 干净多了——而且感觉就像是解决问题的“Symfony”方式。
      • 中间件方法很棒。我已经更新了我的其他答案以使用 DI 容器不必手动实例化编码器,并且还指出了这个答案。
      最近更新 更多