3. 认证与授权漏洞:Token伪造与重放攻击、OAuth2.0的常见误用、接口级别的权限绕过
认证和授权,是接口安全的两道大门。但说实话,很多团队把这两件事混为一谈。我见过不少项目,认证做得挺花哨,授权却形同虚设。结果就是——门锁很高级,但门根本没关上。
这一章,我们聚焦三个最要命的坑:Token 怎么被伪造和重放、OAuth2.0 怎么被用歪、以及接口级别的权限绕过是怎么发生的。每个坑我都踩过,或者帮别人填过。
3.1 Token伪造与重放攻击
Token 是接口的“身份证”。但身份证能被伪造,Token 也一样。更麻烦的是,就算 Token 是真的,攻击者截获后重放一次,服务器也分不清。
3.1.1 签名算法不严谨导致的伪造
JWT 是目前最流行的 Token 格式。它的签名算法有个经典陷阱:alg: none。
我遇到过这样一个案例。某团队用 JWT 做用户认证,后端解析时没校验签名算法。攻击者把 header 里的 alg 改成 none,payload 随便改,服务器居然认了。结果就是,任何人都能伪造任意用户的 Token。
alg: none。同时,建议使用白名单机制,只允许 HS256、RS256 等安全算法。
另一个常见问题是密钥泄露。HS256 是对称签名,密钥一旦泄露,攻击者就能签发任意 Token。我建议优先使用 RS256 或 ES256 这类非对称算法。私钥只放在服务端,公钥可以公开。
3.1.2 重放攻击的防御
重放攻击,说白了就是“一次请求,多次生效”。攻击者截获你的登录请求,然后反复发送,服务器每次都认为你是合法用户。
防御手段主要有三种:
| 手段 | 原理 | 缺点 |
|---|---|---|
| 时间戳 + 窗口 | Token 携带签发时间,服务端只接受 5 分钟内的请求 | 时钟偏差可能导致误判 |
| Nonce(一次性随机数) | 每个请求带一个唯一 Nonce,服务端记录已用 Nonce | 需要存储,高并发下压力大 |
| 时间戳 + Nonce 组合 | 先验时间窗口,再查 Nonce 是否重复 | 实现稍复杂,但最安全 |
我个人习惯用第三种。时间窗口过滤掉大部分过期请求,Nonce 只在小窗口内有效,存储压力可控。
3.2 OAuth2.0的常见误用
OAuth2.0 是个好东西,但也是个容易用歪的东西。我见过太多“拿着锤子看什么都像钉子”的案例。
3.2.1 授权码模式 vs 隐式模式
很多人图省事,直接上隐式模式。前端拿到 access_token 就完事。但隐式模式有个致命问题:Token 直接暴露在 URL 中,浏览器历史、Referer 头都可能泄露它。
我曾经审计过一个移动端项目。他们用隐式模式,Token 直接拼在回调 URL 里。结果 Android 的 WebView 会把 URL 记到日志里,日志又被第三方 SDK 上传了。嗯,你想想看,这 Token 跟公开有什么区别?
正确的做法是:
- 授权码模式(Authorization Code):后端换 Token,前端不接触敏感信息
- PKCE(Proof Key for Code Exchange):即使是授权码模式,也要加 PKCE,防止授权码被截获
3.2.2 scope 权限控制形同虚设
OAuth2.0 的 scope 是用来限制 Token 权限的。但很多应用只检查 Token 是否有效,不检查 scope 是否匹配。
举个例子:用户授权了“读取基本信息”的 scope,但应用拿着这个 Token 去调“删除用户”的接口。如果服务端不校验 scope,那就出大事了。
我建议在网关层统一做 scope 校验。每个接口绑定一个 scope,Token 的 scope 必须包含接口所需的 scope 才能放行。
// 伪代码示例:scope 校验
function checkScope(token, requiredScope) {
const tokenScopes = token.payload.scope.split(' ');
if (!tokenScopes.includes(requiredScope)) {
throw new Error('Insufficient scope');
}
}
3.2.3 refresh_token 的滥用
refresh_token 是用来换新 access_token 的,它本身应该比 access_token 更安全。但我见过有人把 refresh_token 也放在前端,甚至跟 access_token 一起存 localStorage。
这等于把备用钥匙和主钥匙串在一起,还挂在门外。正确的做法是:
- refresh_token 必须存储在 httpOnly 的 cookie 中
- refresh_token 只能使用一次,用完即废
- refresh_token 要有过期时间,不能无限期有效
3.3 接口级别的权限绕过
权限绕过,是接口安全里最隐蔽的坑。认证通过了,但授权没做好,攻击者就能越权操作。
3.3.1 水平越权 vs 垂直越权
先区分两个概念:
| 类型 | 描述 | 例子 |
|---|---|---|
| 水平越权 | 同级别用户访问其他用户的数据 | 用户 A 查看用户 B 的订单 |
| 垂直越权 | 低权限用户执行高权限操作 | 普通用户调用管理员删除接口 |
水平越权最常见的原因是:接口只校验了用户是否登录,没校验数据是否属于该用户。
我曾经审计过一个电商系统。订单详情接口是 /order/{orderId},后端只查了订单是否存在,没查订单的 userId 是否等于当前登录用户的 ID。结果就是,只要知道别人的订单号,就能看别人的订单。
3.3.2 接口权限的常见绕过方式
攻击者绕过接口权限的手段很多,我列几个最常见的:
- HTTP 方法篡改:GET 改成 POST,或者 POST 改成 PUT,绕过权限校验
- 参数篡改:修改请求参数中的角色、权限字段
- 路径遍历:
/admin/user改成/../user绕过路径拦截 - 未授权接口暴露:内部接口没加权限校验,被攻击者发现
防御这些绕过,我建议做三层校验:
- 网关层:统一校验 Token 和 scope,拦截非法请求
- 服务层:校验用户角色和资源归属
- 数据层:SQL 查询中强制带上用户 ID 过滤
3.3.3 权限模型的落地
权限模型不是越复杂越好。小项目用 RBAC(基于角色的访问控制)就够了,大项目可以考虑 ABAC(基于属性的访问控制)。
我个人的经验是:
- 角色控制在 10 个以内,否则维护成本爆炸
- 权限点要细粒度,但不要细到每个按钮
- 接口权限和页面权限分开管理
3.4 本章知识体系
下面这张图,把认证与授权的核心攻击面串起来了。你可以对照着检查自己的系统。
这张图把三个攻击面、各自的子问题、以及最终的防御措施串在了一起。你可以把它当作一个检查清单,对照自己的系统逐项排查。