mirror of
http://124.126.16.154:8888/singularity/HyperfDevelopmentKit.git
synced 2026-01-15 00:35:08 +08:00
feat(account): 添加了鉴权
JWT 鉴权未处理,因为暂时没人用
This commit is contained in:
35
publish/common.php
Normal file
35
publish/common.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* common.php@hyperf-development-kit
|
||||
*
|
||||
* @author 李东云<dongyun.li@luxcreo.cn>
|
||||
* Powered by PhpStorm
|
||||
* Created on 2022/4/27
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
// 鉴权相关
|
||||
'token' => [
|
||||
// jwt 必需,否则无效
|
||||
'jwt' => [
|
||||
// 过期时间
|
||||
'expire_time' => 30 * 24 * 60 * 60,
|
||||
|
||||
'private_key' => '', // 用于加密 jwt
|
||||
'public_key' => '', // 用于解密 jwt
|
||||
],
|
||||
|
||||
// session 必需,否则无效
|
||||
'session' => [
|
||||
// 'expire_time' => null, // 始终为 session 的过期时间
|
||||
'redis' => [
|
||||
'prefix' => 'LuxAccountX:' . env('APP_ENV', 'product') . ':', // 强烈建议按此格式划分
|
||||
'keys' => [
|
||||
'forbidden_key' => 'user:last_invalidate_time', // redis 中储存时的 key 名(此时间之前登录的用户都会被 T 掉)
|
||||
]
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
49
src/Account/Model/AbstractUser.php
Normal file
49
src/Account/Model/AbstractUser.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* AbstractUser.php@hyperf-development-kit
|
||||
*
|
||||
* @author 李东云<dongyun.li@luxcreo.cn>
|
||||
* Powered by PhpStorm
|
||||
* Created on 2022/4/27
|
||||
*/
|
||||
|
||||
namespace Singularity\HyperfDevelopmentKit\Account\Model;
|
||||
|
||||
use Hyperf\Database\Model\Model;
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*
|
||||
* @property int $id LuxAccount唯一标识,遗留字段,用于校验
|
||||
* @property string $uid 对外唯一标识
|
||||
* @property string $username 用户名(适用于迁移的用户)
|
||||
* @property string $secPhone 安全手机号,用于登录
|
||||
* @property string $secEmail 安全邮箱,用于登录
|
||||
* @property string $password 密码的哈希值,使用国密SM3算法
|
||||
* @property string $name 昵称、姓名
|
||||
* @property string $avatar 用户头像地址
|
||||
* @property int $gender 性别,1:男,0:女
|
||||
* @property string $birthday 生日
|
||||
* @property string $contactPhone 联系电话
|
||||
* @property string $contactEmail 联系邮箱
|
||||
* @property string $company 公司名称
|
||||
* @property string $lastLoginIp 上次登录的IP
|
||||
* @property string $lastLoginTimestamp 上次登录的时间
|
||||
* @property string $site 注册站点。CN/US等
|
||||
* @property string $source 用户来源,记录 ServiceProvider中的EntityId
|
||||
* @property string $remarks 备注(保留字段)
|
||||
* @property string $status 账户状态(inactivated:未激活,activated:已激活,deleted:已注销)
|
||||
* @property \Carbon\Carbon $createdAt
|
||||
* @property \Carbon\Carbon $updatedAt
|
||||
* @property \Carbon\Carbon $deletedAt
|
||||
* @property string $acsUrl
|
||||
*
|
||||
* @property string $token
|
||||
* @property UserWechatInterface $wechatInfo
|
||||
* @property-write int $secureLevel
|
||||
* @property-read bool $hasPassword
|
||||
*/
|
||||
class AbstractUser extends Model
|
||||
{
|
||||
|
||||
}
|
||||
24
src/Account/Model/JsonWebToken.php
Normal file
24
src/Account/Model/JsonWebToken.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Singularity\HyperfDevelopmentKit\Account\Model;
|
||||
|
||||
use ArrayObject;
|
||||
|
||||
/**
|
||||
* 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 ArrayObject
|
||||
{
|
||||
public function __construct($array = [])
|
||||
{
|
||||
parent::__construct($array, ArrayObject::ARRAY_AS_PROPS);
|
||||
}
|
||||
}
|
||||
31
src/Account/Model/UserWechatInterface.php
Normal file
31
src/Account/Model/UserWechatInterface.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
/**
|
||||
* UserWechatInterface.php@hyperf-development-kit
|
||||
*
|
||||
* @author 李东云<dongyun.li@luxcreo.cn>
|
||||
* Powered by PhpStorm
|
||||
* Created on 2022/4/27
|
||||
*/
|
||||
|
||||
namespace Singularity\HyperfDevelopmentKit\Account\Model;
|
||||
|
||||
/**
|
||||
* 用户的微信信息
|
||||
*
|
||||
* @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\Carbon $createdAt
|
||||
* @property \Carbon\Carbon $updatedAt
|
||||
*/
|
||||
interface UserWechatInterface
|
||||
{
|
||||
|
||||
}
|
||||
197
src/Account/Services/Auth/JsonWebTokenService.php
Normal file
197
src/Account/Services/Auth/JsonWebTokenService.php
Normal file
@@ -0,0 +1,197 @@
|
||||
<?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\HyperfDevelopmentKit\Account\Services\Auth;
|
||||
|
||||
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\HyperfDevelopmentKit\Account\Model\AbstractUser;
|
||||
use Singularity\HyperfDevelopmentKit\Account\Model\JsonWebToken;
|
||||
use Singularity\HyperfDevelopmentKit\Utils\Constants\CommonErrorCode;
|
||||
use Singularity\HyperfDevelopmentKit\Utils\Exceptions\Unauthorized;
|
||||
use Singularity\HyperfDevelopmentKit\Utils\Exceptions\ValidateException;
|
||||
|
||||
/**
|
||||
* @deprecated since version 0 暂时未完成,因为还没有项目用到
|
||||
* Singularity\HyperfDevelopmentKit\Account\Services\Auth\JsonWebTokenService@hyperf-development-kit
|
||||
*
|
||||
* @author 李东云<dongyun.li@luxcreo.cn>
|
||||
* Powered by PhpStorm
|
||||
* Created on 2022/4/27
|
||||
*/
|
||||
class JsonWebTokenService implements TokenServiceInterface
|
||||
{
|
||||
use JustDont;
|
||||
|
||||
public const PUBLIC_KEY = 'dbr6TvFfOLwYfb9UllswgJ8utxolPG252Hymz+zdLz8=';
|
||||
|
||||
public const EXPIRE_TIME = 30 * 24 * 60 * 60;
|
||||
|
||||
/**
|
||||
* @Inject()
|
||||
* @var \Hyperf\HttpServer\Contract\RequestInterface
|
||||
*/
|
||||
private RequestInterface $request;
|
||||
|
||||
public function generate(
|
||||
AbstractUser|array $user,
|
||||
int $expireTime = self::EXPIRE_TIME
|
||||
): string {
|
||||
$jwt = new JsonWebToken();
|
||||
|
||||
// 设置限定条件
|
||||
$jwt->iss = config('idp_id');
|
||||
$jwt->sub = '';
|
||||
$jwt->aud = config('idp_id');
|
||||
$jwt->iat = microtime(true);
|
||||
$jwt->nbf = microtime(true);
|
||||
$jwt->exp = microtime(true) + $expireTime;
|
||||
|
||||
// 绑定当前用户信息
|
||||
$jwt = (array)$jwt + $user->toArray();
|
||||
|
||||
// 加密
|
||||
return JWT::encode(
|
||||
payload: $jwt,
|
||||
key: config('common.token.'),
|
||||
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, self::PUBLIC_KEY, ['EdDSA']);
|
||||
} 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $column
|
||||
* @param bool $returnNull
|
||||
* @param bool $redirectReturn
|
||||
*
|
||||
* @return \Singularity\HyperfDevelopmentKit\Account\Model\AbstractUser|string|int
|
||||
*/
|
||||
public function getCurrentUser(
|
||||
?string $column = null,
|
||||
bool $returnNull = false,
|
||||
bool $redirectReturn = false
|
||||
): AbstractUser|string|int {
|
||||
// 惰性查询当前用户信息
|
||||
/** @var \Singularity\HyperfDevelopmentKit\Account\Model\JsonWebToken $jwt */
|
||||
$jwt = Context::get('jwt');
|
||||
$uid = $jwt->uid;
|
||||
$currentUser = AbstractUser::query()
|
||||
->where('uid', $uid);
|
||||
if ($redirectReturn) {
|
||||
$currentUser->select([]);
|
||||
}
|
||||
$currentUser = $currentUser->firstOrFail();
|
||||
|
||||
if (isset($column)) {
|
||||
if (!isset($currentUser[$column])) {
|
||||
throw new InvalidArgumentException('属性不存在');
|
||||
}
|
||||
|
||||
return $currentUser[$column];
|
||||
}
|
||||
return $currentUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function invalid(bool $clearAll = false): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
162
src/Account/Services/Auth/SessionTokenService.php
Normal file
162
src/Account/Services/Auth/SessionTokenService.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?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\HyperfDevelopmentKit\Account\Services\Auth;
|
||||
|
||||
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\HyperfDevelopmentKit\Account\Model\AbstractUser;
|
||||
use Singularity\HyperfDevelopmentKit\Utils\Constants\CommonErrorCode;
|
||||
use Singularity\HyperfDevelopmentKit\Utils\Exceptions\Unauthorized;
|
||||
use Singularity\HyperfDevelopmentKit\Utils\Exceptions\ValidateException;
|
||||
|
||||
class SessionTokenService implements TokenServiceInterface
|
||||
{
|
||||
use JustDont;
|
||||
|
||||
private string $lastInvalidateTimeKey;
|
||||
|
||||
public function __construct(
|
||||
private SessionInterface $session,
|
||||
private RequestInterface $request,
|
||||
private Redis $redis,
|
||||
) {
|
||||
$redis_path = config('token.session.redis.prefix', '');
|
||||
$last_invalidate_time_key = config('token.session.forbidden_key', 'user:last_invalidate_time');
|
||||
$this->lastInvalidateTimeKey = $redis_path . $last_invalidate_time_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function generate(AbstractUser|array $user): string
|
||||
{
|
||||
$this->session->set('userInfo', $user);
|
||||
$this->session->set('createdAt', time());
|
||||
|
||||
return $this->session->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码,并返回验证后的值
|
||||
*/
|
||||
public function verified(?string $token = null): AbstractUser
|
||||
{
|
||||
if (!$this->session->isValidId($token ?? '')) {
|
||||
throw new ValidateException(CommonErrorCode::AUTH_SESSION_ERROR, 'token', $token);
|
||||
}
|
||||
/** @var ?array $decoded */
|
||||
$decoded = $this->session->get('userInfo');
|
||||
|
||||
if (empty($decoded)) {
|
||||
throw new Unauthorized(CommonErrorCode::AUTH_SESSION_ERROR);
|
||||
}
|
||||
|
||||
if (empty($decoded['uid'])) {
|
||||
throw new Unauthorized(CommonErrorCode::AUTH_SESSION_UID_ERROR);
|
||||
}
|
||||
|
||||
// 判断用户 session 是否应该失效
|
||||
$last_invalidate_time = $this->redis->hGet(
|
||||
$this->lastInvalidateTimeKey,
|
||||
$decoded['uid']
|
||||
);
|
||||
|
||||
/**
|
||||
* @link SessionTokenService::invalid(true)
|
||||
*/
|
||||
if ($this->session->get('createdAt') < $last_invalidate_time) {
|
||||
throw new Unauthorized(CommonErrorCode::AUTH_SESSION_CREATED_AT_ERROR);
|
||||
}
|
||||
|
||||
return new AbstractUser($decoded);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function invalid(
|
||||
bool $clearAll = false,
|
||||
): Cookie {
|
||||
if ($clearAll) {
|
||||
$this->redis->hSet(
|
||||
$this->lastInvalidateTimeKey,
|
||||
$this->session->get('userInfo')['uid'] ?? '',
|
||||
time()
|
||||
);
|
||||
}
|
||||
$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,
|
||||
): AbstractUser|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])) {
|
||||
throw new InvalidArgumentException('属性不存在');
|
||||
}
|
||||
|
||||
return $user[$column];
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
51
src/Account/Services/Auth/TokenServiceInterface.php
Normal file
51
src/Account/Services/Auth/TokenServiceInterface.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Singularity\HyperfDevelopmentKit\Account\Services\Auth;
|
||||
|
||||
use Singularity\HyperfDevelopmentKit\Account\Model\AbstractUser;
|
||||
|
||||
interface TokenServiceInterface
|
||||
{
|
||||
/**
|
||||
* 生成 token
|
||||
*
|
||||
* @param \Singularity\HyperfDevelopmentKit\Account\Model\AbstractUser $user
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generate(AbstractUser $user): string;
|
||||
|
||||
/**
|
||||
* 将传入的 token 进行校验
|
||||
*
|
||||
* @param string $token
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function verified(string $token): mixed;
|
||||
|
||||
/**
|
||||
* 从 Header 中获取 Token
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function parseTokenFromHeaders(): ?string;
|
||||
|
||||
/**
|
||||
* 获取当前登录用户的用户信息
|
||||
*
|
||||
* @param string|null $column
|
||||
* @param bool $returnNull
|
||||
*
|
||||
* @return \Singularity\HyperfDevelopmentKit\Account\Model\AbstractUser|string|int|null
|
||||
* @throws \Hyperf\Server\Exception\InvalidArgumentException
|
||||
*/
|
||||
public function getCurrentUser(?string $column = null, bool $returnNull = false): AbstractUser|string|int|null;
|
||||
|
||||
/**
|
||||
* 作废此 Token
|
||||
*
|
||||
* @param bool $clearAll
|
||||
*/
|
||||
public function invalid(bool $clearAll = false);
|
||||
}
|
||||
Reference in New Issue
Block a user