Silex:在防火墙外的路由上获取经过身份验证的用户信息

Silex: Get Authenticated User Information on Routes Outside of Firewall

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

简而言之,我关心的用户分为三类:

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

因此,有三段路线与这些 "roles":

  1. /admin/* 管理员可以在其中执行超级操作
  2. /commish/* 专员或管理员可以在其中对其对象执行操作
  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')
);

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

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

技术细节:首先,您必须从请求header中获取令牌值。您的控制器方法将被传递给 Symfony\Component\HttpFoundation\Request object,您可以从中访问 $request->headers->get('X-Access-Token')。在大多数情况下,用户不会通过身份验证,因此这将为空,您可以 return 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;
}

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

编辑:我在这里更新了代码以使用 Silex 的 built-in 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 来获取我需要的用户名。

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

您可以使用 $app['security.jwt.encoder'] 解码 jwt 并在路由级别创建自定义 trait and extending the route object or using midddlewareee,或者更简单的方法是在应用程序级别使用中间件。我有类似的问题,这就是我解决它的方法,如下所示

例如。

   $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 
  })

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

$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,
       )
   ),
);