square1/laravel-idempotency

为基于Laravel的API添加幂等性,防止重复请求处理。

2.0.0 2024-03-21 09:49 UTC

This package is auto-updated.

Last update: 2024-09-21 10:46:11 UTC


README

Build and Test

为您的Laravel API添加幂等性

此包使您轻松将幂等性密钥支持添加到Laravel API中。当接收到带有先前使用的幂等性密钥的请求时,服务器将返回为原始请求生成的相同响应,并避免在服务器端进行任何重复处理。这在请求可能因网络问题或用户重试而重复的情况(如支付处理或表单提交)中特别有用。

目录

什么是幂等性?

幂等性是指能够多次执行相同的请求,并且只应用一次更改的能力。通过为每个传入请求添加一个唯一键,服务器可以检测到重复的请求。如果之前没有看到该请求,服务器可以安全地处理它。如果键之前已经出现过,服务器可以返回先前的响应,而无需重新处理请求。这在API客户端在不可靠的网络条件下操作时特别有用,其中请求在重新建立连接后会自动重试。

考虑向API发送支付请求的示例。

Standard API Flow

在这种情况下,客户端的连接性很差,因此并不总是收到“确认#1”响应。当连接丢失然后恢复时,客户端将重新发送请求。这里的风险是,原始请求已经正确处理,导致重试的事务处理了重复的支付请求,最终导致“确认#2”。

在上面的流程中,我们的用户将被收取两次费用。现在,让我们看看当相同的API和客户端实现幂等性时,相同的支付流程是如何工作的。

Idempotency API Flow

在这个流程中,客户端为每个请求生成一个幂等性键。第一次支付请求被发送,并且再次,客户端没有收到“确认#1”响应。然而,当重新尝试时,客户端正在发送相同的幂等性键。当发生这种情况时,服务器会识别出它已经处理了该请求。请求不会在后台重新处理(根本不会与银行通信来处理交易!)并再次返回原始响应(“确认#1”)。

包功能

  • 幂等性键验证:确保每个API请求都是唯一的,并且只处理一次,防止重复操作。
  • 用户特定缓存:幂等性键基于Laravel的默认认证对每个用户都是唯一的。
  • 可定制的缓存持续时间:设置默认缓存持续时间并根据需要定制。
  • 可配置的幂等性头:自定义幂等性头的名称。
  • 灵活的用户ID检索:根据您应用程序的需求更改检索用户ID的方法。

安装

本文档涵盖在服务器端安装幂等性包。客户端生成的幂等性密钥的格式不由包强制规定。当前的最佳实践是使用V4 UUID或类似长度的随机键,具有足够的熵以避免冲突。

### 要求包

通过Composer

$ composer require square1/laravel-idempotency

包将自动注册。

发布配置(可选)

php artisan vendor:publish --provider="Square1\LaravelIdempotency\IdempotencyServiceProvider"

这将创建一个位于您的config目录中的config/idempotency.php文件。

配置

发布配置文件后,您可以根据需求对其进行修改。可用的主要配置选项包括:

  • cache_duration:响应缓存时间,单位为秒。此值控制重用幂等键被视为新请求的周期。默认为1天。
  • idempotency_header:客户端用于传递幂等键的请求头名称。默认为Idempotency-Key
  • ignore_empty_key:默认情况下,如果我们期望传递幂等键但没有传递,将抛出MissingIdempotencyKeyException异常。如果您想支持缺少键的请求,可以将此值设置为true以防止抛出该异常。这在更新API客户端以推出键的期间可能很有用。默认为false
  • enforced_verbs:应用于幂等性检查的HTTP动词数组。通常GET和HEAD请求不改变状态,因此不需要幂等性检查,但如果您希望检查不同的动词,则可以编辑此数组。默认为['POST', 'PUT', 'PATCH', 'DELETE']
  • on_duplicate_behaviour:默认情况下,当幂等键被重复使用时,包将重新播放之前看到的响应。您的应用程序可能希望以不同的方式处理这种重复 - 例如抛出错误。将此值设置为exception将在此情况下抛出DuplicateRequestException异常。默认为replay
  • max_lock_wait_time:为了避免竞态条件,当请求到达时创建缓存锁。如果另一个请求在缓存填充之前到达但锁已被获取,则第二个请求将内部每秒轮询一次以查看缓存是否已填充,并在这些秒数后放弃。
  • user_id_resolver:幂等键对每个用户是唯一的,这意味着如果两个不同的用户以某种方式使用相同的键,则不会发生键冲突。这需要包在构建缓存时使用当前认证用户来构建缓存键。默认情况下,包将使用Laravel的auth()->user()->id值。如果您想使用不同的值来唯一标识您的用户,可以将类-方法对添加到此值以实现该自定义值。
    // Define custom resolver of per-user identifier.
    'user_id_resolver' => [ExampleUserIdResolver::class, 'resolveUserId'],
// App\Services\ExampleUserIdResolver

namespace App\Services;

class ExampleUserIdResolver
{
    public function resolveUserId()
    {
        // Implement custom logic to return the user ID
        return session()->special_user_token;
    }
}

用法

通过中间件提供包的核心功能。要使用它,您只需将中间件添加到您的路由或控制器中。

注意 由于此包需要了解当前用户,请确保在执行任何用户身份验证操作之后添加中间件。

### 全局用法

Laravel 11+

要将中间件应用于所有路由,您可以将它追加到应用程序的app/bootstrap.php文件中的全局中间件堆栈。

use Square1\LaravelIdempotency\Http\Middleware\IdempotencyMiddleware;

...
->withMiddleware(function (Middleware $middleware) {
     $middleware->append(IdempotencyMiddleware::class);
})

这里的append函数将此中间件添加到应用程序的全局中间件末尾。有关Laravel 11中处理中间件顺序的更多信息,请参阅文档

Laravel <= 10

要将中间件应用于所有路由,请将其添加到app/Http/Kernel.php中的$middlewareGroups数组。

protected $middlewareGroups = [
    'api' => [
        // other middleware...
        \Square1\LaravelIdempotency\Http\Middleware\IdempotencyMiddleware::class,
    ],
];

这将在此应用程序的所有路由上运行中间件。但是,包配置中的enforced_verbs值将控制中间件是否对给定路由有任何影响(默认情况下,中间件不会干扰GET或HEAD请求)。

特定路由

Laravel 11+

您可以将中间件追加到所有API路由,利用Laravel的默认中间件组

// app/boostrap.php
use Square1\LaravelIdempotency\Http\Middleware\IdempotencyMiddleware;
...
->withMiddleware(function (Middleware $middleware) {
    $middleware->api(append: [
        IdempotencyMiddleware::class
    ]);
})

或者,您可以在路由文件中应用中间件到特定路由

Route::get('/profile', function () {
    // ...
})->middleware(IdempotencyMiddleware::class);

Laravel <= 10

或者,您可以将中间件应用于特定路由

// App\Http\Kernel
protected $middlewareAliases = [
    ...
    'idempotency' => \Square1\LaravelIdempotency\Http\Middleware\IdempotencyMiddleware::class,
    ...

// routes/api.php
Route::middleware('idempotency')->group(function () {
    Route::post('/your-api-endpoint', 'YourController@method');
    // other routes
});

识别幂等响应

当请求成功执行时,它将被返回给客户端,并且响应被缓存。在看到重复的幂等性键后,将返回此缓存值。在这种情况下,将返回一个额外的头信息 Idempotency-Relayed。这个头信息包含了客户端发送的相同的幂等性键,并且是向客户端发出的信号,表示这个响应已被重复。这个头信息只出现在重复响应中,绝不会出现在原始响应中。

Response Header

异常处理

此包可能会抛出多个异常,所有异常都位于 Square1\LaravelIdempotency\Exceptions 命名空间下

  • MismatchedPathException:如果在具有相同幂等性键的重复请求中请求路径不同,则会抛出此异常。这通常是客户端存在错误的标志,例如,键在每次请求时没有更改,例如,客户端重复使用相同的键来请求 POST /users 然后 POST /accounts
  • DuplicateRequestException:默认情况下,当再次看到之前的幂等性键时,该包将重新播放响应。将配置值 on_duplicate_behaviour 更改为 exception 将导致抛出异常(对于应用程序来说,当重新发送的请求更可能是客户端的错误时非常有用)。
  • LockExceededException:为了避免具有相同键的两个请求之间的竞争条件,每个未看到已存在的缓存响应的请求首先尝试获取缓存锁。只有一个请求可以得到这个锁,因此失败的请求将定期轮询缓存以获取响应。如果等待时间超过 config('idempotency.max_lock_wait_time') 中的值,将抛出 LockExceededException 异常。
  • MissingIdempotencyKeyException:当由幂等性中间件处理的请求中没有键时,将抛出此异常。此检查是在 enforced_verbs 检查之后执行的,因此,例如,如果GET请求不应由中间件考虑,则没有键的GET请求不会触发此异常。除非将配置值 ignore_empty_key 更改为 true,否则将抛出此异常。

测试

$ composer test

许可协议

MIT许可协议(MIT)。请参阅 许可文件 了解更多信息。