feat: 增强管理员 API 鉴权,新增 token 有效天数配置,更新相关异常处理与错误码引用

This commit is contained in:
2026-05-09 11:26:39 +08:00
parent 8a70c029f6
commit f1b38ef421
13 changed files with 124 additions and 42 deletions

View File

@@ -3,6 +3,7 @@
namespace App\Services;
use App\Exceptions\PlayerAuthenticationException;
use App\Lottery\ErrorCode;
use App\Models\Player;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
@@ -17,11 +18,7 @@ use Illuminate\Http\Request;
* 2) 生产:使用 `MAIN_SITE_SSO_JWT_SECRET` 验签 JWT默认 HS256
* payload 读取 `site_code``site_player_id`(字段名可 env 覆盖)再查 players 表。
*
* 错误码约定(见中间件返回 JSON code
* - 8001:无 Bearer / Token / Header 格式不对
* - 8002JWT 无效、过期或缺少站点/玩家字段
* - 8003:数据库无对应玩家(未建档)
* - 8004:未配置 MAIN_SITE_SSO_JWT_SECRETHTTP 503
* 错误码约定(见 {@see ErrorCode} 玩家 SSO
*/
final class PlayerTokenResolver
{
@@ -29,13 +26,16 @@ final class PlayerTokenResolver
{
$header = $request->header('Authorization', '');
if (! is_string($header) || ! str_starts_with($header, 'Bearer ')) {
throw new PlayerAuthenticationException('缺少或非法 Authorization', 8001);
throw new PlayerAuthenticationException(
'缺少或非法 Authorization',
ErrorCode::PlayerAuthorizationInvalid->value,
);
}
// 标准:`Authorization: Bearer <token>`,此处去掉前缀 7 字节 "Bearer "
$token = trim(substr($header, 7));
if ($token === '') {
throw new PlayerAuthenticationException('Token 为空', 8001);
throw new PlayerAuthenticationException('Token 为空', ErrorCode::PlayerAuthorizationInvalid->value);
}
// 本地 dev: 优先于 JWT避免未配密钥时仍能测需登录接口
@@ -46,7 +46,11 @@ final class PlayerTokenResolver
// 与 .env 中 MAIN_SITE_SSO_JWT_SECRET 一致,用于 firebase/php-jwt 验签
$secret = config('lottery.main_site.sso_jwt_secret');
if (! is_string($secret) || $secret === '') {
throw new PlayerAuthenticationException('SSO 未配置MAIN_SITE_SSO_JWT_SECRET', 8004, 503);
throw new PlayerAuthenticationException(
'SSO 未配置MAIN_SITE_SSO_JWT_SECRET',
ErrorCode::PlayerSsoSecretNotConfigured->value,
503,
);
}
return $this->resolveJwt($token, $secret);
@@ -61,12 +65,15 @@ final class PlayerTokenResolver
private function resolveDevToken(string $token): Player
{
if (! preg_match('/^dev:(\d+)$/', $token, $m)) {
throw new PlayerAuthenticationException('开发 Token 格式应为 dev:{玩家ID}', 8002);
throw new PlayerAuthenticationException(
'开发 Token 格式应为 dev:{玩家ID}',
ErrorCode::PlayerTokenInvalid->value,
);
}
$player = Player::query()->find((int) $m[1]);
if ($player === null) {
throw new PlayerAuthenticationException('玩家不存在', 8003);
throw new PlayerAuthenticationException('玩家不存在', ErrorCode::PlayerNotRegistered->value);
}
return $player;
@@ -81,7 +88,7 @@ final class PlayerTokenResolver
$claims = JWT::decode($jwt, new Key($secret, $alg));
} catch (\Throwable $e) {
// 签名错误、exp 过期、格式损坏等均归 8002
throw new PlayerAuthenticationException('Token 无效或已过期', 8002);
throw new PlayerAuthenticationException('Token 无效或已过期', ErrorCode::PlayerTokenInvalid->value);
}
// 与主站约定 JWT 里字段名;若主站用 sub/iss 等可改 env LOTTERY_JWT_CLAIM_*
@@ -92,7 +99,7 @@ final class PlayerTokenResolver
$sitePlayerId = data_get($claims, $pidKey);
if (! is_string($siteCode) || $siteCode === '' || ! is_string($sitePlayerId) || $sitePlayerId === '') {
throw new PlayerAuthenticationException('JWT 缺少站点或玩家标识', 8002);
throw new PlayerAuthenticationException('JWT 缺少站点或玩家标识', ErrorCode::PlayerTokenInvalid->value);
}
// 首期:库中必须先有该行;若需「首次进入自动建档」可在此处 firstOrCreate
@@ -102,7 +109,7 @@ final class PlayerTokenResolver
->first();
if ($player === null) {
throw new PlayerAuthenticationException('玩家未建档', 8003);
throw new PlayerAuthenticationException('玩家未建档', ErrorCode::PlayerNotRegistered->value);
}
return $player;