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 只在小窗口内有效,存储压力可控。

💡 实战建议: Nonce 可以用 Redis 的 SETNX 实现,设置过期时间等于时间窗口。这样既保证唯一性,又自动清理。

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,防止授权码被截获
📌 核心原则: 永远不要让 access_token 出现在前端可访问的 URL、日志、缓存中。能用授权码模式就别用隐式模式。

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。结果就是,只要知道别人的订单号,就能看别人的订单。

⚠️ 修复方案: 每个数据查询都必须带上当前用户的 ID 作为过滤条件。不要相信前端传的任何“用户标识”。

3.3.2 接口权限的常见绕过方式

攻击者绕过接口权限的手段很多,我列几个最常见的:

  • HTTP 方法篡改:GET 改成 POST,或者 POST 改成 PUT,绕过权限校验
  • 参数篡改:修改请求参数中的角色、权限字段
  • 路径遍历:/admin/user 改成 /../user 绕过路径拦截
  • 未授权接口暴露:内部接口没加权限校验,被攻击者发现

防御这些绕过,我建议做三层校验:

  1. 网关层:统一校验 Token 和 scope,拦截非法请求
  2. 服务层:校验用户角色和资源归属
  3. 数据层:SQL 查询中强制带上用户 ID 过滤

3.3.3 权限模型的落地

权限模型不是越复杂越好。小项目用 RBAC(基于角色的访问控制)就够了,大项目可以考虑 ABAC(基于属性的访问控制)。

我个人的经验是:

  • 角色控制在 10 个以内,否则维护成本爆炸
  • 权限点要细粒度,但不要细到每个按钮
  • 接口权限和页面权限分开管理
💡 避坑指南: 我曾经在一个项目中把权限点细化到“每个字段的读写”。结果权限配置表比业务表还大,最后不得不重构。权限粒度要适中,够用就行。

3.4 本章知识体系

下面这张图,把认证与授权的核心攻击面串起来了。你可以对照着检查自己的系统。

认证与授权攻击面全景图 认证与授权攻击面全景图 Token 伪造与重放 OAuth2.0 误用 接口权限绕过 alg: none 攻击 密钥泄露 重放攻击 隐式模式滥用 scope 未校验 refresh_token 泄露 水平越权 垂直越权 方法/参数篡改 防御:签名校验 + Nonce + 授权码模式 + 三层权限校验 认证通过 ≠ 授权通过。Token 有效不代表它有权限做这件事。 每一层都要校验,不要信任任何上游传递的信息。

这张图把三个攻击面、各自的子问题、以及最终的防御措施串在了一起。你可以把它当作一个检查清单,对照自己的系统逐项排查。