Symfony PHP8中Security属性无法重复使用的解决方案求助
解决多个Security属性无法重复使用的问题
针对你遇到的Security属性不能重复标注,且合并表达式无法区分不同状态码的问题,这里有几个实用方案:
方案1:自定义可重复的Security属性
自己实现一个支持重复标注的权限校验属性,完全复用原有表达式逻辑,同时保留不同状态码和消息的配置:
步骤1:创建自定义可重复属性
# src/Attribute/RepeatableSecurity.php namespace App\Attribute; use Attribute; #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class RepeatableSecurity { public function __construct( public string $expression, public int $statusCode = 403, public string $message = '' ) {} }
步骤2:编写属性处理监听器
注册一个事件监听器,在控制器执行前逐个校验每个RepeatableSecurity属性的表达式:
# src/EventListener/RepeatableSecurityListener.php namespace App\EventListener; use App\Attribute\RepeatableSecurity; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\Security\Core\Security; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; class RepeatableSecurityListener implements EventSubscriberInterface { public function __construct( private Security $security, private ExpressionLanguage $expressionLanguage ) {} public static function getSubscribedEvents(): array { return [ ControllerEvent::class => 'onControllerEvent', ]; } public function onControllerEvent(ControllerEvent $event): void { $controller = $event->getController(); if (is_array($controller)) { [$controllerInstance, $methodName] = $controller; $reflectionMethod = new \ReflectionMethod($controllerInstance, $methodName); $attributes = $reflectionMethod->getAttributes(RepeatableSecurity::class); foreach ($attributes as $attribute) { /** @var RepeatableSecurity $securityAttr */ $securityAttr = $attribute->newInstance(); // 构建表达式上下文,传入user和请求中的参数(比如thing) $context = [ 'user' => $this->security->getUser(), 'request' => $event->getRequest(), 'thing' => $event->getRequest()->attributes->get('thing'), ]; // 执行表达式校验 if (!$this->expressionLanguage->evaluate($securityAttr->expression, $context)) { throw new HttpException( $securityAttr->statusCode, $securityAttr->message ?: 'Access Denied' ); } } } } }
步骤3:在控制器中使用
替换原有Security属性为自定义的RepeatableSecurity:
use App\Attribute\RepeatableSecurity; #[RepeatableSecurity('thing.getUserId() == user.getUserId()', statusCode: 403)] #[RepeatableSecurity('thing.someCheck() == false', statusCode: 410, message: 'Something something')] public function controllerMethod(Thing $thing) { // 控制器逻辑 }
方案2:将校验逻辑移到控制器内部
如果不想自定义属性,直接把原有属性的表达式逻辑移到控制器方法开头,手动校验并抛出对应异常:
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; public function controllerMethod(Thing $thing) { // 第一个校验:403状态码 if ($thing->getUserId() !== $this->getUser()->getUserId()) { throw new AccessDeniedHttpException(); } // 第二个校验:410状态码 if (!$thing->someCheck()) { throw new HttpException(410, 'Something something'); } // 控制器逻辑 }
这个方案最简单直接,完全保留原有逻辑,只是从属性转移到了代码层面。
方案3:自定义Security表达式函数
通过扩展Security的表达式语言,添加一个支持抛出指定异常的函数,在单个Security属性中调用多次:
步骤1:注册自定义表达式函数
# src/Security/Expression/ThrowExceptionFunction.php namespace App\Security\Expression; use Symfony\Component\ExpressionLanguage\FunctionNode; use Symfony\Component\ExpressionLanguage\Compiler; use Symfony\Component\ExpressionLanguage\FunctionProviderInterface; use Symfony\Component\HttpKernel\Exception\HttpException; class ThrowExceptionFunction implements FunctionProviderInterface { public function getFunctions(): array { return [ 'checkAndThrow' => [ 'compiler' => function (Compiler $compiler, FunctionNode $node) { $compiler ->raw('if (!(') ->compile($node->args[0]) ->raw(')) { throw new \Symfony\Component\HttpKernel\Exception\HttpException(') ->compile($node->args[1]) ->raw(', ') ->compile($node->args[2]) ->raw('); }') ; }, 'evaluator' => function ($arguments, $condition, $statusCode, $message) { if (!$condition) { throw new HttpException($statusCode, $message); } return true; }, ], ]; } }
步骤2:配置表达式函数
在services.yaml中注册:
services: App\Security\Expression\ThrowExceptionFunction: tags: ['security.expression_language.function']
步骤3:在单个Security属性中使用
#[Security('checkAndThrow(thing.getUserId() == user.getUserId(), 403, "") and checkAndThrow(thing.someCheck() == false, 410, "Something something")')] public function controllerMethod(Thing $thing) { // 控制器逻辑 }
这个方案用单个属性实现多校验,每个校验失败时抛出对应状态码的异常。
内容的提问来源于stack exchange,提问作者PaaPs
相关产品推荐
相关产品推荐

