feat(player-auth): add JWT TTL check and AES wrapped token support
1. 新增JWT有效期校验,限制exp-iat最大时长并支持强制校验iat字段 2. 新增AES-GCM密文Token解包能力,支持非标准JWT格式的令牌传递 3. 新增相关配置项和环境变量,可灵活调整校验策略
This commit is contained in:
@@ -7,6 +7,7 @@ use Firebase\JWT\Key;
|
||||
use App\Models\Player;
|
||||
use App\Lottery\ErrorCode;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Support\PlayerTokenAesUnwrap;
|
||||
use Illuminate\Database\QueryException;
|
||||
use App\Exceptions\PlayerAuthenticationException;
|
||||
|
||||
@@ -65,7 +66,7 @@ final class PlayerTokenResolver
|
||||
);
|
||||
}
|
||||
|
||||
$player = $this->resolveJwt($token, $secret);
|
||||
$player = $this->resolveJwtOrAesWrappedJwt($token, $secret);
|
||||
}
|
||||
|
||||
$this->assertPlayerActive($player);
|
||||
@@ -103,6 +104,36 @@ final class PlayerTokenResolver
|
||||
return $player;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL/Query Opaque:支持内联 JWT,或 AES-GCM 包裹的内层 JWT(见 {@see PlayerTokenAesUnwrap})。
|
||||
*/
|
||||
private function resolveJwtOrAesWrappedJwt(string $opaque, string $secret): Player
|
||||
{
|
||||
$jwtPlain = $this->unwrapOpaqueToJwtString($opaque);
|
||||
|
||||
return $this->resolveJwt($jwtPlain, $secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* 已是 `xxx.yyy.zzz` 则视为明文 JWT;否则在配置了 AES 密钥时尝试解包。
|
||||
*/
|
||||
private function unwrapOpaqueToJwtString(string $token): string
|
||||
{
|
||||
$trimmed = trim($token);
|
||||
if ($this->looksLikeCompactJwt($trimmed)) {
|
||||
return $trimmed;
|
||||
}
|
||||
|
||||
$inner = PlayerTokenAesUnwrap::tryUnwrap($trimmed);
|
||||
|
||||
return $inner ?? $trimmed;
|
||||
}
|
||||
|
||||
private function looksLikeCompactJwt(string $token): bool
|
||||
{
|
||||
return preg_match('/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/', $token) === 1;
|
||||
}
|
||||
|
||||
private function resolveJwt(string $jwt, string $secret): Player
|
||||
{
|
||||
$alg = (string) config('lottery.player_auth.jwt.algorithm', 'HS256');
|
||||
@@ -115,6 +146,8 @@ final class PlayerTokenResolver
|
||||
throw new PlayerAuthenticationException('Token 无效或已过期', ErrorCode::PlayerTokenInvalid->value);
|
||||
}
|
||||
|
||||
$this->assertJwtTemporalPolicy($claims);
|
||||
|
||||
// 与主站约定 JWT 里字段名;若主站用 sub/iss 等可改 env LOTTERY_JWT_CLAIM_*
|
||||
$siteKey = (string) config('lottery.player_auth.jwt.claim_site_code', 'site_code');
|
||||
$pidKey = (string) config('lottery.player_auth.jwt.claim_site_player_id', 'site_player_id');
|
||||
@@ -161,6 +194,48 @@ final class PlayerTokenResolver
|
||||
return $player->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 短效 SSO:JWT 须有 exp(由 decode 校验),可选要求 iat,且 exp-iat 不得超过配置秒数。
|
||||
*
|
||||
* @param object $claims stdClass from firebase/php-jwt
|
||||
*/
|
||||
private function assertJwtTemporalPolicy(object $claims): void
|
||||
{
|
||||
$maxTtl = (int) config('lottery.player_auth.jwt.max_ttl_seconds', 300);
|
||||
$requireIat = (bool) config('lottery.player_auth.jwt.require_iat_claim', true);
|
||||
|
||||
if (! isset($claims->exp) || ! is_numeric($claims->exp)) {
|
||||
throw new PlayerAuthenticationException(
|
||||
'JWT 缺少过期时间',
|
||||
ErrorCode::PlayerTokenInvalid->value,
|
||||
);
|
||||
}
|
||||
|
||||
$exp = (int) $claims->exp;
|
||||
|
||||
if ($requireIat) {
|
||||
if (! isset($claims->iat) || ! is_numeric($claims->iat)) {
|
||||
throw new PlayerAuthenticationException(
|
||||
'JWT 缺少签发时间',
|
||||
ErrorCode::PlayerTokenInvalid->value,
|
||||
);
|
||||
}
|
||||
$iat = (int) $claims->iat;
|
||||
if ($exp <= $iat) {
|
||||
throw new PlayerAuthenticationException(
|
||||
'JWT 时间与签名无效',
|
||||
ErrorCode::PlayerTokenInvalid->value,
|
||||
);
|
||||
}
|
||||
if ($exp - $iat > $maxTtl) {
|
||||
throw new PlayerAuthenticationException(
|
||||
'JWT 有效期超过允许的 '.(string) $maxTtl.' 秒',
|
||||
ErrorCode::PlayerTokenInvalid->value,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function assertPlayerActive(Player $player): void
|
||||
{
|
||||
if ((int) $player->status !== self::PLAYER_STATUS_ACTIVE) {
|
||||
|
||||
Reference in New Issue
Block a user