update permission middleware

This commit is contained in:
2026-03-12 14:04:32 +08:00
parent 5c28488bc5
commit 6ff5320ace
3 changed files with 326 additions and 5 deletions
@@ -12,6 +12,7 @@ use App\Service\ScopeBitmapService;
use App\Service\ScopeTableManager; use App\Service\ScopeTableManager;
use Hyperf\DbConnection\Db; use Hyperf\DbConnection\Db;
use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse; use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse;
use Hyperf\HttpServer\Router\Dispatched;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\MiddlewareInterface;
@@ -33,7 +34,7 @@ class PermissionMiddleware implements MiddlewareInterface
// 获取已认证用户(由 AuthMiddleware 预先认证) // 获取已认证用户(由 AuthMiddleware 预先认证)
$user = $this->auth->guard('jwt')->user(); $user = $this->auth->guard('jwt')->user();
if (!$user) { if (!$user) {
return $handler->handle($request); return $this->forbiddenResponse('用户认证异常');
} }
// 获取用户 scope(含角色和 bitmap // 获取用户 scope(含角色和 bitmap
@@ -44,11 +45,14 @@ class PermissionMiddleware implements MiddlewareInterface
$role = $user_scope['role']; $role = $user_scope['role'];
$method = $request->getMethod(); $method = $request->getMethod();
$path = $request->getUri()->getPath();
// 通过 Dispatched 获取路由模板路径(如 /api/v1/users/{id}),解决参数化路由匹配问题
$dispatched = $request->getAttribute(Dispatched::class);
$route_path = $dispatched?->handler?->route ?? $request->getUri()->getPath();
// ===== Step 1: 路由访问检查 ===== // ===== Step 1: 路由访问检查 =====
if ($role !== 'administrator') { if ($role !== 'administrator') {
$access_result = $this->checkRouteAccess($user->role_id, $method, $path); $access_result = $this->checkRouteAccess($user->role_id, $method, $route_path);
if ($access_result === false) { if ($access_result === false) {
return $this->forbiddenResponse('无权访问该接口'); return $this->forbiddenResponse('无权访问该接口');
} }
@@ -76,8 +80,8 @@ class PermissionMiddleware implements MiddlewareInterface
// 查找路由记录 // 查找路由记录
$route = Route::query()->where('method', $method)->where('path', $path)->first(); $route = Route::query()->where('method', $method)->where('path', $path)->first();
if (!$route) { if (!$route) {
// 未注册到 routes 表的路由默认放行 // 白名单模式:未注册到 routes 表的路由拒绝访问
return true; return false;
} }
// 1. 先查 override(优先级最高) // 1. 先查 override(优先级最高)
@@ -0,0 +1,226 @@
<?php
declare(strict_types=1);
namespace HyperfTest\Cases\Integration\Permission;
use App\Model\Role;
use App\Model\RoleRouteOverride;
use App\Model\Route;
use App\Model\RouteGroup;
use App\Model\Store;
use App\Model\User;
use App\Model\UserDataScope;
use Hyperf\DbConnection\Db;
use HyperfTest\TestCase;
use Qbhy\HyperfAuth\AuthManager;
use function Hyperf\Support\make;
/**
* 权限中间件端到端集成测试
*
* 覆盖 AuthMiddleware → PermissionMiddleware → Controller 完整链路
*
* @internal
* @coversNothing
*/
class PermissionFlowTest extends TestCase
{
protected function getAuthToken(User $user): string
{
$auth = make(AuthManager::class);
return $auth->guard('jwt')->login($user);
}
protected function authHeaders(User $user): array
{
return ['Authorization' => 'Bearer ' . $this->getAuthToken($user)];
}
protected function createTestUser(string $role_name, array $overrides = []): User
{
$role = Role::query()->where('name', $role_name)->firstOrFail();
$suffix = bin2hex(random_bytes(4));
return User::query()->create(array_merge([
'username' => 'integ_perm_' . $suffix,
'email' => 'integ_perm_' . $suffix . '@example.com',
'password' => 'Pass_' . $suffix,
'status' => 1,
'role_id' => $role->id,
], $overrides));
}
/**
* 为指定角色授权指定路由的路由组
*
* @return array{group: RouteGroup, old_group_id: ?int} 用于清理
*/
protected function authorizeRouteGroup(int $role_id, Route $route): array
{
$group = RouteGroup::query()->create([
'name' => 'integ_test_' . uniqid(),
'label' => '集成测试路由组',
]);
$old_group_id = $route->group_id;
$route->group_id = $group->id;
$route->save();
Db::table('role_route_groups')->insert([
'role_id' => $role_id,
'group_id' => $group->id,
]);
return ['group' => $group, 'old_group_id' => $old_group_id];
}
/**
* 清理路由组授权
*/
protected function cleanupRouteGroup(int $role_id, Route $route, array $auth_data): void
{
Db::table('role_route_groups')
->where('role_id', $role_id)
->where('group_id', $auth_data['group']->id)
->delete();
$route->group_id = $auth_data['old_group_id'];
$route->save();
$auth_data['group']->delete();
}
// ========== 测试用例 ==========
public function test_admin_full_access(): void
{
$user = $this->createTestUser('administrator');
// administrator 可访问全部 UserController 端点
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
$response->assertStatus(200);
$response->assertJsonPath('code', 0);
// 参数化路由
$response = $this->get('/api/v1/users/1', [], $this->authHeaders($user));
$this->assertContains($response->getStatusCode(), [200, 404]);
}
public function test_developer_with_group_auth_access(): void
{
$user = $this->createTestUser('developer');
$route = Route::query()->where('method', 'GET')->where('path', '/api/v1/users')->first();
if (!$route) {
$this->markTestSkipped('routes 表中无 GET /api/v1/users 路由');
}
$auth_data = $this->authorizeRouteGroup($user->role_id, $route);
try {
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
$response->assertStatus(200);
} finally {
$this->cleanupRouteGroup($user->role_id, $route, $auth_data);
}
}
public function test_accessor_without_auth_denied(): void
{
$user = $this->createTestUser('accessor');
// accessor 无任何路由授权 → 403
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
$response->assertStatus(403);
}
public function test_accessor_with_override_access(): void
{
$user = $this->createTestUser('accessor');
$route = Route::query()->where('method', 'GET')->where('path', '/api/v1/users')->first();
if (!$route) {
$this->markTestSkipped('routes 表中无 GET /api/v1/users 路由');
}
// 添加 store scope 以通过数据范围检查
$store = Store::query()->first();
if ($store) {
UserDataScope::query()->create([
'user_id' => $user->id,
'scope_type' => 'store',
'scope_id' => $store->id,
]);
}
// 设置 override 允许访问
RoleRouteOverride::query()->create([
'role_id' => $user->role_id,
'route_id' => $route->id,
'allowed' => true,
]);
try {
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
$response->assertStatus(200);
} finally {
RoleRouteOverride::query()
->where('role_id', $user->role_id)
->where('route_id', $route->id)
->delete();
UserDataScope::query()->where('user_id', $user->id)->delete();
}
}
public function test_override_deny_overrides_group(): void
{
$user = $this->createTestUser('developer');
$route = Route::query()->where('method', 'GET')->where('path', '/api/v1/users')->first();
if (!$route) {
$this->markTestSkipped('routes 表中无 GET /api/v1/users 路由');
}
// 授权路由组
$auth_data = $this->authorizeRouteGroup($user->role_id, $route);
// 设置 override 为拒绝(优先级高于 group)
RoleRouteOverride::query()->create([
'role_id' => $user->role_id,
'route_id' => $route->id,
'allowed' => false,
]);
try {
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
$response->assertStatus(403);
} finally {
RoleRouteOverride::query()
->where('role_id', $user->role_id)
->where('route_id', $route->id)
->delete();
$this->cleanupRouteGroup($user->role_id, $route, $auth_data);
}
}
public function test_developer_scope_param_validation(): void
{
$user = $this->createTestUser('developer');
$route = Route::query()->where('method', 'GET')->where('path', '/api/v1/users')->first();
if (!$route) {
$this->markTestSkipped('routes 表中无 GET /api/v1/users 路由');
}
// 授权路由访问
$auth_data = $this->authorizeRouteGroup($user->role_id, $route);
try {
// 传不属于自己的 platform_id → 403
$response = $this->get('/api/v1/users', ['platform_id' => 999999], $this->authHeaders($user));
$response->assertStatus(403);
} finally {
$this->cleanupRouteGroup($user->role_id, $route, $auth_data);
}
}
}
@@ -260,4 +260,95 @@ class PermissionMiddlewareTest extends TestCase
$response->assertStatus(200); $response->assertStatus(200);
$response->assertJsonPath('code', 0); $response->assertJsonPath('code', 0);
} }
// ========== 安全修复测试 ==========
public function test_unregistered_route_denied_for_non_admin(): void
{
$user = $this->createTestUser('developer');
// 从 routes 表删除 GET /api/v1/users 记录,模拟未注册场景
$route = Route::query()->where('method', 'GET')->where('path', '/api/v1/users')->first();
if (!$route) {
$this->markTestSkipped('routes 表中无 GET /api/v1/users 路由');
}
$route_data = $route->toArray();
$route->delete();
try {
// 白名单模式:routes 表中无记录 → 403
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
$response->assertStatus(403);
} finally {
// 恢复路由记录
Route::query()->create($route_data);
}
}
public function test_admin_allowed_on_unregistered_route(): void
{
$user = $this->createTestUser('administrator');
// 从 routes 表删除 GET /api/v1/users 记录
$route = Route::query()->where('method', 'GET')->where('path', '/api/v1/users')->first();
if (!$route) {
$this->markTestSkipped('routes 表中无 GET /api/v1/users 路由');
}
$route_data = $route->toArray();
$route->delete();
try {
// administrator 跳过路由检查,即使路由未在 routes 表中也应正常访问
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
$response->assertStatus(200);
} finally {
// 恢复路由记录
Route::query()->create($route_data);
}
}
public function test_parametric_route_matches_template_path(): void
{
$user = $this->createTestUser('developer');
$route = Route::query()
->where('method', 'GET')
->where('path', '/api/v1/users/{id}')
->first();
if (!$route) {
$this->markTestSkipped('routes 表中无 GET /api/v1/users/{id} 路由');
}
// 创建路由组并授权
$group = RouteGroup::query()->create([
'name' => 'test_param_route_' . uniqid(),
'label' => '参数化路由测试',
]);
$old_group_id = $route->group_id;
$route->group_id = $group->id;
$route->save();
Db::table('role_route_groups')->insert([
'role_id' => $user->role_id,
'group_id' => $group->id,
]);
// 访问参数化路由 /api/v1/users/1(应匹配模板路径 /api/v1/users/{id}
$response = $this->get('/api/v1/users/1', [], $this->authHeaders($user));
// 路由检查应通过(200 或 404 取决于用户是否存在,但不应是 403)
$this->assertNotEquals(403, $response->getStatusCode());
// 清理
Db::table('role_route_groups')
->where('role_id', $user->role_id)
->where('group_id', $group->id)
->delete();
$route->group_id = $old_group_id;
$route->save();
$group->delete();
}
} }