强化电商安全:防止客户端篡改的服务器端防御策略
在电商场景中,用户经常会在前端页面修改商品价格、数量、优惠信息等参数,这类客户端篡改行为会直接导致订单金额异常、库存数据错误等安全问题。很多开发者会过度依赖前端的参数校验和HTML属性限制,但实际上客户端的所有代码和数据都可以被用户修改,只有服务器端的防御策略才能从根源上避免篡改风险。本文结合实际电商业务场景,介绍几类核心的服务器端防御方法。
一、核心防御策略解析
1. 关键业务参数不依赖客户端传递
很多电商系统的漏洞来源于直接信任前端传递的商品价格、优惠折扣等参数。正确的做法是在服务器存储商品、优惠活动的权威数据,客户端仅传递商品ID、数量等标识类参数,所有价格和规则都从服务器端实时查询计算。
以下是一段典型的错误实现和正确实现的对比:
<?php
// 错误示例:直接使用客户端传递的价格参数
$productId = $_POST['product_id'];
$clientPrice = $_POST['price']; // 客户端可篡改的数值
$quantity = $_POST['quantity'];
// 直接使用客户端价格计算订单金额,存在篡改风险
$totalAmount = $clientPrice * $quantity;
echo "订单总金额:" . $totalAmount;
// 正确示例:从数据库查询商品权威价格
$productId = $_POST['product_id'];
$quantity = $_POST['quantity'];
// 连接数据库查询商品信息(这里简化为模拟查询)
$productInfo = getProductInfoFromDB($productId);
$serverPrice = $productInfo['price']; // 服务器端存储的权威价格
$totalAmount = $serverPrice * $quantity;
echo "订单总金额:" . $totalAmount;
// 模拟数据库查询函数
function getProductInfoFromDB($pid) {
// 实际场景中这里会执行数据库查询,返回商品的价格、库存等信息
$productList = [
1001 => ['price' => 299.00, 'stock' => 50],
1002 => ['price' => 599.00, 'stock' => 30]
];
return $productList[$pid] ?? null;
}
?>2. 请求参数签名校验
对于部分需要从客户端传递的业务参数,比如用户选择的优惠活动ID、配送方式等,可以采用参数签名的方式验证参数是否被篡改。服务器端和客户端约定相同的签名密钥,将关键参数按照固定规则排序后拼接密钥生成签名,客户端传递参数时附带签名,服务器端重新计算签名并校验是否一致。
以下是签名校验的实现示例:
<?php
// 签名密钥,实际场景中需要妥善保管,不要暴露在前端代码中
$secretKey = "ecommerce_secure_key_2024";
// 客户端传递的参数
$params = [
'product_id' => 1001,
'quantity' => 2,
'coupon_id' => 5,
'timestamp' => time()
];
// 客户端传递的签名
$clientSign = $_POST['sign'] ?? '';
// 签名校验逻辑
function checkSign($params, $secretKey, $clientSign) {
// 排除签名参数本身,按照参数键名升序排序
unset($params['sign']);
ksort($params);
// 拼接参数和密钥
$signStr = '';
foreach ($params as $key => $value) {
$signStr .= $key . '=' . $value . '&';
}
$signStr = rtrim($signStr, '&') . $secretKey;
// 计算服务器端签名(使用sha256算法)
$serverSign = hash('sha256', $signStr);
// 对比签名是否一致,避免时序攻击可使用hash_equals函数
return hash_equals($serverSign, $clientSign);
}
if (checkSign($params, $secretKey, $clientSign)) {
echo "参数校验通过,未检测到篡改";
} else {
echo "参数校验失败,可能存在篡改行为";
}
?>3. 业务规则二次校验
即使参数本身没有被篡改,也需要对业务规则进行服务器端校验,比如用户选择的商品库存是否充足、使用的优惠券是否属于当前用户、优惠活动是否在有效期内等。这些规则不能仅在前端判断,必须全部在服务器端重新校验。
以下是库存和优惠券校验的示例:
<?php
// 模拟从数据库获取的用户信息、商品信息、优惠券信息
$userId = 123;
$productId = 1001;
$quantity = 3;
$couponId = 5;
// 查询商品库存
$productStock = getProductStock($productId);
if ($productStock < $quantity) {
echo "商品库存不足,当前剩余库存:" . $productStock;
exit;
}
// 查询优惠券是否可用
$couponInfo = getCouponInfo($couponId, $userId);
if (empty($couponInfo)) {
echo "优惠券不存在或不属于当前用户";
exit;
}
if ($couponInfo['expire_time'] < time()) {
echo "优惠券已过期";
exit;
}
if ($couponInfo['min_order_amount'] > ($productStock * 299)) { // 这里的价格从服务器查询
echo "订单金额未达到优惠券使用门槛";
exit;
}
echo "订单校验通过,可以提交";
// 模拟查询商品库存
function getProductStock($pid) {
$stockList = [1001 => 50, 1002 => 30];
return $stockList[$pid] ?? 0;
}
// 模拟查询优惠券信息
function getCouponInfo($cid, $uid) {
$couponList = [
5 => ['user_id' => 123, 'expire_time' => strtotime('+7 days'), 'min_order_amount' => 200]
];
$coupon = $couponList[$cid] ?? null;
if ($coupon && $coupon['user_id'] == $uid) {
return $coupon;
}
return null;
}
?>4. 使用不可篡改的HTML属性标记关键数据
虽然HTML本身可以被客户端修改,但我们可以结合服务器端渲染,将关键数据通过隐藏域、数据属性等方式写入HTML,同时配合服务器端的校验。需要注意的是,<input> 标签的 type="hidden" 属性仅能隐藏前端显示,无法防止篡改,必须搭配服务器端校验使用。
以下是服务器端渲染关键数据的示例:
<!-- 服务器端渲染的商品表单,关键数据由服务器生成 -->
<form action="/submit_order" method="post">
<!-- 商品ID由服务器输出,避免客户端篡改 -->
<input type="hidden" name="product_id" value="1001">
<!-- 商品单价由服务器输出,仅做展示,实际计算用服务器查询的价格 -->
<input type="hidden" name="display_price" value="299.00">
<p>商品名称:纯棉T恤</p>
<p>单价:299.00元</p>
<p>购买数量:<input type="number" name="quantity" value="1" min="1" max="10"></p>
<!-- 签名由服务器生成,防止参数篡改 -->
<input type="hidden" name="sign" value="a1b2c3d4e5f6...">
<button type="submit">提交订单</button>
</form>二、防御策略总结
- 所有涉及金额、库存、权限的关键数据,必须存储在服务器端,禁止信任客户端传递的对应参数
- 对客户端传递的关键参数做签名校验,防止参数被中途篡改
- 所有业务规则必须在服务器端重新校验,不能依赖前端判断
- HTML相关标记仅做辅助展示,不能将其作为安全校验的依据
电商系统的安全防御需要前后端配合,但核心的安全底线必须放在服务器端。通过上述策略的落地,可以有效避免绝大多数客户端篡改带来的业务风险,保障平台和用户的资金、数据安全。