45 lines
1.8 KiB
PHP
45 lines
1.8 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace app\mcp\service;
|
||
|
||
use think\Request;
|
||
use think\Response;
|
||
|
||
/**
|
||
* 请求级防护:浏览器 Origin 校验(防 DNS 重绑定)、来源 IP 白名单、401 响应格式。
|
||
*/
|
||
class Guard
|
||
{
|
||
/** 返回 null 表示放行,否则返回 [HTTP 状态码, 原因, reason] */
|
||
public static function check(Request $request): ?array
|
||
{
|
||
$origin = trim((string) $request->header('origin', ''));
|
||
if ($origin !== '' && !in_array(rtrim($origin, '/'), array_map(static fn ($o) => rtrim($o, '/'), McpConfig::allowedOrigins()), true)) {
|
||
return [403, 'Origin not allowed', 'origin_not_allowed'];
|
||
}
|
||
$ips = McpConfig::allowedIps();
|
||
if ($ips && !in_array($request->ip(), $ips, true)) {
|
||
return [403, '来源 IP 不在 AI 助手白名单内', 'ip_not_allowed'];
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** MCP 端点的 401:JSON-RPC 错误体 + WWW-Authenticate */
|
||
public static function unauthorized(McpException $e): Response
|
||
{
|
||
$body = ['jsonrpc' => '2.0', 'id' => null, 'error' => ['code' => -32001, 'message' => $e->getMessage(), 'data' => ['reason' => $e->reason]]];
|
||
return json($body, 401)->header(['WWW-Authenticate' => 'Bearer error="invalid_token", error_description="' . $e->reason . '"']);
|
||
}
|
||
|
||
/** REST 接口的统一信封(与后台 JsonService 一致) */
|
||
public static function envelope(int $code, string $msg, $data = [], int $httpStatus = 200, int $show = 0): Response
|
||
{
|
||
$response = json(['code' => $code, 'show' => $show, 'msg' => $msg, 'data' => $data ?: new \stdClass()], $httpStatus);
|
||
if ($httpStatus === 401) {
|
||
$response->header(['WWW-Authenticate' => 'Bearer error="invalid_token"']);
|
||
}
|
||
return $response;
|
||
}
|
||
}
|