后端接口一旦裸奔在公网上,面临的不是“会不会被搞”的问题,而是“什么时候被搞、被搞到什么程度”的问题。防篡改和防窃取不能只靠HTTPS一把梭,TLS解决的是传输通道加密,但解决不了请求参数被离线重放、中间人拿到明文后篡改再转发、以及客户端代码被逆向出签名算法后伪造请求。真正有效的方案要把身份认证、参数完整性校验、时效性控制、密钥管理和异常行为识别串成一条链,缺一环就会被从最薄弱的点撕开口子。
先分清两个核心威胁模型防篡改的核心是保证“请求从客户端发出后没有被改动过”,防窃取的核心是保证“请求内容即使被截获也无法被读懂或再次使用”。这两个目标经常被混为一谈,但技术手段完全不同。防篡改依赖签名和摘要算法,防窃取依赖加密和会话密钥协商。实际攻击中,攻击者往往先窃取流量分析参数结构,再尝试篡改参数值进行越权或注入,最后用重放攻击绕过时间限制。所以方案设计必须同时覆盖三个层面:参数加密、完整性签名、请求时效绑定。
参数级加密比全包加密更实用很多人第一反应是对整个请求体做AES加密,但全包加密会让网关层和日志系统变成瞎子,排查问题极其痛苦,而且大包体加密对移动端性能不友好。更合理的做法是敏感字段级加密:只对手机号、身份证、金额、令牌、业务ID等核心字段做加密,非敏感字段保持明文但参与签名。这样中间人即使截获请求,看到的是一堆密文夹杂少量明文,无法直接读懂业务含义,也无法单独修改某个明文参数而不破坏签名。
// 字段级加密示例:仅对data字段做AES-GCM加密
{
"timestamp": 1715000000,
"nonce": "8f3a2b1c9d4e5f6a",
"data": "U2FsdGVkX1+v7Q9x...",
"sign": "a1b2c3d4e5f6..."
}
加密算法优先选AES-256-GCM,GCM模式自带完整性校验,解密时如果密文被篡改会直接抛异常,相当于免费多了一层防篡改能力。密钥不要硬编码在客户端,用非对称加密做密钥协商:客户端用服务端公钥加密一个随机生成的会话密钥,服务端用私钥解开后双方基于该会话密钥进行对称加密通信。会话密钥可以设置有效期,比如30分钟,过期后重新协商。
签名机制必须绑定请求上下文单纯对参数做MD5或SHA256签名已经不够了,攻击者抓包后原样重放就能绕过签名校验。签名必须把时间戳、随机数、用户身份、请求路径全部纳入计算范围。推荐使用HMAC-SHA256,密钥由服务端下发的access_token派生,不要直接拿token当签名密钥。签名串的拼接顺序要固定,建议按参数名字典序排列后拼接,避免客户端和服务端因顺序不一致导致验签失败。
// 签名串构造示例(按参数名排序) signString = "nonce=8f3a2b1c9d4e5f6a&path=/api/order/create×tamp=1715000000&userId=10086&key=secret" sign = HMAC_SHA256(signString, derivedKey)
时间戳和nonce必须双重校验。服务端收到请求后检查时间戳与服务器当前时间的差值,超过60秒直接拒绝,这能干掉绝大部分离线重放。nonce要落库或用Redis缓存,保证同一个nonce在有效期内只能使用一次。有人会问时间戳被篡改怎么办,答案是时间戳本身参与签名,改时间戳必然导致验签失败,所以攻击者没法通过改时间戳来绕过时效检查。
密钥管理和派生是容易被忽略的命门很多团队在客户端硬编码一个AES密钥,被逆向只是时间问题。正确做法是登录成功后服务端下发access_token和refresh_token,客户端用access_token通过HKDF派生签名密钥和加密密钥。token本身不直接参与加密签名,即使token泄露,攻击者还需要知道派生算法和盐值才能算出真正的密钥。token有效期设短,比如2小时,refresh_token设长但只能用于换新token,且每次刷新后旧token立即失效。
服务端存储密钥时不要明文落库,用KMS或至少用环境变量隔离。多环境密钥必须隔离,测试环境的密钥泄露不能影响生产。密钥轮换机制也要提前设计,比如每季度强制轮换一次,轮换时新旧密钥并存一段时间,给客户端平滑过渡的窗口。
防窃取要防的不只是传输层TLS确实能防住大部分网络嗅探,但防不住客户端被逆向、防不住用户手机上的恶意软件、防不住代理工具解密HTTPS流量。所以敏感数据在进入TLS之前就应该已经是密文,也就是“先加密再传输”。这样即使攻击者通过中间人代理拿到了明文HTTP请求,看到的也只是密文字段。这个思路叫“纵深防御”,TLS是第一道墙,字段加密是第二道墙,签名是第三道墙。
对于特别敏感的接口,比如支付、提现、修改绑定手机,可以再加一层二次确认机制:服务端在关键操作前下发一个短时效的挑战码,客户端用挑战码和用户密码派生一个操作凭证,服务端验证通过后才执行。这样即使请求被完整窃取并重放,没有用户密码也算不出凭证。
防篡改要覆盖响应报文大多数人只关注请求防篡改,忘了响应也能被中间人篡改。比如攻击者劫持了查询余额的响应,把余额改成0,或者把收款账号改成自己的。响应报文同样需要签名,服务端在返回数据时带上响应签名,客户端验签通过后才展示。响应签名可以用与请求签名相同的算法,但建议用不同的密钥派生路径,防止一个方向的密钥泄露影响另一个方向。
// 响应签名示例
{
"code": 200,
"data": {
"balance": 1000.00,
"accountNo": "62221234"
},
"timestamp": 1715000001,
"sign": "f6e5d4c3b2a1..."
}
客户端验签失败时要立即中断业务流程并上报安全事件,不能静默忽略。很多客户端为了“用户体验”对验签失败只打日志不阻断,这等于把防线拆了。安全性和体验冲突时,安全必须优先,尤其是涉及资金和敏感信息的接口。
异常行为识别是最后一道防线即使前面所有机制都做到位,也不能假设万无一失。服务端要记录每个请求的签名验签结果、时间戳偏差、nonce重复次数、同一token的请求频率、同一IP的失败次数。当某个token在短时间内出现大量验签失败,或者同一个nonce被重复使用,立即触发风控:冻结token、拉黑IP、要求重新登录。这套风控逻辑可以做成独立的中间件,对业务代码零侵入。
频率限制要分维度做:按用户ID限流、按IP限流、按接口路径限流。比如登录接口同一IP每分钟最多10次,下单接口同一用户每分钟最多5次。限流阈值要根据业务实际调整,太严影响正常用户,太松挡不住脚本攻击。
客户端代码保护不能完全依赖但也不能放弃客户端的签名算法和密钥派生逻辑一定会被逆向,这是时间问题,不是技术问题。但通过代码混淆、字符串加密、反调试、完整性校验等手段,可以大幅提高逆向成本。很多攻击者是“机会主义者”,看到防护严密就换目标了。客户端可以定期从服务端拉取配置,动态调整签名串的拼接规则或加密字段列表,让攻击者逆向出来的逻辑很快失效。
同时客户端要检测自身运行环境,发现被Hook、被调试、运行在模拟器或Root设备上时,对高风险接口拒绝调用。这些检测不能作为唯一防线,但能过滤掉一大批低水平攻击。
落地顺序建议第一步先上时间戳和nonce校验,成本最低收益最大,能立刻挡住重放攻击。第二步上HMAC签名,把参数完整性管住。第三步对敏感字段做AES-GCM加密,解决窃取问题。第四步做响应签名和风控联动。第五步再做密钥轮换和客户端加固。不要一上来就追求完美方案,先把最容易实现且收益最高的部分落地,再逐步补齐。
整套方案的核心思想是:让攻击者截获了看不懂、看懂了改不了、改了验签过不了、重放了时间戳和nonce不答应、批量尝试了风控直接拉黑。每一层单独看都有绕过方法,但叠加起来后攻击成本会高到让绝大多数攻击者放弃。
