header('Authorization', ''); if (! is_string($header) || ! str_starts_with($header, 'Bearer ')) { throw new PlayerAuthenticationException( '缺少或非法 Authorization', ErrorCode::PlayerAuthorizationInvalid->value, ); } // 标准:`Authorization: Bearer `,此处去掉前缀 7 字节 "Bearer " $token = trim(substr($header, 7)); if ($token === '') { throw new PlayerAuthenticationException('Token 为空', ErrorCode::PlayerAuthorizationInvalid->value); } // 仅当 dev bypass 开启且 token 形如 dev:{id} 时走开发分支;否则一律按 JWT 处理 if ($this->devBypassAllowed() && str_starts_with($token, 'dev:')) { $player = $this->resolveDevToken($token); } else { // 与 .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)', ErrorCode::PlayerSsoSecretNotConfigured->value, 503, ); } $player = $this->resolveJwt($token, $secret); } $this->assertPlayerActive($player); // 正式 JWT 已在 resolveJwt 内写入 last_login_at;dev 仅在此处写入 if ($this->devBypassAllowed() && str_starts_with($token, 'dev:')) { $player->forceFill(['last_login_at' => now()])->save(); return $player->refresh(); } return $player; } private function devBypassAllowed(): bool { return (bool) config('lottery.player_auth.dev_bypass') && app()->environment(['local', 'testing']); } private function resolveDevToken(string $token): Player { if (! preg_match('/^dev:(\d+)$/', $token, $m)) { throw new PlayerAuthenticationException( '开发 Token 格式应为 dev:{玩家ID}', ErrorCode::PlayerTokenInvalid->value, ); } $player = Player::query()->find((int) $m[1]); if ($player === null) { throw new PlayerAuthenticationException('玩家不存在', ErrorCode::PlayerNotRegistered->value); } return $player; } private function resolveJwt(string $jwt, string $secret): Player { $alg = (string) config('lottery.player_auth.jwt.algorithm', 'HS256'); try { /** @var object $claims */ $claims = JWT::decode($jwt, new Key($secret, $alg)); } catch (\Throwable $e) { // 签名错误、exp 过期、格式损坏等均归 8002 throw new PlayerAuthenticationException('Token 无效或已过期', ErrorCode::PlayerTokenInvalid->value); } // 与主站约定 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'); $siteCode = data_get($claims, $siteKey); $sitePlayerId = data_get($claims, $pidKey); if (! is_string($siteCode) || $siteCode === '' || ! is_string($sitePlayerId) || $sitePlayerId === '') { throw new PlayerAuthenticationException('JWT 缺少站点或玩家标识', ErrorCode::PlayerTokenInvalid->value); } $now = now(); $defaults = [ 'username' => null, 'nickname' => null, 'default_currency' => (string) config('lottery.default_currency', 'NPR'), 'status' => self::PLAYER_STATUS_ACTIVE, 'last_login_at' => $now, ]; try { $player = Player::query()->firstOrCreate( [ 'site_code' => $siteCode, 'site_player_id' => $sitePlayerId, ], $defaults, ); } catch (QueryException $e) { // 并发首登时可能重复插入唯一键,改为加载已有行 $player = Player::query() ->where('site_code', $siteCode) ->where('site_player_id', $sitePlayerId) ->first(); if ($player === null) { throw $e; } } if (! $player->wasRecentlyCreated) { $player->forceFill(['last_login_at' => $now])->save(); } return $player->refresh(); } private function assertPlayerActive(Player $player): void { if ((int) $player->status !== self::PLAYER_STATUS_ACTIVE) { throw new PlayerAuthenticationException( '账号已冻结或不可用', ErrorCode::PlayerAccountSuspended->value, 403, ); } } }