,且必须通过 JWT 签名与 plat=api 校验 */ class CheckAuthTokenMiddleware implements MiddlewareInterface { /** 不需要 auth-token 的路径 */ private const WHITELIST = [ 'api/authToken', ]; /** JWT 至少为 xxx.yyy.zzz 三段 */ private const JWT_PARTS_MIN = 3; public function process(Request $request, callable $handler): Response { $path = trim((string) $request->path(), '/'); if ($this->isWhitelist($path)) { return $handler($request); } $token = $this->getAuthTokenFromRequest($request); if ($token === '') { throw new ApiException('请携带 auth-token', ReturnCode::UNAUTHORIZED); } if (!$this->looksLikeJwt($token)) { throw new ApiException('auth-token 格式无效', ReturnCode::TOKEN_INVALID); } $decoded = $this->verifyAuthToken($token); $extend = $decoded['extend'] ?? []; if (($extend['plat'] ?? '') !== 'api') { throw new ApiException('auth-token 无效(非 API 凭证)', ReturnCode::TOKEN_INVALID); } // 同一设备只允许一个 auth-token 生效,非当前 token 视为已失效 $device = (string) ($extend['device'] ?? ''); if ($device !== '' && !AuthTokenCache::isCurrentToken($device, $token)) { throw new ApiException('auth-token 已失效(该设备已签发新凭证,请使用新 auth-token)', ReturnCode::TOKEN_INVALID); } return $handler($request); } private function getAuthTokenFromRequest(Request $request): string { $token = $request->header('auth-token'); if ($token !== null && $token !== '') { return trim((string) $token); } $auth = $request->header('authorization'); if ($auth && stripos($auth, 'Bearer ') === 0) { return trim(substr($auth, 7)); } return ''; } private function looksLikeJwt(string $token): bool { $parts = explode('.', $token); return count($parts) >= self::JWT_PARTS_MIN; } /** * 校验 auth-token 有效性(签名、过期、iss 等),无效或过期必抛 ApiException */ private function verifyAuthToken(string $token): array { try { return JwtToken::verify(1, $token); } catch (JwtTokenExpiredException $e) { Log::error('auth-token 已过期, 报错信息' . $e); throw new ApiException('auth-token 已过期', ReturnCode::TOKEN_INVALID); } catch (JwtTokenException $e) { Log::error('auth-token 无效, 报错信息' . $e); throw new ApiException($e->getMessage() ?: 'auth-token 无效', ReturnCode::TOKEN_INVALID); } catch (\Throwable $e) { Log::error('auth-token 校验失败, 报错信息' . $e); throw new ApiException('auth-token 校验失败', ReturnCode::TOKEN_INVALID); } } private function isWhitelist(string $path): bool { foreach (self::WHITELIST as $prefix) { if ($path === $prefix || str_starts_with($path, $prefix . '/')) { return true; } } return false; } }