feat(database): 添加用户角色权限系统及相关监控功能

- 创建用户表(users)包含基本信息和认证字段
- 创建角色表(roles)用于权限控制
- 创建权限表(permissions)定义系统权限
- 创建用户角色关联表(user_roles)建立用户与角色关系
- 创建角色权限关联表(role_permissions)建立角色与权限关系
- 创建迁移记录表(migrations)追踪数据库变更
- 添加AdminController提供管理员面板功能
- 实现系统监控、配置管理、缓存清理等功能
- 添加AOP切面编程支持的各种通知类型
- 实现告警管理AlertManager支持多渠道告警
- 添加文档注解接口规范
This commit is contained in:
Lawson
2026-04-08 17:00:28 +08:00
commit 2782d765fb
270 changed files with 107192 additions and 0 deletions

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

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

View 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;
}
}
/**
* 生成JTIJWT 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);
}
}

View 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] = [];
}
}

View 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, '-_', '+/'));
}
}