Laminas ListenerAggregate认证重定向登录页死循环问题
Laminas框架迁移后认证监听器重复重定向问题修复
问题描述
将应用从ZF2/3迁移至Laminas框架时,把认证校验逻辑从Module.php的bootstrap环节迁移到ListenerAggregate实现,预期未登录用户访问特定路由时自动重定向到登录页。该逻辑在原ZF版本运行正常,迁移后出现20-30次重复重定向,触发「页面重定向不正确」报错。
原有实现代码
Module.php的onBootstrap方法
class Module implements ConfigProviderInterface { /** * @param $event */ public function onBootstrap(MvcEvent $event) { /** @var ApplicationInterface $application*/ $application = $event->getApplication(); /** @var TemplateMapResolver $templateMapResolver */ $templateMapResolver = $application->getServiceManager()->get( 'ViewTemplateMapResolver' ); // Create and register layout listener $listener = new LayoutListener($templateMapResolver); $listener->attach($application->getEventManager()); //Create and register Authentication listener $authenticationListener = new AuthenticationListener(); $authenticationListener->attach($application->getEventManager()); } }
初始AuthenticationListener类实现
class AuthenticationListener extends AbstractListenerAggregate { /** * @param EventManagerInterface $events * @param int $priority */ public function attach(EventManagerInterface $events, $priority = 1) { $this->listeners[] = $events->attach( MvcEvent::EVENT_DISPATCH, [$this, 'userHasAuthentication'] ); } /** * @param MvcEvent $event */ public function userHasAuthentication(MvcEvent $event) { $authenticationService = $event->getApplication()->getServiceManager()->get(AuthenticationService::class); if($authenticationService->hasIdentity() === false){ $event->stopPropagation(true); // ? how to redirect } return true; } }
已尝试的无效重定向方案
- 方案一:直接调用控制器redirect插件
if($authenticationService->hasIdentity() === false){ $event->stopPropagation(true); /**@var AbstractActionController $target*/ $target = $event->getTarget(); return $target->redirect()->toRoute('auth.login'); }
- 方案二:手动设置响应头Location
if($authenticationService->hasIdentity() === false){ $event->stopPropagation(true); /**@var AbstractActionController $target*/ $target = $event->getTarget(); $response = $target->getResponse(); $response->getHeaders()->addHeaderLine('Location', '/login'); $response->setStatusCode(403); $response->sendHeaders(); return $response; }
问题原因
重复重定向的核心原因有三点:
- 未排除登录路由本身:未登录访问任意路由(包括登录路由
auth.login)都会触发重定向逻辑,访问登录页时因为没有身份又被重定向到登录页,形成死循环 - 事件选择不合理:
MvcEvent::EVENT_DISPATCH事件触发时控制器已经实例化,此时返回重定向响应没有正确绑定到MvcEvent的响应属性,导致事件循环没有终止,继续重复触发调度 - 错误调用
sendHeaders()方法:在事件阶段提前发送头信息会打断Laminas的完整请求响应流程,导致框架无法正常终止请求循环
正确实现方式
调整逻辑要点
- 把监听事件改为优先级更高的
MvcEvent::EVENT_ROUTE,在路由匹配完成后、控制器调度前就做校验,避免不必要的控制器实例化 - 设置高于默认值的优先级(比如100),保证认证逻辑在其他路由逻辑前执行
- 增加白名单判断,直接放行登录相关路由,避免循环重定向
- 构造标准的RedirectResponse,设置到MvcEvent中,同时终止事件传播,不要直接调用控制器插件或者手动发送头
修正后的AuthenticationListener代码
use Laminas\EventManager\AbstractListenerAggregate; use Laminas\EventManager\EventManagerInterface; use Laminas\Http\PhpEnvironment\Response; use Laminas\Mvc\MvcEvent; use Laminas\Router\Http\TreeRouteStack; use Laminas\Authentication\AuthenticationService; class AuthenticationListener extends AbstractListenerAggregate { /** * 不需要校验登录的路由白名单 * @var string[] */ protected array $allowedRoutes = [ 'auth.login', // 其他不需要登录的公开路由可在此处添加 ]; /** * @param EventManagerInterface $events * @param int $priority */ public function attach(EventManagerInterface $events, $priority = 100) { // 挂载到ROUTE事件,高优先级优先执行 $this->listeners[] = $events->attach( MvcEvent::EVENT_ROUTE, [$this, 'checkAuthentication'], $priority ); } /** * 登录校验逻辑 * @param MvcEvent $event * @return Response|null */ public function checkAuthentication(MvcEvent $event) { $routeMatch = $event->getRouteMatch(); // 没有匹配到路由直接放行,交给框架404逻辑处理 if (!$routeMatch) { return null; } $currentRoute = $routeMatch->getMatchedRouteName(); // 当前路由在白名单内直接放行 if (in_array($currentRoute, $this->allowedRoutes)) { return null; } /** @var AuthenticationService $authService */ $authService = $event->getApplication()->getServiceManager()->get(AuthenticationService::class); // 已登录直接放行 if ($authService->hasIdentity()) { return null; } // 未登录,构造重定向响应 /** @var TreeRouteStack $router */ $router = $event->getRouter(); $loginUrl = $router->assemble([], ['name' => 'auth.login']); /** @var Response $response */ $response = $event->getResponse(); $response->setStatusCode(302); $response->getHeaders()->addHeaderLine('Location', $loginUrl); // 把响应设置到事件对象,终止后续事件传播 $event->setResponse($response); $event->stopPropagation(true); return $response; } }
注意事项
- 必须维护公开路由白名单,登录路由一定要加入白名单,这是避免循环重定向的核心
- 不要在监听器中调用
sendHeaders()方法,框架会在请求运行结束后统一发送响应 - 构造重定向地址时使用路由器的
assemble()方法生成,不要硬编码路径,避免部署在子目录时路径错误 - 一定要把构造好的响应通过
$event->setResponse()绑定到事件对象,否则框架无法识别到已经生成响应,会继续执行后续调度逻辑
内容的提问来源于stack exchange,提问作者John Crest
相关产品推荐
相关产品推荐

