manghe/app/admin/middleware/DomainBind.php
2025-04-10 02:46:53 +08:00

220 lines
6.4 KiB
PHP
Executable File

<?php
/**
* 域名绑定中间件
* 用于检查请求域名是否在允许的列表中
*/
namespace app\admin\middleware;
use think\facade\Config;
use think\exception\HttpResponseException;
use think\Response;
class DomainBind
{
/**
* 处理请求
*
* @param \think\Request $request
* @param \Closure $next
* @return \think\Response
*/
public function handle($request, \Closure $next)
{
// 获取当前请求域名
$domain = $request->host();
// 获取当前请求路径
$path = $request->pathinfo();
// 白名单路径检查 - 不需要验证域名的路径
$whitelistPaths = $this->getWhitelistPaths();
foreach ($whitelistPaths as $whitePath) {
// 支持简单的路径匹配,如 'notify/*' 匹配所有通知路径
if ($this->pathMatch($whitePath, $path)) {
// 白名单路径,跳过域名检查
\think\facade\Log::info('白名单路径访问: ' . $path . ', 域名: ' . $domain);
return $next($request);
}
}
// 检查域名是否允许
if (!$this->checkDomain($domain)) {
// 记录被拒绝的请求
\think\facade\Log::warning('域名访问被拒绝: ' . $domain . ', 路径: ' . $path);
return $this->error('未授权,无法访问', 403);
}
// 域名允许,记录请求信息(可选,仅用于调试)
// \think\facade\Log::info('域名访问通过: ' . $domain . ', 路径: ' . $path);
// 域名允许,继续处理请求
return $next($request);
}
/**
* 检查域名是否在允许列表中
*
* @param string $domain 请求域名
* @return bool 是否允许
*/
protected function checkDomain($domain)
{
// 获取所有允许的域名
$allowedDomains = $this->getAllowedDomains();
// 如果没有设置允许的域名,默认放行所有域名
if (empty($allowedDomains)) {
return true;
}
// 检查域名是否在允许列表中
foreach ($allowedDomains as $allowedDomain) {
// 支持通配符匹配,例如 *.example.com
if ($this->domainMatch($allowedDomain, $domain)) {
return true;
}
}
// 域名不在允许列表中
return false;
}
/**
* 获取所有允许的域名
*
* @return array 允许的域名列表
*/
protected function getAllowedDomains()
{
$allowedDomains = [];
// 1. 从配置文件中获取额外允许的域名
try {
$configDomains = Config::get('api.admin_domains', []);
if (!empty($configDomains) && is_array($configDomains)) {
foreach ($configDomains as $d) {
$d = trim($d);
if (!empty($d) && !in_array($d, $allowedDomains)) {
$allowedDomains[] = $d;
}
}
}
} catch (\Exception $e) {
// 配置获取失败,记录日志
\think\facade\Log::error('获取API域名配置失败: ' . $e->getMessage());
}
// 3. 记录可用域名到日志,用于调试
// \think\facade\Log::info('允许的域名列表: ' . json_encode($allowedDomains));
return $allowedDomains;
}
/**
* 检查域名是否匹配
*
* @param string $pattern 配置中的域名模式
* @param string $domain 当前请求的域名
* @return bool 是否匹配
*/
protected function domainMatch($pattern, $domain)
{
// 完全匹配
if (strtolower($pattern) === strtolower($domain)) {
return true;
}
// 通配符匹配 (例如: *.example.com)
if (strpos($pattern, '*') !== false) {
$pattern = str_replace('*', '.*', $pattern);
$pattern = '/^' . str_replace('.', '\.', $pattern) . '$/i';
return preg_match($pattern, $domain);
}
return false;
}
/**
* 返回错误信息
*
* @param string $msg 错误信息
* @param int $code 错误码
* @return \think\Response
*/
protected function error($msg, $code = 0)
{
$result = [
'status' => $code,
'msg' => $msg,
'data' => null
];
$response = Response::create($result, 'json', $code);
throw new HttpResponseException($response);
}
/**
* 中间件结束调度
*
* @param \think\Response $response
* @return void
*/
public function end(\think\Response $response)
{
// 可以在响应发送前进行一些后置处理
}
/**
* 获取路径白名单
*
* @return array 白名单路径列表
*/
protected function getWhitelistPaths()
{
// 1. 默认白名单路径(如支付回调通知等)
$defaultWhitelist = [
'notify/*', // 支付回调等通知
'health', // 健康检查
'debug', // 调试接口
];
// 2. 从配置文件中获取白名单路径
try {
$configWhitelist = Config::get('api.whitelist_paths', []);
if (!empty($configWhitelist) && is_array($configWhitelist)) {
return array_merge($defaultWhitelist, $configWhitelist);
}
} catch (\Exception $e) {
\think\facade\Log::error('获取API白名单路径配置失败: ' . $e->getMessage());
}
return $defaultWhitelist;
}
/**
* 路径匹配检查
*
* @param string $pattern 白名单路径模式
* @param string $path 请求路径
* @return bool 是否匹配
*/
protected function pathMatch($pattern, $path)
{
// 完全匹配
if ($pattern === $path) {
return true;
}
// 通配符匹配 (例如: 'notify/*')
if (strpos($pattern, '*') !== false) {
$pattern = str_replace('*', '.*', $pattern);
$pattern = '/^' . str_replace('/', '\/', $pattern) . '$/i';
return preg_match($pattern, $path);
}
return false;
}
}