mirror of
https://devops.lemonos.cn/lawson/FendxPHP.git
synced 2026-06-15 23:12:49 +08:00
feat(database): 添加用户角色权限系统及相关监控功能
- 创建用户表(users)包含基本信息和认证字段 - 创建角色表(roles)用于权限控制 - 创建权限表(permissions)定义系统权限 - 创建用户角色关联表(user_roles)建立用户与角色关系 - 创建角色权限关联表(role_permissions)建立角色与权限关系 - 创建迁移记录表(migrations)追踪数据库变更 - 添加AdminController提供管理员面板功能 - 实现系统监控、配置管理、缓存清理等功能 - 添加AOP切面编程支持的各种通知类型 - 实现告警管理AlertManager支持多渠道告警 - 添加文档注解接口规范
This commit is contained in:
24
fendx-framework/fendx-security/composer.json
Normal file
24
fendx-framework/fendx-security/composer.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "fendx/security",
|
||||
"description": "FendxPHP Security Module - 权限、认证、安全、防护",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Lawson",
|
||||
"email": "lawson@fendx.cn"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"fendx/common": "^1.0",
|
||||
"fendx/core": "^1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Fendx\\Security\\": "src/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
166
fendx-framework/fendx-security/src/Auth/Auth.php
Normal file
166
fendx-framework/fendx-security/src/Auth/Auth.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Security\Auth;
|
||||
|
||||
use Fendx\Core\Context\Context;
|
||||
use Fendx\Security\Token\TokenManager;
|
||||
use Fendx\Common\Exception\BusinessException;
|
||||
|
||||
final class Auth
|
||||
{
|
||||
private static TokenManager $tokenManager;
|
||||
private static ?array $currentUser = null;
|
||||
|
||||
public static function initialize(TokenManager $tokenManager): void
|
||||
{
|
||||
self::$tokenManager = $tokenManager;
|
||||
}
|
||||
|
||||
public static function login(array $credentials): string
|
||||
{
|
||||
if (!isset(self::$tokenManager)) {
|
||||
throw new BusinessException(500, 'AUTH_NOT_INITIALIZED');
|
||||
}
|
||||
|
||||
// TODO: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.
|
||||
$token = self::$tokenManager->generate($credentials);
|
||||
|
||||
self::setCurrentUser($credentials);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public static function logout(string $token): bool
|
||||
{
|
||||
if (!isset(self::$tokenManager)) {
|
||||
throw new BusinessException(500, 'AUTH_NOT_INITIALIZED');
|
||||
}
|
||||
|
||||
$result = self::$tokenManager->revoke($token);
|
||||
|
||||
self::setCurrentUser(null);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function authenticate(string $token): ?array
|
||||
{
|
||||
if (!isset(self::$tokenManager)) {
|
||||
throw new BusinessException(500, 'AUTH_NOT_INITIALIZED');
|
||||
}
|
||||
|
||||
try {
|
||||
$payload = self::$tokenManager->verify($token);
|
||||
|
||||
if ($payload && !self::$tokenManager->isRevoked($token)) {
|
||||
self::setCurrentUser($payload);
|
||||
return $payload;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Token 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.
|
||||
}
|
||||
|
||||
self::setCurrentUser(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function check(): bool
|
||||
{
|
||||
return self::getCurrentUser() !== null;
|
||||
}
|
||||
|
||||
public static function user(): ?array
|
||||
{
|
||||
return self::getCurrentUser();
|
||||
}
|
||||
|
||||
public static function id(): mixed
|
||||
{
|
||||
$user = self::getCurrentUser();
|
||||
return $user['id'] ?? null;
|
||||
}
|
||||
|
||||
public static function hasRole(string $role): bool
|
||||
{
|
||||
$user = self::getCurrentUser();
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$roles = $user['roles'] ?? [];
|
||||
return in_array($role, $roles);
|
||||
}
|
||||
|
||||
public static function hasPermission(string $permission): bool
|
||||
{
|
||||
$user = self::getCurrentUser();
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$permissions = $user['permissions'] ?? [];
|
||||
return in_array($permission, $permissions);
|
||||
}
|
||||
|
||||
public static function can(string $permission): bool
|
||||
{
|
||||
return self::hasPermission($permission);
|
||||
}
|
||||
|
||||
private static function setCurrentUser(?array $user): void
|
||||
{
|
||||
self::$currentUser = $user;
|
||||
|
||||
if ($user) {
|
||||
Context::setUser($user);
|
||||
} else {
|
||||
Context::setUser([]);
|
||||
}
|
||||
}
|
||||
|
||||
private static function getCurrentUser(): ?array
|
||||
{
|
||||
if (self::$currentUser === null) {
|
||||
$contextUser = Context::getUser();
|
||||
self::$currentUser = !empty($contextUser) ? $contextUser : null;
|
||||
}
|
||||
|
||||
return self::$currentUser;
|
||||
}
|
||||
|
||||
public static function refresh(string $token): ?string
|
||||
{
|
||||
if (!isset(self::$tokenManager)) {
|
||||
throw new BusinessException(500, 'AUTH_NOT_INITIALIZED');
|
||||
}
|
||||
|
||||
try {
|
||||
$payload = self::$tokenManager->verify($token);
|
||||
|
||||
if ($payload && !self::$tokenManager->isRevoked($token)) {
|
||||
// 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.
|
||||
self::$tokenManager->revoke($token);
|
||||
return self::$tokenManager->generate($payload);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Token 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function validateToken(string $token): bool
|
||||
{
|
||||
if (!isset(self::$tokenManager)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$payload = self::$tokenManager->verify($token);
|
||||
return $payload !== null && !self::$tokenManager->isRevoked($token);
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
252
fendx-framework/fendx-security/src/Auth/JwtManager.php
Normal file
252
fendx-framework/fendx-security/src/Auth/JwtManager.php
Normal file
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Security\Auth;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use Firebase\JWT\ExpiredException;
|
||||
use Firebase\JWT\BeforeValidException;
|
||||
use Firebase\JWT\SignatureInvalidException;
|
||||
|
||||
/**
|
||||
* JWT管理器
|
||||
* 提供JWT令牌的生成、验证和刷新功能
|
||||
*/
|
||||
class JwtManager
|
||||
{
|
||||
private string $secretKey;
|
||||
private string $algorithm;
|
||||
private int $expiresIn;
|
||||
private int $refreshExpiresIn;
|
||||
private string $issuer;
|
||||
private string $audience;
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->secretKey = $config['secret_key'] ?? 'your-secret-key';
|
||||
$this->algorithm = $config['algorithm'] ?? 'HS256';
|
||||
$this->expiresIn = $config['expires_in'] ?? 3600; // 1小时
|
||||
$this->refreshExpiresIn = $config['refresh_expires_in'] ?? 2592000; // 30天
|
||||
$this->issuer = $config['issuer'] ?? 'fendx-php';
|
||||
$this->audience = $config['audience'] ?? 'fendx-client';
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成访问令牌
|
||||
*/
|
||||
public function generateToken(array $payload, bool $refresh = false): string
|
||||
{
|
||||
$now = time();
|
||||
$expiresIn = $refresh ? $this->refreshExpiresIn : $this->expiresIn;
|
||||
|
||||
$tokenPayload = array_merge($payload, [
|
||||
'iss' => $this->issuer,
|
||||
'aud' => $this->audience,
|
||||
'iat' => $now,
|
||||
'exp' => $now + $expiresIn,
|
||||
'type' => $refresh ? 'refresh' : 'access',
|
||||
'jti' => $this->generateJTI()
|
||||
]);
|
||||
|
||||
return JWT::encode($tokenPayload, $this->secretKey, $this->algorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成访问令牌和刷新令牌
|
||||
*/
|
||||
public function generateTokenPair(array $payload): array
|
||||
{
|
||||
return [
|
||||
'access_token' => $this->generateToken($payload, false),
|
||||
'refresh_token' => $this->generateToken($payload, true),
|
||||
'expires_in' => $this->expiresIn,
|
||||
'refresh_expires_in' => $this->refreshExpiresIn,
|
||||
'token_type' => 'Bearer'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证令牌
|
||||
*/
|
||||
public function verifyToken(string $token): ?array
|
||||
{
|
||||
try {
|
||||
$payload = JWT::decode($token, new Key($this->secretKey, $this->algorithm));
|
||||
return (array) $payload;
|
||||
} catch (ExpiredException $e) {
|
||||
throw new JwtException('Token expired', JwtException::EXPIRED);
|
||||
} catch (BeforeValidException $e) {
|
||||
throw new JwtException('Token not valid yet', JwtException::INVALID);
|
||||
} catch (SignatureInvalidException $e) {
|
||||
throw new JwtException('Invalid token signature', JwtException::INVALID_SIGNATURE);
|
||||
} catch (\Exception $e) {
|
||||
throw new JwtException('Invalid token', JwtException::INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
public function refreshToken(string $refreshToken): array
|
||||
{
|
||||
$payload = $this->verifyToken($refreshToken);
|
||||
|
||||
if (($payload['type'] ?? '') !== 'refresh') {
|
||||
throw new JwtException('Invalid refresh token', JwtException::INVALID_TYPE);
|
||||
}
|
||||
|
||||
// 移除时间相关字段,重新生成
|
||||
unset($payload['iat'], $payload['exp'], $payload['jti']);
|
||||
|
||||
return $this->generateTokenPair($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头提取令牌
|
||||
*/
|
||||
public function extractTokenFromHeader(string $header): ?string
|
||||
{
|
||||
if (strpos($header, 'Bearer ') === 0) {
|
||||
return substr($header, 7);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取令牌剩余有效时间
|
||||
*/
|
||||
public function getTokenRemainingTime(string $token): int
|
||||
{
|
||||
$payload = $this->verifyToken($token);
|
||||
return max(0, $payload['exp'] - time());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查令牌是否即将过期
|
||||
*/
|
||||
public function isTokenExpiringSoon(string $token, int $bufferSeconds = 300): bool
|
||||
{
|
||||
$remainingTime = $this->getTokenRemainingTime($token);
|
||||
return $remainingTime <= $bufferSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析令牌但不验证过期时间
|
||||
*/
|
||||
public function parseTokenWithoutExpiration(string $token): ?array
|
||||
{
|
||||
try {
|
||||
$payload = JWT::decode($token, new Key($this->secretKey, $this->algorithm), [$this->algorithm]);
|
||||
return (array) $payload;
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成JTI(JWT ID)
|
||||
*/
|
||||
private function generateJTI(): string
|
||||
{
|
||||
return uniqid() . bin2hex(random_bytes(8));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建黑名单令牌(用于注销)
|
||||
*/
|
||||
public function blacklistToken(string $token, int $ttl = 3600): void
|
||||
{
|
||||
$payload = $this->verifyToken($token);
|
||||
$jti = $payload['jti'] ?? '';
|
||||
$exp = $payload['exp'] ?? 0;
|
||||
|
||||
if ($jti && $exp > time()) {
|
||||
// 这里应该将jti存储到缓存中,直到过期
|
||||
// 简化实现,实际应该使用Redis等缓存
|
||||
$key = "jwt_blacklist:{$jti}";
|
||||
$remainingTime = $exp - time();
|
||||
$cacheTtl = min($remainingTime, $ttl);
|
||||
|
||||
// cache()->set($key, time(), $cacheTtl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查令牌是否在黑名单中
|
||||
*/
|
||||
public function isTokenBlacklisted(string $token): bool
|
||||
{
|
||||
$payload = $this->parseTokenWithoutExpiration($token);
|
||||
if (!$payload) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$jti = $payload['jti'] ?? '';
|
||||
if (!$jti) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$key = "jwt_blacklist:{$jti}";
|
||||
// return cache()->has($key);
|
||||
|
||||
// 简化实现
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证令牌并检查黑名单
|
||||
*/
|
||||
public function validateToken(string $token): ?array
|
||||
{
|
||||
if ($this->isTokenBlacklisted($token)) {
|
||||
throw new JwtException('Token is blacklisted', JwtException::BLACKLISTED);
|
||||
}
|
||||
|
||||
return $this->verifyToken($token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置信息
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
return [
|
||||
'algorithm' => $this->algorithm,
|
||||
'expires_in' => $this->expiresIn,
|
||||
'refresh_expires_in' => $this->refreshExpiresIn,
|
||||
'issuer' => $this->issuer,
|
||||
'audience' => $this->audience,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置配置
|
||||
*/
|
||||
public function setConfig(array $config): void
|
||||
{
|
||||
foreach ($config as $key => $value) {
|
||||
if (property_exists($this, $key)) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT异常类
|
||||
*/
|
||||
class JwtException extends \Exception
|
||||
{
|
||||
const EXPIRED = 1;
|
||||
const INVALID = 2;
|
||||
const INVALID_SIGNATURE = 3;
|
||||
const INVALID_TYPE = 4;
|
||||
const BLACKLISTED = 5;
|
||||
|
||||
public function __construct(string $message = "", int $code = 0, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
426
fendx-framework/fendx-security/src/Auth/RbacManager.php
Normal file
426
fendx-framework/fendx-security/src/Auth/RbacManager.php
Normal file
@@ -0,0 +1,426 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Security\Auth;
|
||||
|
||||
/**
|
||||
* RBAC权限管理器
|
||||
* 实现基于角色的访问控制
|
||||
*/
|
||||
class RbacManager
|
||||
{
|
||||
private array $roles = [];
|
||||
private array $permissions = [];
|
||||
private array $userRoles = [];
|
||||
private array $rolePermissions = [];
|
||||
private array $userPermissions = [];
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->loadConfig($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载配置
|
||||
*/
|
||||
private function loadConfig(array $config): void
|
||||
{
|
||||
$this->roles = $config['roles'] ?? [];
|
||||
$this->permissions = $config['permissions'] ?? [];
|
||||
$this->userRoles = $config['user_roles'] ?? [];
|
||||
$this->rolePermissions = $config['role_permissions'] ?? [];
|
||||
$this->userPermissions = $config['user_permissions'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加角色
|
||||
*/
|
||||
public function addRole(string $name, string $description = ''): void
|
||||
{
|
||||
$this->roles[$name] = [
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'created_at' => time()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加权限
|
||||
*/
|
||||
public function addPermission(string $name, string $description = '', string $resource = '', string $action = ''): void
|
||||
{
|
||||
$this->permissions[$name] = [
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'resource' => $resource,
|
||||
'action' => $action,
|
||||
'created_at' => time()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 为用户分配角色
|
||||
*/
|
||||
public function assignRoleToUser(int $userId, string $roleName): bool
|
||||
{
|
||||
if (!isset($this->roles[$roleName])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($this->userRoles[$userId])) {
|
||||
$this->userRoles[$userId] = [];
|
||||
}
|
||||
|
||||
if (!in_array($roleName, $this->userRoles[$userId])) {
|
||||
$this->userRoles[$userId][] = $roleName;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除用户角色
|
||||
*/
|
||||
public function removeRoleFromUser(int $userId, string $roleName): bool
|
||||
{
|
||||
if (!isset($this->userRoles[$userId])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$key = array_search($roleName, $this->userRoles[$userId]);
|
||||
if ($key !== false) {
|
||||
unset($this->userRoles[$userId][$key]);
|
||||
$this->userRoles[$userId] = array_values($this->userRoles[$userId]);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为角色分配权限
|
||||
*/
|
||||
public function assignPermissionToRole(string $roleName, string $permissionName): bool
|
||||
{
|
||||
if (!isset($this->roles[$roleName]) || !isset($this->permissions[$permissionName])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($this->rolePermissions[$roleName])) {
|
||||
$this->rolePermissions[$roleName] = [];
|
||||
}
|
||||
|
||||
if (!in_array($permissionName, $this->rolePermissions[$roleName])) {
|
||||
$this->rolePermissions[$roleName][] = $permissionName;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除角色权限
|
||||
*/
|
||||
public function removePermissionFromRole(string $roleName, string $permissionName): bool
|
||||
{
|
||||
if (!isset($this->rolePermissions[$roleName])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$key = array_search($permissionName, $this->rolePermissions[$roleName]);
|
||||
if ($key !== false) {
|
||||
unset($this->rolePermissions[$roleName][$key]);
|
||||
$this->rolePermissions[$roleName] = array_values($this->rolePermissions[$roleName]);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接为用户分配权限
|
||||
*/
|
||||
public function assignPermissionToUser(int $userId, string $permissionName): bool
|
||||
{
|
||||
if (!isset($this->permissions[$permissionName])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($this->userPermissions[$userId])) {
|
||||
$this->userPermissions[$userId] = [];
|
||||
}
|
||||
|
||||
if (!in_array($permissionName, $this->userPermissions[$userId])) {
|
||||
$this->userPermissions[$userId][] = $permissionName;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的所有角色
|
||||
*/
|
||||
public function getUserRoles(int $userId): array
|
||||
{
|
||||
return $this->userRoles[$userId] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的所有权限
|
||||
*/
|
||||
public function getUserPermissions(int $userId): array
|
||||
{
|
||||
$permissions = $this->userPermissions[$userId] ?? [];
|
||||
|
||||
// 获取角色权限
|
||||
$userRoles = $this->getUserRoles($userId);
|
||||
foreach ($userRoles as $roleName) {
|
||||
$rolePerms = $this->rolePermissions[$roleName] ?? [];
|
||||
$permissions = array_merge($permissions, $rolePerms);
|
||||
}
|
||||
|
||||
return array_unique($permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有指定角色
|
||||
*/
|
||||
public function hasRole(int $userId, string $roleName): bool
|
||||
{
|
||||
return in_array($roleName, $this->getUserRoles($userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有指定权限
|
||||
*/
|
||||
public function hasPermission(int $userId, string $permissionName): bool
|
||||
{
|
||||
return in_array($permissionName, $this->getUserPermissions($userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有多个角色中的任意一个
|
||||
*/
|
||||
public function hasAnyRole(int $userId, array $roleNames): bool
|
||||
{
|
||||
foreach ($roleNames as $roleName) {
|
||||
if ($this->hasRole($userId, $roleName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有所有指定角色
|
||||
*/
|
||||
public function hasAllRoles(int $userId, array $roleNames): bool
|
||||
{
|
||||
foreach ($roleNames as $roleName) {
|
||||
if (!$this->hasRole($userId, $roleName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有多个权限中的任意一个
|
||||
*/
|
||||
public function hasAnyPermission(int $userId, array $permissionNames): bool
|
||||
{
|
||||
foreach ($permissionNames as $permissionName) {
|
||||
if ($this->hasPermission($userId, $permissionName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有所有指定权限
|
||||
*/
|
||||
public function hasAllPermissions(int $userId, array $permissionNames): bool
|
||||
{
|
||||
foreach ($permissionNames as $permissionName) {
|
||||
if (!$this->hasPermission($userId, $permissionName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否可以访问指定资源
|
||||
*/
|
||||
public function canAccess(int $userId, string $resource, string $action): bool
|
||||
{
|
||||
$permissionName = "{$resource}:{$action}";
|
||||
return $this->hasPermission($userId, $permissionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色信息
|
||||
*/
|
||||
public function getRole(string $roleName): ?array
|
||||
{
|
||||
return $this->roles[$roleName] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限信息
|
||||
*/
|
||||
public function getPermission(string $permissionName): ?array
|
||||
{
|
||||
return $this->permissions[$permissionName] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有角色
|
||||
*/
|
||||
public function getAllRoles(): array
|
||||
{
|
||||
return $this->roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有权限
|
||||
*/
|
||||
public function getAllPermissions(): array
|
||||
{
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色的权限
|
||||
*/
|
||||
public function getRolePermissions(string $roleName): array
|
||||
{
|
||||
return $this->rolePermissions[$roleName] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查角色是否存在
|
||||
*/
|
||||
public function roleExists(string $roleName): bool
|
||||
{
|
||||
return isset($this->roles[$roleName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查权限是否存在
|
||||
*/
|
||||
public function permissionExists(string $permissionName): bool
|
||||
{
|
||||
return isset($this->permissions[$permissionName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
public function deleteRole(string $roleName): bool
|
||||
{
|
||||
if (!isset($this->roles[$roleName])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unset($this->roles[$roleName]);
|
||||
unset($this->rolePermissions[$roleName]);
|
||||
|
||||
// 从所有用户中移除该角色
|
||||
foreach ($this->userRoles as $userId => $roles) {
|
||||
$this->removeRoleFromUser($userId, $roleName);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除权限
|
||||
*/
|
||||
public function deletePermission(string $permissionName): bool
|
||||
{
|
||||
if (!isset($this->permissions[$permissionName])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unset($this->permissions[$permissionName]);
|
||||
|
||||
// 从所有角色中移除该权限
|
||||
foreach ($this->rolePermissions as $roleName => $permissions) {
|
||||
$this->removePermissionFromRole($roleName, $permissionName);
|
||||
}
|
||||
|
||||
// 从所有用户中移除该权限
|
||||
foreach ($this->userPermissions as $userId => $permissions) {
|
||||
$key = array_search($permissionName, $permissions);
|
||||
if ($key !== false) {
|
||||
unset($this->userPermissions[$userId][$key]);
|
||||
$this->userPermissions[$userId] = array_values($this->userPermissions[$userId]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限统计信息
|
||||
*/
|
||||
public function getStatistics(): array
|
||||
{
|
||||
return [
|
||||
'total_roles' => count($this->roles),
|
||||
'total_permissions' => count($this->permissions),
|
||||
'total_user_roles' => array_sum(array_map('count', $this->userRoles)),
|
||||
'total_role_permissions' => array_sum(array_map('count', $this->rolePermissions)),
|
||||
'total_user_permissions' => array_sum(array_map('count', $this->userPermissions)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量分配角色给用户
|
||||
*/
|
||||
public function assignRolesToUser(int $userId, array $roleNames): array
|
||||
{
|
||||
$results = [];
|
||||
foreach ($roleNames as $roleName) {
|
||||
$results[$roleName] = $this->assignRoleToUser($userId, $roleName);
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量分配权限给角色
|
||||
*/
|
||||
public function assignPermissionsToRole(string $roleName, array $permissionNames): array
|
||||
{
|
||||
$results = [];
|
||||
foreach ($permissionNames as $permissionName) {
|
||||
$results[$permissionName] = $this->assignPermissionToRole($roleName, $permissionName);
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空用户所有角色
|
||||
*/
|
||||
public function clearUserRoles(int $userId): void
|
||||
{
|
||||
$this->userRoles[$userId] = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空用户所有权限
|
||||
*/
|
||||
public function clearUserPermissions(int $userId): void
|
||||
{
|
||||
$this->userPermissions[$userId] = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空角色所有权限
|
||||
*/
|
||||
public function clearRolePermissions(string $roleName): void
|
||||
{
|
||||
$this->rolePermissions[$roleName] = [];
|
||||
}
|
||||
}
|
||||
176
fendx-framework/fendx-security/src/Token/TokenManager.php
Normal file
176
fendx-framework/fendx-security/src/Token/TokenManager.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Fendx\Security\Token;
|
||||
|
||||
use Fendx\Cache\Cache;
|
||||
use Fendx\Common\Exception\BusinessException;
|
||||
|
||||
final class TokenManager
|
||||
{
|
||||
private string $secretKey;
|
||||
private int $expiresIn;
|
||||
private string $algorithm;
|
||||
private string $cachePrefix;
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->secretKey = $config['secret_key'] ?? bin2hex(random_bytes(32));
|
||||
$this->expiresIn = $config['expires_in'] ?? 3600;
|
||||
$this->algorithm = $config['algorithm'] ?? 'HS256';
|
||||
$this->cachePrefix = $config['cache_prefix'] ?? 'token:';
|
||||
}
|
||||
|
||||
public function generate(array $payload): string
|
||||
{
|
||||
$header = [
|
||||
'typ' => 'JWT',
|
||||
'alg' => $this->algorithm
|
||||
];
|
||||
|
||||
$payload['iat'] = time();
|
||||
$payload['exp'] = time() + $this->expiresIn;
|
||||
$payload['jti'] = uniqid('token_', true);
|
||||
|
||||
$headerEncoded = $this->base64UrlEncode(json_encode($header));
|
||||
$payloadEncoded = $this->base64UrlEncode(json_encode($payload));
|
||||
|
||||
$signature = hash_hmac(
|
||||
'sha256',
|
||||
"$headerEncoded.$payloadEncoded",
|
||||
$this->secretKey,
|
||||
true
|
||||
);
|
||||
$signatureEncoded = $this->base64UrlEncode($signature);
|
||||
|
||||
$token = "$headerEncoded.$payloadEncoded.$signatureEncoded";
|
||||
|
||||
// 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.
|
||||
Cache::set($this->cachePrefix . $payload['jti'], [
|
||||
'user_id' => $payload['id'] ?? null,
|
||||
'expires_at' => $payload['exp']
|
||||
], $this->expiresIn);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function verify(string $token): ?array
|
||||
{
|
||||
$parts = explode('.', $token);
|
||||
|
||||
if (count($parts) !== 3) {
|
||||
throw new BusinessException(401, 'INVALID_TOKEN_FORMAT');
|
||||
}
|
||||
|
||||
[$headerEncoded, $payloadEncoded, $signatureEncoded] = $parts;
|
||||
|
||||
// 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.
|
||||
$header = json_decode($this->base64UrlDecode($headerEncoded), true);
|
||||
$payload = json_decode($this->base64UrlDecode($payloadEncoded), true);
|
||||
|
||||
if (!$header || !$payload) {
|
||||
throw new BusinessException(401, 'INVALID_TOKEN_PAYLOAD');
|
||||
}
|
||||
|
||||
// 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.
|
||||
if (!isset($payload['exp']) || $payload['exp'] < time()) {
|
||||
throw new BusinessException(401, 'TOKEN_EXPIRED');
|
||||
}
|
||||
|
||||
// 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.
|
||||
$expectedSignature = hash_hmac(
|
||||
'sha256',
|
||||
"$headerEncoded.$payloadEncoded",
|
||||
$this->secretKey,
|
||||
true
|
||||
);
|
||||
$expectedSignatureEncoded = $this->base64UrlEncode($expectedSignature);
|
||||
|
||||
if (!hash_equals($signatureEncoded, $expectedSignatureEncoded)) {
|
||||
throw new BusinessException(401, 'INVALID_TOKEN_SIGNATURE');
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
public function revoke(string $token): bool
|
||||
{
|
||||
try {
|
||||
$payload = $this->verify($token);
|
||||
|
||||
if (isset($payload['jti'])) {
|
||||
Cache::delete($this->cachePrefix . $payload['jti']);
|
||||
return true;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isRevoked(string $token): bool
|
||||
{
|
||||
try {
|
||||
$payload = $this->verify($token);
|
||||
|
||||
if (isset($payload['jti'])) {
|
||||
return !Cache::has($this->cachePrefix . $payload['jti']);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Token 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function revokeAll(): bool
|
||||
{
|
||||
// 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.
|
||||
$pattern = $this->cachePrefix . '*';
|
||||
|
||||
// 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.
|
||||
Cache::clear();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getPayload(string $token): ?array
|
||||
{
|
||||
try {
|
||||
return $this->verify($token);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getExpiresIn(): int
|
||||
{
|
||||
return $this->expiresIn;
|
||||
}
|
||||
|
||||
public function setExpiresIn(int $expiresIn): void
|
||||
{
|
||||
$this->expiresIn = $expiresIn;
|
||||
}
|
||||
|
||||
public function getSecretKey(): string
|
||||
{
|
||||
return $this->secretKey;
|
||||
}
|
||||
|
||||
public function setSecretKey(string $secretKey): void
|
||||
{
|
||||
$this->secretKey = $secretKey;
|
||||
}
|
||||
|
||||
private function base64UrlEncode(string $data): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
private function base64UrlDecode(string $data): string
|
||||
{
|
||||
return base64_decode(strtr($data, '-_', '+/'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user