Laravel 中的 HMAC 身份验证
最近在开发一组 API webhook 时,我需要提供一些安全方案,以便客户端能够验证 webhook 请求是否由我正在开发的正确服务器发送。经过一番在线研究,我偶然发现了用于 API 请求的 HMAC 方案。简单解释一下之后,我将给出一个在 Laravel 中为 API 端点实现该方案的示例。
基于哈希的消息认证码 (HMAC) 是使用加密哈希函数、数据和密钥生成的代码。它用于验证请求来源和内容的真实性。客户端和服务器共享一个预先知道的密钥来生成 HMAC。
简单来说,服务器和客户端共享一个密钥,并且只有服务器和客户端知道。哈希函数会在每次发出请求时使用此密钥生成一个代码。然后,此代码会被添加到请求中并发送出去。接收应用程序收到请求后,会使用刚刚收到的请求信息和存储的密钥,运行与创建代码相同的流程。如果发送者的身份正确,请求中的代码和刚刚生成的代码应该匹配,接收者就可以继续处理请求了。是不是很简单?
这是一种简单而有效的身份验证方法,因为完整性检查不允许中间人攻击,除非他们拥有密钥。然而,攻击者可能会一遍又一遍地重复发送相同的请求,这可能会导致数据损坏或泄露一些敏感信息。这些被称为重放攻击,有很多方法可以应对这类攻击,但我不会讨论任何一种方法,因为我打算尽可能简化讨论。
现在简单解释一下我们的哈希函数。我们将使用的值是:
- URL:小写的完整 URL,包括任何查询参数。
- Verb:大写的请求的 HTTP 方法,例如 POST。
- 内容 MD5:JSON 格式的请求主体的 MD5 哈希值
创建哈希码的步骤如下:
- 使用上面的值创建一个签名字符串。
- 使用字符串和我们的密钥生成哈希。
- 对哈希进行 Base64 编码并将其附加到请求标头
可以使用其他值,例如请求内容类型,但我尽量简单。每个 API 创建用于哈希计算的签名字符串的方案通常在 API 文档中描述。
设置
首先,启动一个新的 Laravel 应用程序并添加几个控制器来添加我们的 API 逻辑。
php artisan make:controller UsersController
php artisan make:controller ClientController
在 routes/api.php 中为端点添加一些路由
Route::prefix('/')->middleware('auth.hmac')->group(static function () {
Route::get('/users', [App\Http\Controllers\UsersController::class, 'getAll']);
Route::get('/users/{id}', [App\Http\Controllers\UsersController::class, 'getOne']);
Route::post('/users', [App\Http\Controllers\UsersController::class, 'create']);
Route::put('/users/{id}', [App\Http\Controllers\UsersController::class, 'update']);
});
我已经为路由分配了一个中间件auth.hmac
,稍后我会再讨论这个问题。接下来,将示例公钥和密钥保存到文件中.env
,并在配置文件中引用它们config/hmac.php
。这里的公钥是 HMAC 的预期请求头密钥。
# .env
HMAC_PUBLIC_KEY="X-MEN-SIGNATURE"
HMAC_SECRET_KEY="Xavier's School for Gifted Youngsters"
<!-- hmac.php -->
<?php
return [
'public' => 'X-MEN-SIGNATURE',
'secret' => "Xavier's School for Gifted Youngsters"
];
接下来,我们将在控制器中实现一些方法来创建、获取和编辑用户,以方便本教程的简单使用。用户模型随 Laravel 安装而来,我们无需进行任何更改。
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class UsersController extends Controller
{
public function getAll(Request $request)
{
$users = User::all();
return response()->json($users);
}
public function getOne(Request $request, $id)
{
$user = User::findOrFail($id);
return response()->json($user);
}
public function create(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required',
'email' => 'required|email|unique:users',
'password' => 'required|min:6'
]);
if ($validator->fails()) {
return response()->json($validator->errors()->first(), 400);
}
extract($request->all());
$password = Hash::make($password);
$user = User::create(compact('name', 'email', 'password'));
return response()->json($user);
}
public function update(Request $request, $id)
{
$user = User::findOrFail($id);
$data = [
'name' => $request->name ?? $user->name,
'email' => $request->email ?? $user->email,
'password' => Hash::make($request->name) ?? $user->password
];
$user->update($data);
return response()->json($user);
}
}
把它弄乱
现在,我们已经为一个简单的 API 编写了有效的控制器逻辑。但是,由于缺少中间件,调用它会导致错误。让我们回过头来看一下。这个中间件将用于 HMAC 身份验证。在控制器处理请求之前,我们将确认它包含正确的 HMAC 头。
添加新的中间件:
php artisan make:middleware HmacAuth
并通过将其添加到$routeMiddleware
进行注册App\Http\Kernel.php
protected $routeMiddleware = [
...
'auth.hmac' => \App\Http\Middleware\HmacAuth::class,
...
];
现在开始真正的工作。记住创建签名字符串的方案。让我们开始实现它。首先,我们检查所需的标头是否在请求中,如果不是,则中止请求。
$header = config('hmac.public');
$request_hash = $request->headers->get($header);
if (!$request_hash) {
$message = 'Header `' . $header . '` missing.';
abort('403', $message);
}
如果匹配,我们将获取字符串,该字符串由 HTTP 方法、URL 和 JSON 格式的请求主体的 MD5 哈希值组成,中间使用换行符连接而成。然后,使用密钥对字符串运行 HMAC SHA-256 哈希算法。最终,该值使用 UTF8 进行 Base64 编码,并与 HMAC 标头中的值进行比较。如果匹配,则可以继续处理请求,否则,中止处理。
最终代码如下:
class HmacAuth
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$header = config('hmac.public');
$request_hash = $request->headers->get($header);
if (!$request_hash) {
$message = 'Header `' . $header . '` missing.';
abort('403', $message);
}
$body = $request->all();
$url = config('hmac.webhook');
$verb = $request->method();
$md5 = md5(json_encode($body));
$string = $verb . PHP_EOL . $url . PHP_EOL . $md5;
$hash = hash_hmac('SHA256', $string, config('hmac.secret'));
$base64_hash = base64_encode($hash);
if ($base64_hash !== $request_hash) {
$message = 'Invalid `' . $header . '` Header';
abort('403', $message);
}
return $next($request);
}
}
就这样。HMAC 签名验证是 API 请求和 webhook 身份验证最简单、最强大的方法之一。
文章来源:https://dev.to/delaneys/hmac-authentication-in-laravel-4je9