feat(account): 添加了鉴权

JWT 鉴权未处理,因为暂时没人用
This commit is contained in:
李东云
2022-04-27 19:00:53 +08:00
parent 2b2f853188
commit e759eda3c4
7 changed files with 549 additions and 0 deletions

35
publish/common.php Normal file
View 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 掉)
]
],
],
],
];

View 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 性别10
* @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
{
}

View 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);
}
}

View 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
{
}

View 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;
}
}

View 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;
}
}

View 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);
}