Slim PHP API与Angular跨域问题:Http 0 Unknown Error
问题现象
向 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, ]));
问题原因
- 中间件执行顺序错误:当前JWT验证中间件在CORS中间件之前执行,导致OPTIONS预检请求(不带token)直接被JWT中间件拦截返回401,此时CORS头还未被添加,浏览器无法正确处理响应,抛出CORS错误。
- 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");
验证修复
- 重启Slim后端服务,确保中间件顺序正确。
- 在Angular应用中重新发起请求,检查控制台是否还有CORS错误。
- 查看浏览器网络请求的响应头,确认
Access-Control-Allow-Credentials: true和Access-Control-Allow-Origin: http://localhost:8100已正确返回。
内容的提问来源于stack exchange,提问作者Conta

