You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Slim PHP API与Angular跨域问题:Http 0 Unknown Error

Angular 调用 Slim PHP API 时的 CORS 问题排查与修复

问题现象

向 http://localhost:8080/api/validate 发送POST请求时触发CORS错误,控制台报错如下:

XHROPTIONS http://localhost:8080/api/validate CORS Missing Allow Credentials
跨源请求被阻止:源匹配策略不允许读取http://localhost:8080/api/validate的远程资源,原因:CORS头“Access-Control-Allow-Credentials”需为“true”。
跨源请求被阻止:源匹配策略不允许读取http://localhost:8080/api/validate的远程资源,原因:CORS请求失败,状态码:(null)。
Http failure response for http://localhost:8080/api/validate: 0 Unknown Error

已确认:后端运行在localhost:8080,Postman请求API正常,Angular应用无JS语法错误。

现有代码

JWT验证中间件

$fitnetApi->slim->add(function (Request $request, Response $response, callable $next) {
    $uri = $request->getUri()->getPath();
    
    // 检查URI是否以'/api'开头
    if (strpos($uri, '/api') === 0) {
        $cookies = $request->getCookieParams();
        $token = $cookies['jwt_token'] ?? null; // 使用null合并运算符

        // 如果没有token,直接返回401
        if (!$token) {
            return $response
            ->withHeader("Content-Type", "application/json")
            ->withStatus(401)
            ->write(json_encode([
                "status" => "error",
                "message" => "token invalid"
            ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
        }

        try {
            // 解码token
            $decoded = JWT::decode($token, new Key('', 'HS256'));

            // 检查token是否过期
            if (isset($decoded->exp) && $decoded->exp < time()) {
                
                $rtoken = $cookies['rjwt_token'] ?? null;;
                $decRjwt = JWT::decode($rtoken, new Key('', 'HS256'));

                // 检查rtoken是否存在且未过期
                if ($decRjwt && isset($decRjwt->exp) && $decRjwt->exp > time()) {
                    // 重新生成token
                    $newToken = JWT::encode(
                        [
                            'id' => $decoded->id,
                            'type' => $decoded->type,
                            'exp' => time() + 60 * 15 // 15分钟
                        ],
                        "",
                        "HS256"
                    );

                    // 将新token添加到请求属性中
                    $request = $request->withAttribute('new_token', $newToken);
                } else {
                    // 如果rtoken过期,返回错误
                    return $response
                        ->withHeader("Content-Type", "application/json")
                        ->withStatus(401)
                        ->write(json_encode([
                            "status" => "error",
                            "message" => "rtoken expired, renew auth"
                        ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
                }
            }
        } catch (ExpiredException $e) {
            // 处理JWT过期情况
            return $response
                ->withHeader("Content-Type", "application/json")
                ->withStatus(401)
                ->write(json_encode([
                    "status" => "error",
                    "message" => "Token expired"
                ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
        } catch (SignatureInvalidException $e) {
            // 处理JWT签名无效情况
            return $response
                ->withHeader("Content-Type", "application/json")
                ->withStatus(401)
                ->write(json_encode([
                    "status" => "error",
                    "message" => "Invalid token signature"
                ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
        } catch (\Exception $e) {
            // 通用JWT异常处理
            return $response
                ->withHeader("Content-Type", "application/json")
                ->withStatus(401)
                ->write(json_encode([
                    "status" => "error",
                    "message" => "Invalid token"
                ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
        }
    }

    // 继续执行下一个中间件或路由
    return $next($request, $response);
});

CORS中间件配置

$fitnetApi->slim->add(new Tuupola\Middleware\CorsMiddleware([
    "origin" => ["http://localhost:8100"],
    "methods" => ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
    "headers.allow" => ["Token", "Authorization", "If-Match", "If-Unmodified-Since", "Content-Type"],
    "headers.expose" => ["Authorization"],
    "credentials" => true,
    "cache" => 0,
]));

问题原因

  1. 中间件执行顺序错误:当前JWT验证中间件在CORS中间件之前执行,导致OPTIONS预检请求(不带token)直接被JWT中间件拦截返回401,此时CORS头还未被添加,浏览器无法正确处理响应,抛出CORS错误。
  2. OPTIONS请求未被正确处理:预检请求不需要验证JWT,应该直接由CORS中间件处理并返回正确的跨域头,而不是进入JWT验证逻辑。

修复步骤

1. 调整中间件执行顺序

确保CORS中间件最先执行,这样所有请求(包括OPTIONS预检)都会先经过CORS处理,添加必要的跨域头。在Slim框架中,中间件的添加顺序决定了执行顺序,后添加的中间件先执行,所以需要先添加CORS中间件,再添加JWT验证中间件:

// 先添加CORS中间件
$fitnetApi->slim->add(new Tuupola\Middleware\CorsMiddleware([
    "origin" => ["http://localhost:8100"],
    "methods" => ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
    "headers.allow" => ["Token", "Authorization", "If-Match", "If-Unmodified-Since", "Content-Type"],
    "headers.expose" => ["Authorization"],
    "credentials" => true,
    "cache" => 0,
]));

// 再添加JWT验证中间件
$fitnetApi->slim->add(function (Request $request, Response $response, callable $next) {
    // 原JWT中间件代码...
});

2. 让JWT中间件跳过OPTIONS请求

即使调整了顺序,也可以在JWT中间件中直接跳过OPTIONS请求,避免不必要的验证:

$fitnetApi->slim->add(function (Request $request, Response $response, callable $next) {
    // 跳过OPTIONS预检请求
    if ($request->getMethod() === "OPTIONS") {
        return $next($request, $response);
    }

    $uri = $request->getUri()->getPath();
    
    // 原JWT验证逻辑...
});

3. 确保Angular请求携带凭据

在Angular的HTTP请求中,需要设置withCredentials: true,让浏览器自动携带Cookie:

import { HttpClient } from '@angular/common/http';

// 在请求时配置
this.http.post('http://localhost:8080/api/validate', body, { withCredentials: true })
  .subscribe(response => {
    // 处理响应
  });

4. 检查JWT密钥配置(可选)

注意到代码中JWT解码和编码使用的是空字符串密钥,这可能导致token验证失败,建议使用实际的密钥:

// 替换为空字符串的地方为实际密钥
$key = "your-real-secret-key-here";
$decoded = JWT::decode($token, new Key($key, 'HS256'));
// 编码时也使用相同密钥
$newToken = JWT::encode(..., $key, "HS256");

验证修复

  1. 重启Slim后端服务,确保中间件顺序正确。
  2. 在Angular应用中重新发起请求,检查控制台是否还有CORS错误。
  3. 查看浏览器网络请求的响应头,确认Access-Control-Allow-Credentials: true和Access-Control-Allow-Origin: http://localhost:8100已正确返回。

内容的提问来源于stack exchange,提问作者Conta

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 18:55:11