// +---------------------------------------------------------------------- declare(strict_types=1); namespace ywxapp\middleware; use Closure; use think\App; use think\Request; use think\Response; /** * 分层 CORS 中间件(替代 think\middleware\AllowCrossDomain 的全站 * 放开)。 * * 规则读取 config/cors.php 的 app_cross:按应用名取白名单。 * - '*' :全放开(无凭证模式,适合公开 API) * - 'a.com,b.com' :精确白名单(带凭证模式) * - '*.example.com' :通配子域 * - ''(默认) :完全不输出 CORS 头 = 禁止跨域(同源页面不受影响) * * 典型部署:仅在 app/api/middleware.php 注册,backend/member/frontend 不注册, * 即实现「仅 API 可跨域、后台/会员零跨域」的分层策略。 */ class CrossDomain { public function __construct(protected App $app) { } public function handle(Request $request, Closure $next): Response { $origin = trim((string) $request->header('origin', '')); $allow = $origin === '' ? '' : $this->resolveOrigin($origin); // 预检请求:白名单内返回带 CORS 头的 204,否则裸 204(浏览器视为跨域失败) if (strtoupper($request->method()) === 'OPTIONS') { $response = Response::create('', 'html', 204); return $allow === '' ? $response : $response->header($this->headers($allow)); } $response = $next($request); if ($allow !== '') { $response->header($this->headers($allow)); } return $response; } /** * 解析 Origin 是否被当前应用的白名单允许。 * * @return string 允许时回显 Origin(或 '*'),不允许返回 '' */ protected function resolveOrigin(string $origin): string { $map = (array) $this->app->config->get('cors.app_cross', []); $appName = $this->app->http->getName() ?: 'index'; $rule = (string) ($map[$appName] ?? ($map['default'] ?? '')); if ($rule === '') { return ''; } if ($rule === '*') { return '*'; } $list = array_values(array_filter(array_map('trim', explode(',', $rule)))); if (in_array($origin, $list, true)) { return $origin; } // 通配子域:*.example.com $originHost = (string) (parse_url($origin, PHP_URL_HOST) ?: ''); foreach ($list as $item) { if ($item !== '' && str_starts_with($item, '*.')) { $suffix = substr($item, 1); // ".example.com" if ($originHost !== '' && str_ends_with($originHost, $suffix)) { return $origin; } } } return ''; } /** * 组装 CORS 响应头。 */ protected function headers(string $allow): array { $config = $this->app->config->get('cors', []); $headers = [ 'Access-Control-Allow-Origin' => $allow, 'Access-Control-Allow-Methods' => $this->join($config['allowed_methods'] ?? 'GET, POST, PUT, PATCH, DELETE, OPTIONS'), 'Access-Control-Allow-Headers' => $this->join($config['allowed_headers'] ?? 'Content-Type, Authorization, X-Requested-With'), 'Access-Control-Max-Age' => (string) ($config['max_age'] ?? 86400), 'Vary' => 'Origin', ]; // 白名单模式下才允许携带凭证('*' 时浏览器强制忽略 credentials) if ($allow !== '*' && !empty($config['supports_credentials'])) { $headers['Access-Control-Allow-Credentials'] = 'true'; } return $headers; } /** * 归一化 CORS 列表项:config/cors.php 的 allowed_methods / allowed_headers * 统一存成数组(think-cors 的 HandleCors 强制要求数组), * 但历史上/手工改配置可能是逗号分隔字符串,故两种都兼容。 */ protected function join($value): string { if (is_array($value)) { return implode(', ', array_filter(array_map('strval', $value), static fn ($v) => trim($v) !== '')); } return (string) $value; } }