first commit

This commit is contained in:
2025-09-12 15:23:08 +08:00
commit a80c237bbb
117 changed files with 15628 additions and 0 deletions

233
app/Lib/Jwt/AbstractJwt.php Normal file
View File

@@ -0,0 +1,233 @@
<?php
namespace App\Lib\Jwt;
use App\Interface\JwtInterface;
use Carbon\Carbon;
use Hyperf\Cache\CacheManager;
use Hyperf\Cache\Driver\DriverInterface;
use Hyperf\Collection\Arr;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\JwtFacade;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\UnencryptedToken;
use Lcobucci\JWT\Validation\Constraint;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Constraint\StrictValidAt;
use Psr\SimpleCache\InvalidArgumentException;
abstract class AbstractJwt implements JwtInterface
{
/**
* @param array $config
* @param CacheManager $cacheManager
* @param Clock $clock
* @param AccessTokenConstraint $accessTokenConstraint
* @param RefreshTokenConstraint $refreshTokenConstraint
*/
public function __construct(
private readonly array $config,
private readonly CacheManager $cacheManager,
private readonly Clock $clock,
private readonly AccessTokenConstraint $accessTokenConstraint,
private readonly RefreshTokenConstraint $refreshTokenConstraint
) {}
/**
* @param string $sub
* @param \Closure|null $callable
* @return UnencryptedToken
*/
public function builderAccessToken(string $sub, ?\Closure $callable = null): UnencryptedToken
{
return $this->getJwtFacade()->issue(
$this->getSigner(),
$this->getSigningKey(),
function (Builder $builder, \DateTimeImmutable $immutable) use ($sub, $callable) {
$builder = $builder->identifiedBy($sub);
if ($callable !== null) {
$builder = $callable($builder);
}
return $builder->expiresAt($this->getExpireAt($immutable));
}
);
}
/**
* @param string $sub
* @param \Closure|null $callable
* @return UnencryptedToken
*/
public function builderRefreshToken(string $sub, ?\Closure $callable = null): UnencryptedToken
{
return $this->getJwtFacade()->issue(
$this->getSigner(),
$this->getSigningKey(),
function (Builder $builder, \DateTimeImmutable $immutable) use ($sub, $callable) {
$builder = $builder->identifiedBy($sub);
$builder = $builder->expiresAt($this->getRefreshExpireAt($immutable));
if ($callable !== null) {
$builder = $callable($builder);
}
return $builder->relatedTo('refresh');
}
);
}
/**
* @param string $accessToken
* @return UnencryptedToken
*/
public function parserAccessToken(string $accessToken): UnencryptedToken
{
return $this->getJwtFacade()
->parse(
$accessToken,
new SignedWith(
$this->getSigner(),
$this->getSigningKey()
),
new StrictValidAt(
$this->clock,
$this->clock->now()->diff($this->getExpireAt($this->clock->now()))
),
$this->getBlackListConstraint(),
$this->refreshTokenConstraint
);
}
/**
* @param string $refreshToken
* @return UnencryptedToken
*/
public function parserRefreshToken(string $refreshToken): UnencryptedToken
{
return $this->getJwtFacade()
->parse(
$refreshToken,
new SignedWith(
$this->getSigner(),
$this->getSigningKey()
),
new StrictValidAt(
$this->clock,
$this->clock->now()->diff($this->getRefreshExpireAt($this->clock->now()))
),
$this->getBlackListConstraint(),
$this->accessTokenConstraint
);
}
/**
* @param UnencryptedToken $token
* @return bool
* @throws InvalidArgumentException
*/
public function addBlackList(UnencryptedToken $token): bool
{
return $this->getCacheDriver()->set($token->toString(), 1, $this->getBlackConfig('ttl', 600));
}
/**
* @param UnencryptedToken $token
* @return bool
* @throws InvalidArgumentException
*/
public function hasBlackList(UnencryptedToken $token): bool
{
return $this->getCacheDriver()->has($token->toString());
}
/**
* @param UnencryptedToken $token
* @return bool
* @throws InvalidArgumentException
*/
public function removeBlackList(UnencryptedToken $token): bool
{
return $this->getCacheDriver()->delete($token->toString());
}
/**
* @param string $key
* @param mixed|null $default
* @return mixed
*/
public function getConfig(string $key, mixed $default = null): mixed
{
return Arr::get($this->config, $key, $default);
}
/**
* @return JwtFacade
*/
private function getJwtFacade(): JwtFacade
{
return new JwtFacade(clock: $this->clock);
}
/**
* @return Signer
*/
private function getSigner(): Signer
{
return Arr::get($this->config, 'alg');
}
/**
* @return Key
*/
private function getSigningKey(): Key
{
return Arr::get($this->config, 'key');
}
/**
* @return DriverInterface
*/
private function getCacheDriver(): DriverInterface
{
return $this->cacheManager->getDriver($this->getBlackConfig('connection'));
}
/**
* @param string $name
* @param mixed|null $default
* @return mixed
*/
private function getBlackConfig(string $name, mixed $default = null): mixed
{
return Arr::get($this->config, 'blacklist.' . $name, $default);
}
/**
* @return Constraint
*/
private function getBlackListConstraint(): Constraint
{
return new BlackListConstraint((bool) $this->getBlackConfig('enable', false), $this->getCacheDriver());
}
/**
* @param \DateTimeImmutable $immutable
* @return \DateTimeImmutable
*/
private function getExpireAt(\DateTimeImmutable $immutable): \DateTimeImmutable
{
return Carbon::create($immutable)
->addSeconds(Arr::get($this->config, 'ttl', 3600))
->toDateTimeImmutable();
}
/**
* @param \DateTimeImmutable $immutable
* @return \DateTimeImmutable
*/
private function getRefreshExpireAt(\DateTimeImmutable $immutable): \DateTimeImmutable
{
return Carbon::create($immutable)
->addSeconds(Arr::get($this->config, 'refresh_ttl', 7200))
->toDateTimeImmutable();
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Lib\Jwt;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Validation\Constraint;
use Lcobucci\JWT\Validation\ConstraintViolation;
class AccessTokenConstraint implements Constraint
{
public function assert(Token $token): void
{
if (! $token->isRelatedTo('refresh')) throw ConstraintViolation::error('Token is not a refresh token', $this);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Lib\Jwt;
use Hyperf\Cache\Driver\DriverInterface;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Validation\Constraint;
use Lcobucci\JWT\Validation\ConstraintViolation;
use Psr\SimpleCache\InvalidArgumentException;
class BlackListConstraint implements Constraint
{
/**
* @param bool $enable
* @param DriverInterface $cache
*/
public function __construct(
private readonly bool $enable,
private readonly DriverInterface $cache
) {}
/**
* @param Token $token
* @return void
* @throws InvalidArgumentException
*/
public function assert(Token $token): void
{
if ($this->enable !== true) return;
if ($this->cache->has($token->toString())) throw ConstraintViolation::error('Token is in blacklist', $this);
}
}

15
app/Lib/Jwt/Clock.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
namespace App\Lib\Jwt;
use Carbon\Carbon;
use DateTimeImmutable;
use Psr\Clock\ClockInterface;
class Clock implements ClockInterface
{
public function now(): DateTimeImmutable
{
return Carbon::now()->toDateTimeImmutable();
}
}

7
app/Lib/Jwt/Jwt.php Normal file
View File

@@ -0,0 +1,7 @@
<?php
namespace App\Lib\Jwt;
use App\Interface\JwtInterface;
final class Jwt extends AbstractJwt implements JwtInterface {}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Lib\Jwt;
use App\Interface\JwtInterface;
use Hyperf\Collection\Arr;
use Hyperf\Contract\ConfigInterface;
use function Hyperf\Support\make;
final class JwtFactory
{
public function __construct(
private readonly ConfigInterface $config,
) {}
/**
* @param string $name
* @return JwtInterface
*/
public function get(string $name = 'default'): JwtInterface
{
return make(Jwt::class, [
'config' => $this->getConfig($name),
]);
}
/**
* 获取场景配置
* @param string $scene
* @return array
*/
public function getConfig(string $scene): array
{
if ($scene === 'default') {
return $this->config->get($this->getConfigKey());
}
return Arr::merge(
$this->config->get($this->getConfigKey()),
$this->config->get($this->getConfigKey($scene), [])
);
}
/**
* @param string $name
* @return string
*/
private function getConfigKey(string $name = 'default'): string
{
return 'jwt.' . $name;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Lib\Jwt;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Validation\Constraint;
use Lcobucci\JWT\Validation\ConstraintViolation;
class RefreshTokenConstraint implements Constraint
{
public function assert(Token $token): void
{
if ($token->isRelatedTo('refresh')) throw ConstraintViolation::error('Token is a refresh token', $this);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Lib\Return;
class AdminReturn extends CommonReturn
{
/**
* 通用返回
* @param array $res
* @return array
*/
final protected function afterSuccess(array $res): array
{
return $res;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Lib\Return;
class ApiReturn extends CommonReturn
{
/**
* 通用返回
* @param array $res
* @return array
*/
final protected function afterSuccess(array $res): array
{
return $res;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Lib\Return;
use App\Constants\ResultCode;
abstract class CommonReturn
{
/**
* 通用 success 返回
* @param string $msg
* @param array $data
* @param int|ResultCode $code
* @param array $debug
* @return array
*/
final public function success(string $msg = 'success', array $data = [], ResultCode|int $code = ResultCode::SUCCESS, array $debug = []): array
{
$res = [
'code' => $code,
'message' => $msg,
'data' => $data
];
return $this->afterSuccess(array_merge($res, $debug));
}
/**
* 通用 fail 返回
* @param string $msg
* @param array $data
* @param int|ResultCode $code
* @param array $debug
* @return array
*/
final public function error(string $msg = 'failed', ResultCode|int $code = ResultCode::ERROR, array $data = [], array $debug = []): array
{
$res = [
'code' => $code,
'message' => $msg,
'data' => $data
];
return $this->afterSuccess(array_merge($res, $debug));
}
/**
* 通用类调子类返回方便切面类识别
* @param array $res
* @return array
*/
abstract protected function afterSuccess(array $res): array;
}