如何限制仅指定JavaScript函数向PHP后端提交数据?
解决后端API非法提交问题的方案
你现在的问题是后端仅靠id参数校验,完全可以被伪造,下面给你几个实用的改进方案:
方案1:CSRF令牌验证(Web端最常用防护方式)
CSRF令牌是服务器生成的随机字符串,绑定用户会话,只有从你的页面发起的请求才能拿到正确令牌,伪造请求无法获取。
修改前端JS代码
先在页面<head>里添加后端渲染的CSRF令牌meta标签:
<meta name="csrf-token" content="<?php echo $_SESSION['csrf_token']; ?>">
然后修改saveData函数,在请求头带上CSRF令牌:
async function saveData() { try { let id = getCookie('hhb-id') | 0; // 获取页面中的CSRF令牌 const csrfToken = document.querySelector('meta[name="csrf-token"]').content; const response = await fetch(`api.php?id=${id}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken // 新增CSRF请求头 }, body: JSON.stringify(dictionary) }); if (!response.ok) { let responseData = await response.json(); console.error('保存数据出错(请求异常): ', responseData); } else { console.log('数据保存成功: ', await response.json()); } } catch (error) { console.error('保存数据出错(捕获异常): ', error); } }
修改后端PHP代码
开启会话并生成、验证CSRF令牌:
// 放在PHP文件最顶部,开启会话 session_start(); // 生成CSRF令牌(会话中不存在则创建) if (!isset($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { // 1. 验证CSRF令牌合法性 $csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; if (!hash_equals($_SESSION['csrf_token'], $csrfToken)) { http_response_code(403); echo json_encode(['error' => '非法请求']); exit; } // 2. 验证id与会话绑定(可选,进一步加固) if (isset($_GET['id']) && $_GET['id'] > 0) { // 假设用户登录后,会话中已存储user_id if ($_GET['id'] != $_SESSION['user_id']) { http_response_code(403); echo json_encode(['error' => '身份不匹配']); exit; } try { $data = json_decode(file_get_contents('php://input')); // 保存数据到数据库 ... } catch (Exception $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]); } } else { http_response_code(400); echo json_encode(['error' => '无效的ID']); } }
方案2:会话绑定身份验证
如果用户是登录状态,直接从服务器会话取用户ID,彻底避免依赖前端传的id参数:
修改前端JS代码
可以去掉URL中的id参数:
async function saveData() { try { const csrfToken = document.querySelector('meta[name="csrf-token"]').content; const response = await fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify(dictionary) }); // 错误处理逻辑不变 if (!response.ok) { let responseData = await response.json(); console.error('保存数据出错(请求异常): ', responseData); } else { console.log('数据保存成功: ', await response.json()); } } catch (error) { console.error('保存数据出错(捕获异常): ', error); } }
修改后端PHP代码
session_start(); // 先验证用户是否登录 if (!isset($_SESSION['user_id']) || $_SESSION['user_id'] <= 0) { http_response_code(401); echo json_encode(['error' => '未登录']); exit; } // 验证CSRF令牌 $csrfToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; if (!hash_equals($_SESSION['csrf_token'], $csrfToken)) { http_response_code(403); echo json_encode(['error' => '非法请求']); exit; } if ($_SERVER['REQUEST_METHOD'] === 'POST') { try { $data = json_decode(file_get_contents('php://input')); $userId = $_SESSION['user_id']; // 直接从会话取用户ID // 保存数据到数据库,使用$userId关联 ... } catch (Exception $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]); } }
方案3:请求签名(适合严格场景)
如果要防止请求被篡改或重放,可以生成请求签名,后端验证签名合法性:
修改前端JS代码
// 注意:密钥不能硬编码在前端!需通过后端接口返回与用户绑定的临时密钥,此处仅为示例 const secretKey = 'user-specific-secret-from-server'; async function saveData() { try { let id = getCookie('hhb-id') | 0; const timestamp = Date.now().toString(); const dataStr = JSON.stringify(dictionary); // 生成SHA-256签名 const signatureBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(timestamp + dataStr + secretKey)); const signatureHex = Array.from(new Uint8Array(signatureBuffer)) .map(b => b.toString(16).padStart(2, '0')) .join(''); const response = await fetch(`api.php?id=${id}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Request-Timestamp': timestamp, 'X-Request-Signature': signatureHex }, body: dataStr }); // 错误处理逻辑不变 if (!response.ok) { let responseData = await response.json(); console.error('保存数据出错(请求异常): ', responseData); } else { console.log('数据保存成功: ', await response.json()); } } catch (error) { console.error('保存数据出错(捕获异常): ', error); } }
修改后端PHP代码
session_start(); // 从会话或数据库获取对应用户的密钥 $secretKey = 'user-specific-secret-from-server'; if ($_SERVER['REQUEST_METHOD'] === 'POST') { // 验证时间戳,防止重放(限制5分钟内的请求) $timestamp = $_SERVER['HTTP_X_REQUEST_TIMESTAMP'] ?? ''; if (empty($timestamp) || (time() - intval($timestamp)/1000) > 300) { http_response_code(403); echo json_encode(['error' => '请求已过期']); exit; } // 验证签名 $signature = $_SERVER['HTTP_X_REQUEST_SIGNATURE'] ?? ''; $dataStr = file_get_contents('php://input'); $expectedSignature = hash('sha256', $timestamp . $dataStr . $secretKey); if (!hash_equals($expectedSignature, $signature)) { http_response_code(403); echo json_encode(['error' => '请求被篡改']); exit; } // 后续验证与保存逻辑 if (isset($_GET['id']) && $_GET['id'] > 0) { try { $data = json_decode($dataStr); // 保存数据到数据库 ... } catch (Exception $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]); } } else { http_response_code(400); echo json_encode(['error' => '无效的ID']); } }
注意事项
- 方案1是Web端基础防护,优先使用,配合会话绑定身份验证效果更佳
- 方案3适合非浏览器客户端调用的场景,绝对不能在前端硬编码固定密钥,必须用与用户绑定的动态密钥
- 验证不通过时,返回对应HTTP状态码(401未登录、403非法请求等),不要静默处理
内容的提问来源于stack exchange,提问作者Kugelfisch
相关产品推荐
相关产品推荐

