dakujem/auth-middleware

高度灵活的PSR-15认证中间件。

2.0 2023-04-19 14:51 UTC

This package is auto-updated.

Last update: 2024-08-29 10:15:55 UTC


README

PHP from Packagist Tests Coverage Status Join the chat at https://gitter.im/dakujem/auth-middleware

现代且高度灵活的PSR-15认证和授权中间件。

💿 composer require dakujem/auth-middleware

📒 变更日志

默认使用方法

要使用此包,您需要创建两个中间件层

  • 一个解码JWT的中间件,该JWT存在于请求中并验证其真实性
  • 以及一个通过断言解码令牌存在的中间件来授权请求

使用Dakujem\Middleware\AuthWizard方便起见

/* @var Slim\App $app */
$app->add(AuthWizard::assertTokens($app->getResponseFactory()));
$app->add(AuthWizard::decodeTokens(new Secret('a-secret-api-key-never-to-commit', 'HS256')));

这对中间件(MW)将在Authorization头部或tokencookie中查找JWT。
然后它将解码JWT并将解码后的有效负载注入到可由应用程序访问的token请求属性中。
如果令牌不存在或无效,断言中间件将终止执行流程并返回401 Unauthorized响应。

可以通过请求属性访问令牌

/* @var Request $request */
$decodedToken = $request->getAttribute('token');

您可以选择只对选定的路由应用断言,而不是对每个路由应用断言

$mwFactory = AuthWizard::factory(new Secret('a-secret-api-key-never-to-commit', 'HS256'), $app->getResponseFactory());

// Decode the token for all routes,
$app->add($mwFactory->decodeTokens());

// but only apply the assertion to selected ones.
$app->group('/foo', ...)->add($mwFactory->assertTokens());

也可以应用自定义的令牌检查

$app->group('/admin', ...)->add(AuthWizard::inspectTokens(
    $app->getResponseFactory(),
    function(MyToken $token, $next, $withError): Response {
        return $token->grantsAdminAccess() ? $next() : $withError('Admin privilege required!');
    }
));

💡
有关实例化中间件的灵活选项,请参阅下面的"自定义中间件"章节。

上面的示例使用Slim PHP框架,但相同的用法适用于任何PSR-15兼容的中间件分发器。

提取和解码JWT

AuthWizard::decodeTokens(__
    // a combination of secret and the encryption algorithm used
    new Secret('a-secret-api-key-never-to-commit', 'HS256'),
    'token',          // what attribute to put the decoded token to
    'Authorization',  // what header to look for the Bearer token in
    'token',          // what cookie to look for the raw token in
    'token.error'     // what attribute to write error messages to
);

这创建了一个使用默认JWT解码器并将解码的令牌注入到应用程序堆栈中可进一步访问的token请求属性的TokenMiddleware实例。

如果解码的令牌出现在属性中,它就是

  • 存在(显然)
  • 真实的(已使用密钥创建)
  • 有效的(未过期)

授权

上面的中间件将仅解码(如果存在)真实且有效的令牌,但不会在任何情况下终止管道❗

授权必须由单独的中间件完成

AuthWizard::assertTokens(
    $responseFactory, // PSR-17 Request factory
    'token',          // what attribute to look for the decoded token in
    'token.error'     // what attribute to look for error messages in
);

这创建了一个将断言请求的token属性包含解码令牌的中间件。
否则,将终止管道并返回401(未授权)响应。错误消息将被编码为JSON放入响应中。

如你所见,这两个中间件像一对夫妻一样工作,但为了灵活性而解耦。

AuthWizard::assertTokens创建的中间件仅断言解码令牌的存在
当然可以创建自定义检查

$inspector = function (object $token, callable $next, callable $withError): Response {
    if ($token->sub === 42) {        // Implement your inspection logic here.
        return $next();              // Invoke the next middleware for valid tokens
    }                                // or
    return $withError('Bad token.'); // return an error response for invalid ones.
};
AuthWizard::inspectTokens(
    $responseFactory, // PSR-17 Request factory
    $inspector,
    'token',          // what attribute to look for the decoded token in
    'token.error'     // what attribute to look for error messages in
);

使用AuthWizard::inspectTokens,可以在涉及令牌的任何条件下终止管道。
可以将自定义错误消息或数据传递给响应。

如果令牌不存在,中间件的行为与由assertTokens创建的中间件相同,并且检查器不会被调用。

您当然可以将令牌转换为自定义类,例如使用 MyToken::grantsAdminAccess 方法来判断令牌是否授权用户进行管理员访问。

AuthWizard::inspectTokens(
    $responseFactory,
    function(MyToken $token, $next, $withError): Response {
        return $token->grantsAdminAccess() ? $next() : $withError('Admin privilege required!');
    }
);

这种转换可以在解码器中完成,也可以在单独的中介中完成。

编写您自己的中介

在上面的示例中,我们使用了 AuthWizard 辅助工具,它提供了合理的默认设置。
然而,使用此包提供的组件自行构建中介是可能的,也是受到鼓励的。

您可以根据任何用例对中介进行微调。

在本文档中,为了简洁起见,我使用的是别名而不是完整的接口名称。

以下是完整的接口名称

TokenMiddleware

TokenMiddleware 负责查找和解码令牌,并将其提供给应用的其他部分。

TokenMiddleware 由以下部分组成

  • 一组 提取器
    • 提取器负责从请求中查找和提取令牌,或者返回 null
    • 按顺序执行,直到其中一个返回字符串
    • fn(Request,Logger):?string
  • 一个 解码器
    • 解码器接受原始令牌并将其解码
    • 必须只返回有效的令牌对象或 null
    • fn(string,Logger):?object
  • 一个 注入器
    • 注入器负责将解码的令牌或错误消息装饰到请求中
    • 通过运行传递给其第一个参数的可调用函数来获取解码的令牌,该参数是 fn():?object
    • fn(callable,Request,Logger):Request

这些可调用组件中的任何一个都可以被替换或扩展。
默认组件也提供了自定义功能。

以下是 AuthWizard::decodeTokens 提供的默认值

new TokenMiddleware(
    // decode JWT tokens
    new FirebaseJwtDecoder('a-secret-never-to-commit', ['HS256', 'HS512', 'HS384']),
    [
        // look for the tokens in the `Authorization` header
        TokenManipulators::headerExtractor('Authorization'),
        // look for the tokens in the `token` cookie
        TokenManipulators::cookieExtractor('token'),
    ],
    // target the `token` and `token.error` attributes for writing the decoded token or error message
    TokenManipulators::attributeInjector('token', 'token.error')
);

使用提示 💡

  • 可以通过交换解码器来使用 OAuth 令牌 或不同的 JWT 实现。
  • 可以通过将提供程序的可调用函数包装在 try-catch 块中来捕获和处理异常 try { $token = $provider(); } catch (RuntimeException $e) { ...
  • 解码器可以返回任何对象,这是将原始有效载荷转换为所选对象的地方。或者,也可以使用单独的中介来完成此目的。

AuthWizard, AuthFactory

AuthWizard 是一个摩擦减少器,它可以帮助快速实例化带有合理默认设置的令牌解码和断言中介。
AuthFactory 是一个可配置的工厂,为了方便提供了合理的默认设置。
AuthWizard 在内部实例化 AuthFactory 并作为工厂的静态外观。

使用 AuthFactory::decodeTokens 来创建令牌解码中介。
使用 AuthFactory::assertTokens 来创建断言存在解码令牌的中介。
使用 AuthFactory::inspectTokens 来创建针对令牌的具有自定义授权规则的中介。

GenericMiddleware

GenericMiddleware 被用于 AuthWizard / AuthFactory 的令牌存在性和自定义授权。

它也可以用于方便的嵌入式中介实现。

$app->add(new GenericMiddleware(function(Request $request, Handler $next): Response {
    $request = $request->withAttribute('foo', 42);
    $response = $next->handle($request);
    return $response->withHeader('foo', 'bar');
}));

TokenManipulators

TokenManipulators 静态类提供了各种请求/响应操纵器,可用于令牌处理。
它们用作中介的组件。

FirebaseJwtDecoder

FirebaseJwtDecoder 类作为 JWT 令牌解码的默认实现。
它用作TokenMiddleware解码器
您可以将其替换为不同的实现。

为了使用此解码器,您需要安装Firebase JWT包。
composer require firebase/php-jwt:"^5.5"

日志记录器

TokenMiddleware接受一个PSR-3 Logger实例以用于调试目的。

提示

多个令牌解码和令牌检查中间件也可以堆叠!

通常将令牌解码应用于应用级别的中间件(每个路由),但断言可以根据需要组合并应用于组或单个路由。

测试

使用以下命令运行单元测试

$ composer test

兼容性

为了使用FirebaseJwtDecoder解码器,必须安装正确的firebase/php-jwt版本。尽管如此,使用此解码器并非必需。

贡献

欢迎提出想法、功能请求和其他贡献。请发送PR或创建一个issue。

安全问题

如果您偶然发现一个安全问题,请在不透露任何相关信息的情况下创建一个issue,我们将私下联系并讨论细节。