webman中jwt认证不安全,因jwt::generatetoken()仅基础签名,未强制校验iss/aud/nbf字段,且未启用密钥轮换;需手动构造含ip、jti等字段的payload,并在中间件中严格验证bearer头、ip一致性及组合限流。

Webman 中 JWT 认证不能只靠插件自动“开箱即用”,必须手动接管签名验证逻辑并绑定限流上下文,否则 Bearer 头被篡改、refresh_token 未隔离、IP+用户维度限流缺失这三类漏洞会直接暴露在生产环境。
为什么 \Jwt::generateToken() 生成的 token 默认不安全
该方法仅做基础签名,不强制校验 iss(签发者)、aud(受众)、nbf(生效时间)字段,且默认未启用 JWT::setSecretKey() 的密钥轮换支持。实际使用中常见问题包括:
- 攻击者截获 token 后可伪造请求头中的
Authorization: Bearer xxx直接重放 - 同一
access_secret_key同时用于登录和刷新,一旦泄露,refresh_token无法单独作废 - payload 中未写入客户端 IP 或 User-Agent,导致无法做设备级绑定
建议在 issueToken 方法中显式构造 payload:
$payload = [
'uid' => $userInfo['id'],
'exp' => time() + config('jwt.jwt.access_exp', 36000),
'iat' => time(),
'iss' => config('jwt.jwt.iss'),
'aud' => 'webman-api',
'jti' => uniqid('jwt_', true), // 防重放唯一标识
'ip' => $request->getRealIp(),
];
$token = \Jwt::encode($payload, config('jwt.jwt.access_secret_key'), config('jwt.jwt.alg'));
如何让 Bearer 验证真正拦截非法请求
Webman 默认的中间件不会自动解析 Authorization 头,也不会拒绝空值、格式错误或过期 token。你必须自己写一个全局中间件,并在其中调用 \Jwt::decode() 做完整校验:
- 必须检查
Authorization是否存在且以Bearer开头(注意末尾空格) - 必须捕获
ExpiredException和SignatureInvalidException并统一返回401 - 不能只依赖
try/catch,要主动调用\Jwt::getPayload()提取ip字段,与当前请求 IP 比对
示例关键判断逻辑:
if (!$auth || !str_starts_with($auth, 'Bearer ')) {
throw new UnauthorizedException('Missing or malformed Authorization header');
}
$token = substr($auth, 7);
try {
$payload = \Jwt::decode($token, config('jwt.jwt.access_secret_key'), [config('jwt.jwt.algorithm')]);
if ($payload->ip !== $request->getRealIp()) {
throw new UnauthorizedException('IP mismatch');
}
} catch (\Exception $e) {
throw new UnauthorizedException('Invalid token');
}
限流必须同时绑定用户 ID 和 IP,不能只靠 Redis key 模糊匹配
很多开发者直接用 rate_limit:ip:{ip} 或 rate_limit:uid:{uid} 单维度限流,这会导致两种绕过:
Webman 2.2.0版本强化了 TCP/UDP 服务支持,优化路由组管理,并增强异步任务处理能力。结合协程与连接池技术,Webman 能轻松应对高并发场景,适用于网站、接口服务、即时通讯、物联网及游戏开发,兼具高性能、灵活扩展与稳定可靠,是多场景 PHP 服务开发的理想选择。
- 攻击者用代理池轮换 IP,但固定用户 ID,绕过 IP 限流
- 攻击者用多个账号(UID)但同一 IP,绕过用户限流
正确做法是组合 key:rate_limit:uid_ip:{uid}_{ip},并在中间件中从 JWT payload 提取 uid 和 ip:
$key = 'rate_limit:uid_ip:' . $payload->uid . '_' . $payload->ip;
$limit = 100; // 每小时
$window = 3600;
$count = redis()->incr($key);
if ($count == 1) {
redis()->expire($key, $window);
}
if ($count > $limit) {
throw new TooManyRequestsException('Rate limit exceeded');
}
注意:Redis 的 INCR + EXPIRE 必须用 pipeline 或 Lua 脚本保证原子性,否则高并发下会漏判。
refresh_token 必须独立存储并校验,不能复用 access_secret_key
Webman 的 JWT 插件默认把 refresh_secret_key 当作另一个密钥,但没提供配套的存储和校验机制。生产环境必须:
- 将
refresh_token存入 Redis,key 为refresh:{jti},设置与refresh_exp一致的 TTL - 在刷新接口中,先查 Redis 确认
jti是否存在且未被使用过 - 成功刷新后,立即
DEL refresh:{old_jti}并写入新refresh:{new_jti} - 禁止在 payload 中携带敏感字段(如密码哈希、权限列表),这些应由业务层动态查询
遗漏这点,等于把长期有效的凭证明文暴露在客户端——哪怕设置了 7 天过期,也无法提前吊销。
最易被忽略的是:JWT 的 exp 是服务端单方面控制的,但客户端可能缓存旧 token 并持续发送。必须配合前端的 token 自动刷新机制,且服务端每次响应都应返回新的 expires_in 时间戳,而不是只依赖 payload 里的 exp 字段做硬判断。










