Laravel的HMAC身份验证
#php #laravel #api #authentication

Security, Security, Security!

最近在一组API Webhooks工作时,我需要提供一些安全方案,使客户可以验证我正在处理的正确服务器发送Webhook请求。经过一番在线研究,我遇到了HMAC计划的API请求计划。经过一点解释,我将举一个为Laravel中的API端点实施的示例。

基于哈希的消息身份验证代码(HMAC)是使用加密哈希功能,数据和秘密获得的代码。它用于验证请求源和内容的真实性。客户端和服务器都共享用于生成HMAC的预先秘密密钥。

用更简单的话来说,只有服务器和客户端共享并知道一个秘密密钥。 Hash函数使用此键在提出请求时生成代码。然后将此代码添加到请求中并发送。当接收应用程序获取请求时,它运行与创建代码相同的过程,使用刚收到的请求和已存储的秘密密钥的详细信息。如果发件人确实是他们本应成为的人,则请求中的代码和刚刚生成的代码都应匹配,并且接收器可以继续处理请求。很干净,对

这是一种简单而有效的身份验证方法,因为完整性检查不允许中间攻击,除非它们拥有秘密钥匙。但是,一个坏人可以一遍又一遍地重新提出相同的请求,这可能会损害数据或揭示一些敏感信息。这些被称为重播攻击,有多种处理方法,但是我不会谈论任何问题,以使其尽可能简单。

现在可以快速解释我们的哈希功能。我们将使用的值是:

  • url :小写的完整URL,包括任何查询参数。
  • 动词:大写速度中请求的HTTP方法,例如发帖。
  • 内容MD5 :JSON中请求正文的MD5哈希

我们将采取的步骤创建哈希代码:

  1. 使用上面的值创建一个符号字符串。
  2. 使用字符串和我们的秘密密钥生成哈希。
  3. base64编码哈希并将其连接到请求标题

可以使用其他值,例如请求内容类型,但我会保持简单。通常在API文档中描述了用于创建用于哈希的签名字符串的每个API的方案。

配置

要启动,请旋转一个新的Laravel应用程序并添加几个控制器以添加我们的API逻辑。

php artisan make:controller UsersController
php artisan make:controller ClientController

在路由/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中的$routeMiddleware
进行注册。

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哈希的串联,即使用newlines的串联。然后,我们使用秘密键,然后在字符串上运行HMAC SHA-256哈希算法。该值最终使用UTF8编码,并将其与我们的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身份验证的最简单,最强大的方法之一。