前后端数据在网络上传输,默认就是裸奔状态。任何中间节点、代理、网关甚至同网段的监听者,只要愿意,都能看到请求体和响应体的完整内容。很多人以为把接口从HTTP换成HTTPS就万事大吉,实际上HTTPS只解决了传输层加密和防篡改,真正的高危风险集中在业务逻辑层:越权访问、参数篡改、重放攻击、敏感字段泄露、前端依赖被投毒。安全防护不能只靠后端加几个过滤器,必须从前端发出请求的那一刻开始设计。
把敏感数据挡在请求体之外最容易被忽视的问题是把不该由前端持有的数据放进请求参数。比如用户ID、角色标识、内部状态码、价格、折扣比例。攻击者用浏览器开发者工具改一个数字,后端如果直接采信,整个权限体系就形同虚设。正确的做法是:前端只传业务必要字段,身份信息从会话令牌中解析,价格和折扣由服务端根据数据库实时计算。凡是能从服务端会话、数据库、缓存中恢复的数据,一律不允许前端提交。前端提交的数据只应该是用户主动输入或选择的内容,而不是系统推导出来的结果。
给每一个请求加上防重放标识即使请求体加密了,攻击者把整个加密包原样重发,依然可能造成重复下单、重复转账、重复领券。防重放的核心是让每个请求具备唯一性和时效性。前端在每次请求时生成一个随机nonce,配合时间戳和请求路径,用HMAC算法生成签名。后端校验时间戳偏差不能超过60秒,同时将nonce写入Redis并设置过期时间,同一个nonce只允许使用一次。这里的关键细节是nonce必须由前端使用加密安全的随机数生成器产生,不能用简单的自增ID或者Math.random(),否则攻击者可以预测并提前构造合法请求。
// 前端生成防重放签名示例
async function buildSignedHeaders(method, path, body) {
const nonce = crypto.randomUUID();
const timestamp = Date.now().toString();
const payload = method + '\n' + path + '\n' + timestamp + '\n' + nonce + '\n' + JSON.stringify(body || {});
const encoder = new TextEncoder();
const keyData = encoder.encode(sessionStorage.getItem('signKey'));
const key = await crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(payload));
const signature = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join('');
return { 'X-Nonce': nonce, 'X-Timestamp': timestamp, 'X-Signature': signature };
}
敏感字段必须做字段级加密
HTTPS保护的是整个通道,但请求日志、浏览器历史、代理缓存、前端内存依然可能泄露明文。对于身份证号、银行卡号、手机号、家庭住址这类高敏感数据,不能只依赖通道加密。前端在提交前使用非对称加密算法,用后端下发的公钥对敏感字段单独加密,后端用私钥解密后再进行业务处理。这样即使请求日志被拖库,攻击者拿到的也只是密文。需要注意两点:公钥必须通过可信接口下发并设置有效期,防止中间人替换;加密算法要选择RSA-OAEP或者ECIES,不要使用已经过时的RSA-PKCS1-v1_5。
接口访问必须做细粒度鉴权登录态校验只是第一道门。真正的数据泄露往往发生在水平越权和垂直越权。水平越权是指用户A通过修改资源ID访问用户B的数据,垂直越权是指普通用户调用管理员接口。前端不能把“隐藏按钮”当成安全措施,后端必须在每一个接口内部重新校验当前会话对目标资源的归属关系。对于查询类接口,资源ID必须与会话中的用户ID进行绑定校验;对于管理类接口,必须校验角色权限点。一个实用的做法是在后端中间件层统一做资源归属校验,而不是在每个Controller里散落着重复的if判断。前端在封装请求库时,也应该对401和403做统一拦截,避免敏感数据在无权限状态下被渲染。
请求体和响应体都要做完整性校验攻击者不一定需要看懂数据内容,只要能篡改数据就能造成破坏。比如把转账金额从100改成10000,把商品数量从1改成99。签名机制可以防止请求体在传输过程中被篡改。前端在发送请求前,用会话密钥对请求体计算摘要,把摘要放进请求头;后端收到请求后重新计算摘要并比对。响应体同样需要校验,防止中间人篡改返回数据或者注入恶意脚本。这里推荐使用HMAC-SHA256而不是简单的MD5,因为MD5已经可以被碰撞攻击。密钥管理是难点:不能把签名密钥硬编码在前端代码里,应该在用户登录成功后由后端动态下发一个短期有效的会话签名密钥,存在内存或sessionStorage中。
敏感接口必须限制频率和并发暴力破解、撞库、短信轰炸、接口刷量,本质上都是因为没有做频率控制。前端可以加按钮倒计时和防重复点击,但这些只能提升用户体验,真正的防线在后端。后端需要针对登录、注册、发送验证码、支付确认等敏感接口做严格的限流,限制维度包括用户ID、IP地址、设备指纹。对于同一用户同一接口,可以设置每分钟最大调用次数;对于同一IP,可以设置更严格的阈值。超过阈值后返回429状态码,并配合验证码或账号临时冻结。前端在收到429后,应该停止自动重试,提示用户稍后再试,避免前端无限重试加剧后端压力。
防止前端依赖链被投毒现代前端项目依赖大量npm包,一个底层包被植入恶意代码,整个应用的数据安全就完全失效。攻击者可以通过窃取npm账号发布恶意版本,或者在依赖的依赖中注入窃取localStorage、拦截fetch请求的代码。防护措施包括:锁定依赖版本,使用lockfile并提交到仓库;定期运行npm audit检查已知漏洞;对关键依赖做源码审查;使用私有npm镜像并配置包的完整性校验。在构建流水线中增加依赖扫描步骤,发现异常包立即阻断发布。前端代码中不要使用来源不明的CDN脚本,所有第三方脚本必须放在自己的域名下并开启SRI(子资源完整性)校验。
前端存储的数据要默认视为不可信localStorage、sessionStorage、IndexedDB、Cookie,所有前端存储都可以被用户或恶意脚本读取和修改。不要把JWT令牌、用户权限、敏感配置放在localStorage中长期保存。令牌应该优先使用HttpOnly、Secure、SameSite属性的Cookie,由浏览器自动携带,前端脚本无法读取。如果必须使用前端存储保存令牌,要设置短过期时间,并在每次请求时通过刷新机制轮换。前端存储中的业务数据,比如购物车信息、表单草稿,在提交给后端时必须重新校验,不能直接采信。后端永远不要信任前端传来的任何“已校验”标记。
错误信息要做最小化处理详细的错误堆栈、SQL语句、内部路径、用户是否存在、密码是否错误,这些信息对攻击者来说是极有价值的情报。后端在返回错误响应时,只给前端一个通用的错误码和用户友好的提示,详细错误记录在服务端日志中。前端在展示错误时,也不要直接把后端返回的原始错误信息渲染到页面上,避免XSS注入。对于登录接口,无论用户名是否存在、密码是否正确,都返回相同的错误提示“用户名或密码错误”,防止攻击者通过错误信息枚举有效账号。
文件上传和下载必须走独立安全通道文件上传接口是攻击者最喜欢攻击的目标,因为可以上传WebShell、恶意脚本、超大文件。前端在上传前要做文件类型、大小、扩展名的初步校验,但这些校验很容易被绕过。后端必须重新校验文件的真实MIME类型,使用文件头魔数判断而不是依赖扩展名,将文件存储在非Web可执行目录,重命名文件名,并对图片文件做二次渲染以清除嵌入的恶意代码。下载接口要防止路径遍历攻击,用户传入的文件名必须经过白名单过滤或由后端生成随机文件名,不允许用户直接指定服务器路径。
建立前端安全基线检查清单数据安全不是某个单一技术点的堆砌,而是一套完整的工程实践。前端在每次发版前,应该检查以下项目:是否还有明文敏感字段在请求中传输;所有请求是否都带上了防重放签名;令牌是否存储在安全位置;第三方依赖是否通过安全扫描;生产环境是否关闭了调试日志和sourcemap;CSP策略是否正确配置;所有表单输入是否做了前端校验且后端有对应校验。把这些检查项集成到CI/CD流水线中,每次构建自动执行,发现违规直接阻断发布。只有把安全要求变成工程约束,才能真正落地。
前后端交互的数据安全,本质上是在假设网络不可信、用户不可信、依赖不可信的前提下设计系统。前端负责减少暴露面、做好第一层过滤和加密,后端负责最终裁决、完整校验和异常监控。任何一层的缺失,都会让整个防护体系出现缺口。最有效的防护不是某个炫技的加密算法,而是把每一个交互环节的风险都识别出来,用最朴素但最严格的方式逐一堵上。
