尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

前端校验与后端校验的区别:为什么后端校验是安全底线?

前端校验与后端校验的区别:为什么后端校验是安全底线? 1. 一次“改价支付”事故让我重新审视校验问题前阵子一个做电商的朋友找我排查线上问题说有人用一张满100减30的优惠券买走了标价1200块钱的商品最后实付金额是个诡异的小数。查了半天突破口居然在一行只有前端校验、没有后端校验的代码上。攻击者把请求拦截下来改掉价格参数再重放后端居然照单全收。这个场景你可能听过——绕过后端余额校验。它字面上说的是余额但本质是同一件事系统把不该信任的输入当成了可信输入把只该存在于用户体验层的检查当成了安全边界。这恰恰是前端校验和后端校验区别中最容易被低估的部分。很多人觉得两者只是“一个在浏览器里转一个在服务器上跑”顶多差点体验和性能。但真正经历过一次线上资损或者被恶意请求打穿过一次你就会明白前端校验和后端校验根本不是同一层级的东西。先说结论再展开讲透前端校验管体验、管提示、管即时反馈它做的事是“用户不该提交这种数据”。后端校验管安全、管业务规则、管系统最终一致性它做的事是“无论谁提交什么数据系统都不允许跑出错误状态”。前端校验只是后端校验的可用性辅助永远不能替代后端校验。一旦把业务底线暴露给前端决定风险就只是“想不想打”的区别而不是“能不能打”的区别。2. 前端校验的本质体验层的辅助设施2.1 前端校验到底在管什么前端校验名字已经说清楚了——它在浏览器里、在JavaScript执行环境下运行的校验逻辑。它管的事情非常具体字段必填用户名不能为空、手机号必须11位。格式检查邮箱格式对不对、身份证号码是否符合规则。输入范围年龄需要在0到120之间、数量不能是负数。业务规则的前置判断余额是否足够、库存是否够用、操作状态是否允许。它最大的特点是即时性。用户输入一个非法邮箱按下Tab键的那一刻页面上就弹出了“格式不正确”的提示。用户不需要把请求发到服务器再等响应这就是前端校验最大的价值——节约请求、提升体验、引导用户正确操作。但请注意这里的“业务规则前置判断”是非常讨巧的说法。所谓前置判断意味着它只是“判断”不是“执行”。它告诉用户“你的余额不够”但绝不代表“后端也认为余额不够”。它更像是一个引导员而不是一个保安。2.2 前端校验的定位与天然缺陷我知道有读者会问既然前端校验这么“弱”那还写它干嘛直接把校验全部放后端前端什么都不做不香吗答案是不行。如果什么校验都在后端做用户填错一个格式请求就要走一轮完整的网络往返服务器要处理、要返回用户要等、要重新输入。在弱网环境下这个体验是灾难性的。所以前端校验有它不可替代的定位它是用户体验的第一道关卡。它拦截了绝大多数“正常用户的错误输入”。它为了减少了后端无谓的请求压力。但它的缺陷同样致命。**所有在前端跑的代码对用户都是透明的。**一个浏览器开发者工具甚至一个简单的抓包工具就能让用户看到校验规则、绕过校验规则。前端代码可以被断点调试、被修改、被删除。JavaScript是解释型语言它永远不可能对用户隐藏逻辑。更关键的是前端校验就算写得再严格它解决的只是“正常用户别填错”而不是“恶意用户别攻击”。一个人只要有基本的开发工具使用能力就能轻松绕过页面上所有的校验规则直接构造请求体发给服务器。2.3 一个让人误会的危险信号我最担心的是有些前端框架的校验方案做得很完善表单校验、自定义校验器、异步校验、服务端校验结果回填样样齐全。于是有人产生了幻觉“我们的校验已经闭环了后端不用重复写了。”这个幻觉的入口很具体很多前后端分离项目后端接口文档里会写着“该字段格式由前端校验”“该参数可选前端已做限制”。接口文档里出现这类描述往往就是校验职责偏移的开始。我在实际项目里见过最典型的例子一个下单接口前端做了非常漂亮的金额校验余额不足时按钮置灰、弹窗提示、跳转充值页一气呵成。看起来用户体验极好产品经理也很满意。但是后端接口接收金额参数时连正负号、最大值、精确位数都没有检查。结果就是一个懂行的用户花三分钟绕过前端把一个负数金额加进购物车下单后系统账面直接多出一笔负收入。前端校验可以做得非常漂亮漂亮到让你忘记它是可以被跳过的。3. 后端校验的本质安全边界与业务兜底3.1 后端校验为什么是唯一可信的后端校验之所以被行业反复强调“必须做”原因只有一个它是校验逻辑运行在你自己掌控的服务器环境中用户无法直接接触、篡改。前端校验运行在用户浏览器里用户想改就改。后端校验运行在你自己服务器上请求到达这里之前要经过网络传输、网关转发任何前端能做的事情在这里都影响不到。只有把校验逻辑放在这里校验的结果才是可信的。所以后端校验管的事情比前端校验多得多也重得多参数级校验类型、长度、格式、枚举值、范围。身份级校验当前用户有没有权限做这个操作。业务级校验余额是否充足、库存是否够用、状态是否可以流转。逻辑级校验操作顺序是否合法、数据组合是否合理、是否在允许的时间范围内。如果说前端校验在管“用户该怎么操作”后端校验管的就是“系统到底允不允许这件事发生”。3.2 后端校验的分层拆解很多刚入行的开发者以为后端校验就是“在Controller里if一下”。但真正健壮的后端校验是有层次的。我自己一般把后端校验分成三层层级作用常见实现接口层校验拦截非法请求格式快速返回错误Spring Boot中的Validated、Java Bean Validation注解服务层业务校验验证业务规则是否成立保持核心业务一致Service方法中显式的规则判断、领域模型校验数据层约束数据库层面的最后防线防绕过逻辑唯一约束、CHECK约束、外键约束、字段非空与非负约束这三层是层层递进的。接口层校验拦截的是“格式不对”的请求服务层校验拦截的是“业务不对”的操作数据层约束拦截的是“代码写错了”的情况。没有哪一层可以替代另一层。拿余额校验来举例。一个最简单的转账场景下接口层校验转账金额是否为数字、是否大于0、是否超过两位小数。服务层校验用户账户是否存在、余额是否大于等于转账金额、账户状态是否正常。数据层余额字段设置为非负、配合事务与锁避免并发场景下多笔支出把余额扣成负数。这三层都做到才叫一个可靠的后端校验。少任何一层都可能出现线上事故。尤其是第三层的数据约束很多项目根本不重视甚至用软删除字段替代唯一约束用代码逻辑替代数据库约束。但凡代码有些并发的边界没处理好资损就来了。3.3 核心原则“永远不要相信客户端输入”是怎么落地安全领域有一句老话翻译过来就是“永远不要相信用户输入”。放在前后端校验这个话题里这句话的含义就几个字客户端的任何东西都不可信。怎么落地我总结了三个具体的操作习惯所有接口的默认姿态是拒绝不是放行。先做校验再执行逻辑校验不通过就直接返回错误不进入业务逻辑。需要客户端传什么后端就显式地要什么。不要让客户端决定金额、折扣、单价、状态这些业务关键字段就算可以让客户端传也要在后台重新计算或严格校验。敏感参数使用服务端会话数据。用户ID、角色、余额这些数据优先从服务端Session、Token解析或数据库查询获得而不是信任前端传上来的ID字符串。这三个习惯写出来很简单但真正贯彻执行到每个接口需要团队形成肌肉记忆。尤其是第二条很多开发为了提高“接口灵活性”喜欢留一个“扩展字段”结果扩展字段被塞进去价格参数绕过校验的重放攻击就这样发生了。4. 绕过后端余额校验的作弊链路以及怎么防4.1 攻击者的视角校验薄弱点在哪儿既然要防绕过首先要搞清楚攻击者会怎么想。站在攻击者视角系统里最有价值的东西就是“能让数据偏离预期”的输入点。余额校验相关的常用攻击路径有这么几条路径一前端校验绕过。打开浏览器开发者工具修改页面脚本或DOM元素把数量改成负数、把单价改成0.01、把优惠金额改成超大值。前端校验完全失效因为校验规则在浏览器里攻击者随时可以删掉。路径二参数篡改重放。通过抓包工具拦截下单请求把请求中的金额字段从“100.00”改成“0.01”再模拟发送给服务器。后端如果拿着这个金额做扣款和入账账面就乱了。这种攻击不需要懂任何开发只需要会使用抓包工具。路径三绕过业务状态规则。系统设计“先充值、再支付、后退款”的流程攻击者跳过中间某个状态直接调用退款接口。如果后端没有状态机的校验退款金额就会凭空产生。路径四并发竞争绕过。一个账户余额只有100块攻击者同时发起10笔扣款99块的请求。如果后端没有加锁、没有数据库约束每笔请求都读到余额是100都校验通过结果10笔请求全部扣款成功余额变成负的800块。看到没有攻击者根本不需要“攻破”什么只需要找到校验的缝隙。而所有的缝隙都指向同一个问题后端没有在业务规则层面做彻底的校验或者做了校验收不住并发。4.2 防绕过加固的实操配置针对上面这些路径后端防护的核心不是把代码写得更“保密”而是把校验和约束放到即使看到代码也无法绕过的地方。我在项目中实际落实过一套配置整理出来给大家参考第一关键字段后端重算不信任前端传入。以订单金额为例正确做法是后端根据商品单价、数量、优惠策略、用户等级重新计算应付金额。前端传的金额只作为展示参考不作为最终计费依据。// 错误示范直接信任前端传来的实付金额 BigDecimal payAmount request.getPayAmount(); // 正确做法后端根据商品与规则重新计算 BigDecimal skuPrice skuService.getPrice(skuId); BigDecimal totalAmount skuPrice.multiply(new BigDecimal(quantity)); BigDecimal discountAmount promotionService.calcDiscount(userId, skuId, totalAmount); BigDecimal payAmount totalAmount.subtract(discountAmount);第二余额扣减使用数据库行锁或乐观锁。防止并发下同一笔余额被多次扣减。乐观锁的典型实现是更新时带上版本号UPDATE account SET balance balance - #{amount}, version version 1 WHERE id #{accountId} AND version #{oldVersion} AND balance #{amount}这一条SQL把“余额够不够”和“并发不冲撞”一起解决了。如果更新影响行数为0说明余额不足或者版本号对不上业务就要回滚并报错。第三业务状态流转用状态机校验。不要在每个接口里只判断“当前状态是不是X”而要说清楚“什么状态可以迁移到什么状态”。订单状态从“待支付”只能到“已支付”不能直接跳到“已退款”。用一张状态机表控制凡是走不通的迁移一律拒绝。第四数据层加上兜底约束。就算业务代码全写对了数据库层面也要加上约束作为最后一道防线ALTER TABLE account ADD CONSTRAINT chk_balance_not_negative CHECK (balance 0);有了这条约束哪怕业务代码出了并发bug余额也不会在数据库层面变成负数。数据库会直接报错让你知道哪个环节出了问题。没有这条约束余额早早变成负数后面排查已经晚了。4.3 前端体验和后端安全怎么优雅共存讲了这么多“前端校验不可信”不是说前端校验不要写了。恰恰相反一个合格的项目两者都需要。关键在于怎么划分边界让它们各司其职。我习惯把抓包理解为“用户体验部分”和“安全与正确性部分”。前端校验只管体验层面的东西比如必填提示、格式提示、按钮置灰。后端校验管安全与正确性所有业务规则、权限控制、数据一致性判断都在这里做。举一个下单页面的例子前端做的用户点了“提交订单”先检查手机号格式对不对、收货地址是不是填了、商品数量是不是正整数。不对就立刻弹出提示不打请求。后端做的校验用户是否登录、地址是否属于该用户、库存是否充足、价格是否与后端一致、余额是否足够、订单状态能否流转。任何一步失败都返回明确的业务错误码。前端校验是“友好”后端校验是“可信”。两者不冲突但是边界必须清晰。前端校验做得再好也不能越俎代庖去决定业务规则执行与否后端校验做得再全也不能替前端承担体验优化的工作。5. 我在项目中踩过的校验相关的坑如果说前四节是方法论那这一节就是实践中的血和泪。校验相关的坑我踩过不止一次有些甚至是在生产环境反复出现后才彻底解决的。5.1 坑一开发环境“顺手”关掉了后端校验这个坑是团队协作中最常见的。某个接口在联调阶段前端反复传错参数后端开发为了调通流程直接在接口里注释了参数校验代码或者给校验逻辑加了一个“只在生产环境生效”的开关。联调通过了代码合并了上线了校验逻辑还是关着的。我后来定了一条铁规矩后端校验代码不允许有任何开关不允许注释不允许“临时绕过”。联调时参数不对就让前端改或者让后端调整接口定义而不是放松校验。校验代码一旦欠下技术债后面出的事故远比联调多花的几小时严重。5.2 坑二校验逻辑只出现在Controller漏了内部调用很多项目只在Controller层写校验认为“外部请求入口就在这里校验一遍就够了”。但实际项目里一个Service方法可能会被多个Controller调用甚至被定时任务、消息队列消费者调用。假如Service内部没有校验某条调用链路上漏了校验数据就会以超出预期的形态进入核心逻辑。举一个真实的例子一个订单导出功能Controller入口做了状态校验只能导出“已完成”的订单。但是内部定时任务直接调用了Service层的导出方法绕过了Controller的校验。结果定时任务在业务高峰期把“处理中”的订单也导出去了合作方收到错乱数据客诉直接爆掉。从那之后我的规范是业务规则校验写进Service层尽量靠近数据变更位置而不是只写在Controller入口。这样不管从哪个入口进来校验都不会漏。5.3 坑三前端校验规则和后端校验规则不一致前端允许输入小数后端只接整数前端提示“最大长度是50”后端实际只支持20个字符前端校验通过的用户名后端却报“包含非法字符”。这类问题在前后端分离项目中非常普遍本质上是校验规则只实现了一遍缺少统一的约束来源。后来我在项目中推了一个做法前端和后端共用一份校验规则的元数据描述。后端定义好字段的规则类型、长度、格式、枚举值、大小范围通过接口暴露给前端。前端拿到元数据动态生成校验逻辑。这样前端和后端永远用同一套规则不会出现“前端过了、后端拒绝”的尴尬。如果团队没有条件做这套动态规则至少要保证前后端各自的校验规则由一个统一的文档维护改的时候同步改上线前用同一个测试用例集检验两边行为一致。5.4 坑四只校验“有没有”不校验“合不合理”还有很多项目不是没做后端校验而是校验得“过于表面”。比如刷卡接口只校验了金额大于0但没有校验金额是否超过单笔限额转账接口校验了用户存在但没有校验转入转出是不是同一个账户注册接口校验了手机号是数字但没有校验是不是合法的手机号段。这种“有校验但不够”的情况比“没有校验”更危险因为它会给人造成一种虚假的安全感。代码评审时看到校验逻辑心想“这里有了”然后放行。实际上校验范围覆盖不足恰恰是漏洞藏身的地方。我的自查习惯是每写一个校验条件都问自己一句这个校验真的能把所有非法情况都挡住吗如果答案是否定的就补充分支条件而不是满足于“有个if”。6. 怎么测试一套后端校验到底过不过关既然后端校验是安全边界那它就必须经得起测试。不过很多人对校验逻辑的测试方式是“拿正常数据跑一遍没问题就过了”。这远远不够。校验逻辑存在的意义就是对抗非法输入而非法输入是无限的。我一般把校验测试分成几层来打第一层字段级测试。针对每个参数构造正常值、边界值、恶意值。比如金额字段要测0、负数、超大数、NaN、Infinity、字符串、null、超过两位小数、科学计数法。通过这套边界测试基本能检验出参数级校验是否完备。第二层权限级测试。用普通用户身份去调用管理员接口用无Token请求去调用需要登录的接口用A用户的凭证操作B用户的数据。这一层主要验证身份认证和权限控制是否生效。第三层业务规则级测试。每种状态流转的组合都测一遍正常流转、非法跳转、重复操作、过期操作。比如订单已经支付了再调一次支付接口系统是幂等拒绝还是重复扣款余额刚好等于支付金额的时候能不能成功支付余额少一分钱的时候是不是被稳定拦截第四层并发测试。模拟多个请求同时操作同一个数据。比如同一账户同时发起多个扣款请求看最终余额是否正确同一订单同时被多个线程处理看状态是否错乱。这一层的问题不能再靠逻辑校验解决要看锁、事务、数据库约束配合得好不好。还要提醒一句每修一个校验漏洞都要写一个对应的回归测试。否则当下一次重构或者升级依赖的时候同一个漏洞可能以另一种形式重新出现。7. 把校验意识刻进每次代码变更里说了这么多最后落到实际操作上我觉得最有用的还是一段自检清单。每次写完一个接口在提交代码之前我都习惯过一遍这份清单这个接口的哪个参数直接影响了资金、状态、数量等核心数据这些参数在后端有没有对应的、不信任前端的校验有没有字段是后端可以自己从服务端获取而不是依赖前端传的这些校验在Service层、数据层有没有兜底并发访问的情况下校验能不能保持一致如果前端被完全绕过直接发原始HTTP请求接口还会安全吗这些问题过一遍大约多花五分钟但换来的是一次又一次免于线上事故的安心。说实话这五分钟是整个开发流程里性价比最高的一段时间。对我个人而言前端校验和后端校验的区别从来不是“写不写代码”的区别而是思维方式的区别。前端校验解决的是“用户会不会搞错”后端校验解决的是“系统能不能承受被搞错的代价”。前者靠用户习惯和可用性设计守护后者靠代码逻辑、数据约束和测试用例守护。两者缺一不可但谁才是底牌永远要拎得清。
返回列表