Files
lotteryLaravel/app/Services/PlayerTokenResolver.php

118 lines
4.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services;
use App\Exceptions\PlayerAuthenticationException;
use App\Lottery\ErrorCode;
use App\Models\Player;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Illuminate\Http\Request;
/**
* 从请求头解析玩家身份,返回已落库的 {@see Player}。
*
* 两种模式(互斥优先级:先判断 dev再走 JWT
* 1) 开发绕过:当 `LOTTERY_PLAYER_AUTH_DEV_BYPASS=true` 且运行环境为 `local` 或 **`testing`**PHPUnit
* 接受 `Authorization: Bearer dev:{players.id}`,直连主键查库(**禁止在生产开启**)。
* 2) 生产:使用 `MAIN_SITE_SSO_JWT_SECRET` 验签 JWT默认 HS256
* 从 payload 读取 `site_code`、`site_player_id`(字段名可 env 覆盖)再查 players 表。
*
* 错误码约定(见 {@see ErrorCode} 玩家 SSO 段):
*/
final class PlayerTokenResolver
{
public function resolve(Request $request): Player
{
$header = $request->header('Authorization', '');
if (! is_string($header) || ! str_starts_with($header, 'Bearer ')) {
throw new PlayerAuthenticationException(
'缺少或非法 Authorization',
ErrorCode::PlayerAuthorizationInvalid->value,
);
}
// 标准:`Authorization: Bearer <token>`,此处去掉前缀 7 字节 "Bearer "
$token = trim(substr($header, 7));
if ($token === '') {
throw new PlayerAuthenticationException('Token 为空', ErrorCode::PlayerAuthorizationInvalid->value);
}
// 本地 dev: 优先于 JWT避免未配密钥时仍能测需登录接口
if ($this->devBypassAllowed() && str_starts_with($token, 'dev:')) {
return $this->resolveDevToken($token);
}
// 与 .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,
);
}
return $this->resolveJwt($token, $secret);
}
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);
}
// 首期:库中必须先有该行;若需「首次进入自动建档」可在此处 firstOrCreate
$player = Player::query()
->where('site_code', $siteCode)
->where('site_player_id', $sitePlayerId)
->first();
if ($player === null) {
throw new PlayerAuthenticationException('玩家未建档', ErrorCode::PlayerNotRegistered->value);
}
return $player;
}
}