init: hyperf-auth

This commit is contained in:
李东云
2023-02-20 14:26:23 +08:00
parent d9fcc282ed
commit 1e8c4c7a3c
21 changed files with 7485 additions and 543 deletions

View File

@@ -1,32 +1,40 @@
{
"name": "singularity/auth",
"name": "singularity/hdk-auth",
"license": "MIT",
"type": "library",
"description": "登录控制",
"autoload": {
"psr-4": {
"Example\\": "src/Example/"
"Singularity\\HDK\\Auth\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\Example\\": "tests/Example/"
"extra": {
"hyperf": {
"config": "Singularity\\HDK\\Auth\\ConfigProvider"
}
},
"require": {
"php": "^8.0",
"composer/composer": "~2.0.14"
"php": ">=7.4",
"composer/composer": "~2.0.14",
"hyperf/validation": "^2.2.33",
"singularity/hdk-core": "0.1.x-dev",
"symfony/polyfill-php80": "^1.27",
"ext-redis": "*"
},
"require-dev": {
"phpunit/phpunit": "^9.5"
"phpunit/phpunit": "^9.5",
"firebase/php-jwt": "^6.3"
},
"suggest": {
"firebase/php-jwt": "^6.3"
},
"minimum-stability": "dev",
"prefer-stable": true,
"config": {
"optimize-autoloader": true,
"sort-packages": true
"sort-packages": true,
"secure-http": false
},
"extra": [],
"scripts": {
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
@@ -36,6 +44,10 @@
"analyse": "phpstan analyse --memory-limit 300M -l 0 -c phpstan.neon ./app ./config"
},
"repositories": {
"lux-map": {
"type": "composer",
"url": "https://satis.luxcreo.cn/"
},
"packagist": {
"type": "composer",
"url": "https://mirrors.aliyun.com/composer"

6782
composer.lock generated

File diff suppressed because it is too large Load Diff

29
docs/overview.md Normal file
View File

@@ -0,0 +1,29 @@
# 概览
[TOC]
## 模块
### 认证
* ***(规范)*** 对接口进行鉴权,推荐使用 *验证器 + Trait* 的形式。可选:
* 不需要鉴权:
* PublicRequest
* 需要鉴权:
* LoginRequiredRequest
* *(灵活)* 对路由、控制器、接口进行认证鉴权,可以使用中间件的方式
### SSO
### 鉴权
### 用户信息交互SDK
通过对 RPC 请求进行封装,包装成类普通数据库操作的 Service对调用者隐藏 Guzzle/cURL 过程。
将用于交互的用户信息、收货地址等,包装成 Resource类似 Model 的方式进行调用。
## 认证方式
### Session/Cookie
### JWT
### Token

View File

@@ -1,4 +1,12 @@
<?php
declare(strict_types=1);
namespace Singularity\HDK\Auth;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Framework\Logger\StdoutLogger;
/**
* ConfigProvider.php@HyperfAuth
*
@@ -7,4 +15,56 @@
* Created on 2023/1/16
*/
return [];
class ConfigProvider
{
public function __invoke(): array
{
/** @noinspection PhpUndefinedConstantInspection */
return [
// 合并到 config/autoload/dependencies.php 文件
'dependencies' => [
StdoutLoggerInterface::class => StdoutLogger::class,
],
// 合并到 config/autoload/annotations.php 文件
'annotations' => [
'scan' => [
'paths' => [
__DIR__,
],
],
],
// 默认 Command 的定义,合并到 Hyperf\Contract\ConfigInterface 内,换个方式理解也就是与 config/autoload/commands.php 对应
'commands' => [
],
// 与 commands 类似
'listeners' => [],
// 组件默认配置文件,即执行命令后会把 source 的对应的文件复制为 destination 对应的的文件
'publish' => [
[
'id' => 'config',
'description' => 'The common config for HDK',
'source' => __DIR__ . '/../publish/common.php',
'destination' => BASE_PATH . '/config/autoload/common.php',
],
[
'id' => 'script',
'description' => 'The common script for HDK',
'source' => __DIR__ . '/../publish/scripts/docker-env.sh',
'destination' => BASE_PATH . '/scripts/docker-env.sh',
],
[
'id' => 'languages_cn',
'description' => 'The common error message for Chinese',
'source' => __DIR__ . '/../publish/languages/zh_CN/common_error.php',
'destination' => BASE_PATH . '/storage/languages/zh_CN/common_error.php',
],
[
'id' => 'languages_en',
'description' => 'The common error message for English',
'source' => __DIR__ . '/../publish/languages/en/common_error.php',
'destination' => BASE_PATH . '/storage/languages/en/common_error.php',
],
],
];
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Singularity\HDK\Auth\Middleware;
use Hyperf\Di\Annotation\Inject;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Singularity\HDK\Auth\Services\AuthenticationInterface;
use Singularity\HDK\Core\Constants\CommonErrorCode;
use Singularity\HDK\Core\Enumerations\Http\Header\RFCs\RFC7486;
use Singularity\HDK\Core\Exceptions\Unauthorized;
/**
* 通用鉴权中间件
* Singularity\HDK\Auth\Middleware\AuthenticateMiddleware@HyperfAuth
*
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
* Powered by PhpStorm
* Created on 2023/1/17
*/
class AuthenticateMiddleware implements MiddlewareInterface
{
/**
* @Inject
* @var AuthenticationInterface
*/
private AuthenticationInterface $authentication;
/**
* @inheritDoc
*/
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
$token = $this->authentication->parseTokenFromHeaders();
if (empty($token) || $token === 'null' || $token === 'undefined' || $token === 'false') {
throw new Unauthorized(CommonErrorCode::UNAUTHORIZED, null, RFC7486::HOBA);
}
// 验证 token 中的参数
$this->authentication->verified($token);
return $handler->handle($request);
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* LoginRequiredRequest.php@HDK
*
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
* Powered by PhpStorm
* Created on 2023/1/17
*/
namespace Singularity\HDK\Auth\Request;
use Hyperf\Validation\Request\FormRequest;
use Singularity\HDK\Auth\Traits\LoginRequired;
/**
* 需要登录认证的接口
* Singularity\HDK\Account\Request\LoginRequiredRequest@HDK
*
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
* Powered by PhpStorm
* Created on 2023/1/17
*/
abstract class LoginRequiredRequest extends FormRequest
{
use LoginRequired;
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* AbstractRequest.php@HDK
*
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
* Powered by PhpStorm
* Created on 2023/1/17
*/
namespace Singularity\HDK\Auth\Request;
use Hyperf\Validation\Request\FormRequest;
use Singularity\HDK\Auth\Traits\LoginOptional;
/**
* 不需要登录就可以使用的接口
* Singularity\HDK\Account\Request\AbstractRequest@HyperfAuth
*
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
* Powered by PhpStorm
* Created on 2023/1/17
*/
abstract class PublicRequest extends FormRequest
{
use LoginOptional;
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Singularity\HDK\Auth\Resource;
use Hyperf\Resource\Json\JsonResource;
use Singularity\HDK\Core\Resource\ClassicResponse;
/**
* JWT 数据模型
*
* @property string $iss JWT 签发者
* @property string $sub 面向的用户
* @property string $aud 接收 JWT 的一方
* @property int $exp 过期时间
* @property int $nbf 生效时间
* @property int $iat 签发时间
* @property string $uid 用户唯一标识
*/
class JsonWebToken extends JsonResource
{
use ClassicResponse;
/**
* Transform the resource into an array.
*
* @return array
*/
public function toArray(): array
{
return parent::toArray();
}
}

55
src/Resource/User.php Normal file
View File

@@ -0,0 +1,55 @@
<?php
/**
* User.php@hyperf-development-kit
*
* @author 李东云<dongyun.li@luxcreo.cn>
* Powered by PhpStorm
* Created on 2022/4/28
*/
namespace Singularity\HDK\Auth\Resource;
use Hyperf\Resource\Json\JsonResource;
use Singularity\HDK\Core\Resource\ClassicResponse;
/**
* 用户信息
* Singularity\HyperfDevelopmentKit\Account\Resource\ User@hyperf-development-kit
*
* @author 李东云<dongyun.li@luxcreo.cn>
* Powered by PhpStorm
* Created on 2022/4/28
*
* @property string $uid 对外唯一标识
* @property string $username 用户名,用于登录(默认中国大陆为手机号,其它为邮箱)
* @property string $secPhone 安全手机号,用于登录
* @property string $secEmail 安全邮箱,用于登录
* @property string $name 昵称、姓名
* @property bool $hasPassword 是否设置了密码
* @property string $avatar 用户头像地址
* @property int $gender 性别10
* @property string $birthday 生日
* @property string $contactPhone 联系电话
* @property string $contactEmail 联系邮箱
* @property string $company 公司名称
* @property string $lastLoginTimestamp 上次登录的时间
* @property string $remarks 备注(保留字段)
* @property string $originToken Account 中的 Token/SessionId
*
* @property UserWechat $wechatInfo
* @property int $secureLevel 安全等级
*/
class User extends JsonResource
{
use ClassicResponse;
/**
* Transform the resource into an array.
*
* @return array
*/
public function toArray(): array
{
return parent::toArray();
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* UserWechat.php@hyperf-development-kit
*
* @author 李东云<dongyun.li@luxcreo.cn>
* Powered by PhpStorm
* Created on 2022/4/27
*/
namespace Singularity\HDK\Auth\Resource;
use Carbon\Carbon;
use Hyperf\Resource\Json\JsonResource;
use Singularity\HDK\Core\Resource\ClassicResponse;
/**
* 用户的微信信息
*
* @property int $id
* @property int $userId
* @property string $openId
* @property string $nickname
* @property int $sex 性别。1为男性2为女性
* @property string $province 个人资料填写的省份
* @property string $city 个人资料填写的城市
* @property string $country 国家如中国为CN
* @property string $privilege 用户特权信息json数组如微信沃卡用户为chinaunicom
* @property string $unionId
* @property Carbon $createdAt
* @property Carbon $updatedAt
*/
class UserWechat extends JsonResource
{
use ClassicResponse;
/**
* Transform the resource into an array.
*
* @return array
*/
public function toArray(): array
{
return parent::toArray();
}
}

43
src/Sdk/AddressApi.php Normal file
View File

@@ -0,0 +1,43 @@
<?php
namespace Singularity\HDK\Auth\Sdk;
use Hyperf\Contract\TranslatorInterface;
use Hyperf\Di\Annotation\Inject;
use Singularity\HDK\Core\Service\HttpRequestService;
class AddressApi extends HttpRequestService
{
/**
* @Inject
* @var TranslatorInterface
*/
private TranslatorInterface $translator;
public function __construct()
{
parent::__construct(
[
'headers' => [
'Accept-Language' => $this->translator->getLocale()
]
],
config('common.http_request.account.api_base_uri')
);
}
public function getStates($code)
{
return $this->get("countries/$code/states");
}
public function getCities($code, $stateId)
{
return $this->get("countries/$code/states/$stateId/cities");
}
public function getAreas($code, $stateId, $cityId)
{
return $this->get("countries/$code/states/$stateId/cities/$cityId/counties");
}
}

64
src/Sdk/AddressRpc.php Normal file
View File

@@ -0,0 +1,64 @@
<?php
namespace Singularity\HDK\Account\Services\Http;
use Hyperf\Contract\TranslatorInterface;
use Hyperf\Di\Annotation\Inject;
use Singularity\HDK\Utils\Service\HttpRequestService;
class AddressRpc extends HttpRequestService
{
/**
* @Inject
* @var TranslatorInterface
*/
private TranslatorInterface $translator;
public function __construct()
{
parent::__construct(
[
'headers' => [
'Accept-Language' => $this->translator->getLocale()
]
],
config('common.http_request.account.rpc_base_uri')
);
}
public function list($uid)
{
return $this->get('address?uid=' . $uid);
}
public function create($uid, $data)
{
return $this->post('address/create?uid='. $uid, $data);
}
public function update($uid, $addrId, $data)
{
return $this->post('address/update?uid=' . $uid . '&addr-id=' . $addrId, $data);
}
public function del($uid, $addrId)
{
return $this->post('address/delete?uid=' . $uid . '&addr-id=' . $addrId);
}
public function getDefault($uid)
{
return $this->get('address/getDefault?uid=' . $uid);
}
public function setDefault($uid, $addrId)
{
return $this->post('address/setDefault', ['uid' => $uid, 'addrId' => $addrId]);
}
public function detail($addrId)
{
return $this->get('address/detail?addr-id=' . $addrId);
}
}

39
src/Sdk/UserApi.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
namespace Singularity\HDK\Account\Services\Http;
use Hyperf\Contract\TranslatorInterface;
use Hyperf\Di\Annotation\Inject;
use Singularity\HDK\Utils\Service\HttpRequestService;
class UserApi extends HttpRequestService
{
/**
* @Inject
* @var TranslatorInterface
*/
private TranslatorInterface $translator;
public function __construct()
{
parent::__construct(
[
'Accept-Language' => $this->translator->getLocale()
],
config('common.http_request.account.api_base_uri')
);
}
public function sendCode($params)
{
return $this->post('secure-code', $params);
}
public function checkCode($params)
{
$code = $params['code'];
unset($params['code']);
return $this->get('secure-code/' . $code, $params);
}
}

60
src/Sdk/UserRpc.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
namespace Singularity\HDK\Auth\Services\Http;
use Hyperf\Contract\TranslatorInterface;
use Hyperf\Di\Annotation\Inject;
use Singularity\HDK\Utils\Service\HttpRequestService;
class UserRpc extends HttpRequestService
{
/**
* @Inject
* @var TranslatorInterface
*/
private TranslatorInterface $translator;
public function __construct()
{
parent::__construct(
[
'headers' => [
'Accept-Language' => $this->translator->getLocale()
]
],
config('common.http_request.account.rpc_base_uri')
);
}
public function alive($name)
{
return $this->get('user/alive?username=' . urlencode($name));
}
public function detail($uid)
{
return $this->get('user/detail?uid=' . $uid);
}
public function create($data)
{
return $this->post('user/create', $data);
}
public function update($uid, $data)
{
return $this->post('user/update?uid=' . $uid, $data);
}
public function list($uid_arr)
{
return $this->post('user', $uid_arr);
}
public function checkNamePass($data)
{
return $this->post('user/signIn', $data);
}
}

View File

@@ -0,0 +1,131 @@
<?php
namespace Singularity\HDK\Auth\Services;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Redis\Redis;
use RedisException;
use Singularity\HDK\Auth\Resource\User;
use Singularity\HDK\Core\Constants\CommonErrorCode;
use Singularity\HDK\Core\Exceptions\ValidateException;
/**
* Singularity\HDK\Auth\Services\AppAuthentication@HyperfAuth
*
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
* Powered by PhpStorm
* Created on 2023/1/17
*/
class AppAuthentication implements AuthenticationInterface
{
private $prefix;
private $expire;
private $user;
/**
* @Inject()
* @var RequestInterface
*/
private RequestInterface $request;
/**
* @Inject()
* @var Redis
*/
private Redis $redis;
public function __construct()
{
$config = config('common.token.app');
$this->prefix = $config['prefix_key'];
$this->expire = $config['expire_time'];
}
/**
* @param $user
* @return string
* @throws RedisException
*/
public function generate($user): string
{
$token = md5(uniqid((string)mt_rand(), true));
$this->redis->set($this->prefix . $token, serialize($user), $this->expire);
$this->user = $user;
return $token;
}
public function verified(string $token)
{
if (empty($token)) {
throw new ValidateException(CommonErrorCode::AUTH_APP_ERROR, 'token', $token);
}
$redis_data = $this->redis->get($this->prefix . $token);
if (empty($redis_data)) {
throw new ValidateException(CommonErrorCode::AUTH_APP_ERROR, 'token', $token);
}
$user = unserialize($redis_data);
$this->user = $user;
$this->redis->expire($this->prefix . $token, $this->expire);
return $user;
}
/**
* @return string|null
*/
public function parseTokenFromHeaders(): ?string
{
$token = $this->request->getHeaderLine('Authorization');
var_dump('header token: ' . $token);
return $token ?? '';
}
/**
* @param string|null $column
* @param bool $returnNull
* @return User|string|int|null|array
*/
public function getCurrentUser(?string $column = null, bool $returnNull = false)
{
if (!empty($column)) {
return $this->user[$column];
}
return $this->user;
}
/**
* @param bool $clearAll
* @return mixed
*/
public function invalid(bool $clearAll = false)
{
return true;
}
/**
* @param string $uid
* @return bool
*/
public function invalidByUser(string $uid): bool
{
return true;
}
/**
* @param string $token
* @return bool
*/
public function invalidByToken(string $token): bool
{
return true;
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Singularity\HDK\Auth\Services;
use Singularity\HDK\Auth\Resource\User;
interface AuthenticationInterface
{
/**
* 生成 token
*
* @param \Singularity\HDK\Auth\Resource\User $user
*
* @return string
*/
public function generate(User $user): string;
/**
* 将传入的 token 进行校验
*
* @param string $token
*
* @return mixed
*/
public function verified(string $token);
/**
* 从 Header 中获取 Token
*
* @return string|null
*/
public function parseTokenFromHeaders(): ?string;
/**
* 获取当前登录用户的用户信息
*
* @param string|null $column 要获取的单个字段
* @param bool $returnNull 开启后不抛出异常,而是返回 null
*
* @return \Singularity\HDK\Auth\Resource\User|string|int|null|array
*/
public function getCurrentUser(?string $column = null, bool $returnNull = false);
/**
* 作废此 Token
*
* @param bool $clearAll
*
* @deprecated
* @uses \Singularity\HDK\Auth\AuthenticationInterface::invalidByToken()
* @uses \Singularity\HDK\Auth\AuthenticationInterface::invalidByUser()
*/
public function invalid(bool $clearAll = false);
/**
* 通过用户作废 Token
*
* @param string $uid
*
* @return bool
*/
public function invalidByUser(string $uid): bool;
/**
* 作废此 token
*
* @param string $token
*/
public function invalidByToken(string $token): bool;
}

View File

@@ -0,0 +1,184 @@
<?php
/** @noinspection SpellCheckingInspection */
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Singularity\HDK\Auth\Services;
use Dont\JustDont;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\JWT;
use Hyperf\Context\Context;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Server\Exception\InvalidArgumentException;
use Singularity\HDK\Auth\Resource\JsonWebToken;
use Singularity\HDK\Auth\Resource\User;
use Singularity\HDK\Auth\Traits\CannotInvalid;
use Singularity\HDK\Core\Enumerations\Http\Header\RFCs\RFC7616;
use Singularity\HDK\Core\Exceptions\Unauthorized;
use Singularity\HDK\Core\Constants\CommonErrorCode;
use Singularity\HDK\Core\Exceptions\ValidateException;
/**
* Singularity\HyperfDevelopmentKit\Account\Services\Auth\JwtAuthentication@hyperf-development-kit
*
* @author 李东云<dongyun.li@luxcreo.cn>
* Powered by PhpStorm
* Created on 2022/4/27
*/
class JwtAuthentication implements AuthenticationInterface
{
use JustDont;
use CannotInvalid;
/**
* @Inject()
* @var \Hyperf\HttpServer\Contract\RequestInterface
*/
private RequestInterface $request;
public function generate(User $user): string
{
$expireTime = config('common.token.jwt.expire_time');
$jwt = [
// 设置限定条件
'iss' => config('idp_id'),
'sub' => '',
'aud' => config('idp_id'),
'iat' => microtime(true),
'nbf' => microtime(true),
'exp' => microtime(true) + $expireTime,
];
// 绑定当前用户信息
$jwt = $jwt + $user->toArray();
// 加密
return JWT::encode(
payload: $jwt,
key: config('common.token.jwt.private_key'),
alg: 'EdDSA'
);
}
/**
* 解码,并返回验证后的值
*/
public function verified(?string $token = null): JsonWebToken
{
if (!empty(Context::get('jwt'))) {
return Context::get('jwt');
}
if (empty($token)) {
throw new ValidateException(CommonErrorCode::AUTH_JWT_ERROR, 'token', $token);
}
JWT::$leeway = 30;
try {
$decoded = (array)JWT::decode($token, config('common.token.jwt.public_key'));
} catch (ExpiredException $exception) {
$error_code = CommonErrorCode::AUTH_JWT_EXP_TIMEOUT;
throw new Unauthorized($error_code, $exception);
}
if (empty($decoded)) {
throw new ValidateException(CommonErrorCode::AUTH_JWT_ERROR, 'token', $decoded);
}
// 判断签发机构
if (($decoded['iss'] ?? '') !== config('idp_id')) {
$error_code = CommonErrorCode::AUTH_JWT_ISS_ERROR;
throw new Unauthorized($error_code);
}
// 判断签发时间
if (($decoded['iat'] ?? 0) > microtime(true)) {
$error_code = CommonErrorCode::AUTH_JWT_IAT_ERROR;
throw new Unauthorized($error_code);
}
// 判断生效时间
if (($decoded['nbf'] ?? 0) > microtime(true)) {
$error_code = CommonErrorCode::AUTH_JWT_NBF_ERROR;
throw new Unauthorized($error_code);
}
// 判断过期时间
if (($decoded['exp'] ?? 0) <= microtime(true)) {
$error_code = CommonErrorCode::AUTH_JWT_EXP_TIMEOUT;
throw new Unauthorized($error_code);
}
if (empty($decoded['uid'])) {
$error_code = CommonErrorCode::AUTH_JWT_UID_ERROR;
throw new Unauthorized($error_code);
}
$jwt = new JsonWebToken($decoded);
Context::set('jwt', $jwt);
return $jwt;
}
/**
* 从请求中解析出 token.
*
* @return string|null
*/
public function parseTokenFromHeaders(): ?string
{
$token = $this->request->getHeaderLine('Authorization');
$token = (
empty($token)
|| !is_string($token)
|| strlen($token) <= 0
|| !str_starts_with($token, 'Bearer ')
)
? null
: substr($token, 7);
if ($token === false || $token === 'null') {
return null;
}
return $token;
}
/**
* @inheritDoc
*
* @param string|null $column
* @param bool $returnNull
* @param bool $redirectReturn
*
* @return \Singularity\HDK\Auth\Resource\User|string|int
*/
public function getCurrentUser(
?string $column = null,
bool $returnNull = false,
bool $redirectReturn = false
) {
// 惰性查询当前用户信息
/** @var \Singularity\HDK\Auth\Resource\JsonWebToken $currentUser */
$currentUser = Context::get('jwt');
if (empty($currentUser)) {
throw new Unauthorized(RFC7616::DIGEST);
}
if (isset($column)) {
if (!isset($currentUser[$column])) {
throw new InvalidArgumentException('属性不存在');
}
return $currentUser[$column];
}
return $currentUser;
}
}

View File

@@ -0,0 +1,192 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Singularity\HDK\Auth\Services;
use Dont\JustDont;
use Hyperf\Contract\SessionInterface;
use Hyperf\HttpMessage\Cookie\Cookie;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Redis\Redis;
use Hyperf\Server\Exception\InvalidArgumentException;
use Singularity\HDK\Account\Services\Auth\AuthenticationInterface;
use Singularity\HDK\Auth\Resource\User;
use Singularity\HDK\Utils\Constants\CommonErrorCode;
use Singularity\HDK\Utils\Exceptions\Unauthorized;
use Singularity\HDK\Utils\Exceptions\ValidateException;
class SessionAuthentication implements AuthenticationInterface
{
use JustDont;
private string $lastInvalidateTimeKey;
public function __construct(
private SessionInterface $session,
private RequestInterface $request,
private Redis $redis,
) {
$redis_path = config('common.redis.prefix', '');
$last_invalidate_time_key = config('common.token.session.forbidden_key', 'user:last_invalidate_time');
$this->lastInvalidateTimeKey = $redis_path . $last_invalidate_time_key;
}
/**
* @inheritDoc
*/
public function generate(User $user): string
{
$this->session->set('userInfo', $user->toArray());
$this->session->set('createdAt', microtime(true));
return $this->session->getId();
}
/**
* 解码,并返回验证后的值
*/
public function verified(?string $token = null): User
{
if (!$this->session->isValidId($token ?? '')) {
throw new ValidateException(CommonErrorCode::AUTH_SESSION_ERROR, 'token', $token);
}
$user = $this->session->get('userInfo');
if (empty($user)) {
throw new Unauthorized(CommonErrorCode::AUTH_SESSION_ERROR);
}
$user = new User($user);
if (empty($user['uid'])) {
throw new Unauthorized(CommonErrorCode::AUTH_SESSION_UID_ERROR);
}
// 判断用户 session 是否应该失效
$last_invalidate_time = $this->redis->hGet(
$this->lastInvalidateTimeKey,
$user['uid']
);
/**
* @link SessionAuthentication::invalid(true)
*/
if ($this->session->get('createdAt') < $last_invalidate_time) {
throw new Unauthorized(CommonErrorCode::AUTH_SESSION_CREATED_AT_ERROR);
}
return $user;
}
/**
* @inheritDoc
*/
public function invalid(
bool $clearAll = false,
): Cookie {
if ($clearAll) {
$user = $this->session->get('userInfo');
$user = new User($user);
$this->redis->hSet(
$this->lastInvalidateTimeKey,
$user['uid'] ?? '',
microtime(true)
);
}
$this->session->invalidate();
return new Cookie(
'is_login',
'',
time() - 3600,
'/',
domain: $this->request->getUri()->getHost(),
httpOnly: false,
sameSite: Cookie::SAMESITE_LAX
);
}
/**
* 从请求中解析出 token.
*
* @return string|null
*/
public function parseTokenFromHeaders(): ?string
{
$session_name = config('session.options.session_name');
$token = $this->request->getCookieParams()[$session_name] ?? null;
return (
empty($token)
|| !is_string($token)
|| strlen($token) <= 0
|| $token === 'null'
)
? null
: $token;
}
/**
* @inheritDoc
*/
public function getCurrentUser(
?string $column = null,
bool $returnNull = false,
bool $redirectReturn = true,
): User|string|int|null {
// 查询是否已通过中间件鉴权
$user = $this->session->get('userInfo');
// 未匹配到任何用户信息
if (empty($user)) {
if ($returnNull) {
return null;
}
throw new Unauthorized(CommonErrorCode::AUTH_SESSION_ERROR);
}
// 已匹配到时返回指定字段或整个用户信息
if ($column !== null) {
if (!isset($user[$column])) {
if ($returnNull) {
return null;
}
throw new InvalidArgumentException("属性 $column 不存在");
}
return $user[$column];
}
return new User($user);
}
/**
* @inheritDoc
*/
public function invalidByUser(string $uid): bool
{
$result = $this->redis->hSet(
$this->lastInvalidateTimeKey,
$uid,
microtime(true)
);
return $result !== false;
}
/**
* @inheritDoc
*/
public function invalidByToken(string $token = ''): bool
{
if ($token === '') {
return $this->session->invalidate();
}
$result = $this->redis->del($token);
return $result > 0;
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* CannotInvalid.php@hyperf-development-kit
*
* @author 李东云<dongyun.li@luxcreo.cn>
* Powered by PhpStorm
* Created on 2022/4/28
*/
namespace Singularity\HDK\Auth\Traits;
/**
* 用于无法作废token的鉴权
* Singularity\HDK\Account\Services\Auth\CannotInvalid@hyperf-development-kit
*
* @author 李东云<dongyun.li@luxcreo.cn>
* Powered by PhpStorm
* Created on 2022/4/28
*/
trait CannotInvalid
{
/**
* @inheritDoc
*/
public function invalid(bool $clearAll = false): bool
{
return true;
}
/**
* @inheritDoc
*/
public function invalidByUser(string $uid): bool
{
return true;
}
/**
* @inheritDoc
*/
public function invalidByToken(string $token): bool
{
return true;
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* LoginOptional.php@HDK
*
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
* Powered by PhpStorm
* Created on 2023/1/17
*/
namespace Singularity\HDK\Auth\Traits;
/**
* 无需登录的处理
* Singularity\HDK\Account\Traits\LoginOptional@HDK
*
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
* Powered by PhpStorm
* Created on 2023/1/17
*/
trait LoginOptional
{
public function authorize(): bool {
return true;
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* LoginRequired.php@HDK
*
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
* Powered by PhpStorm
* Created on 2023/1/17
*/
namespace Singularity\HDK\Auth\Traits;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Validation\Request\FormRequest;
use Singularity\HDK\Auth\Services\AuthenticationInterface;
/**
* Singularity\HDK\Account\Traits\LoginRequired@HDK
*
* @author 李东云 <Dongyun.Li@LuxCreo.Ai>
* Powered by PhpStorm
* Created on 2023/1/17
*
* @mixin FormRequest
*/
trait LoginRequired
{
/**
* @Inject()
* @var AuthenticationInterface
*/
private AuthenticationInterface $authentication;
public function authorize(): bool {
// 登录逻辑
return !!$this->authentication->getCurrentUser();
}
}