ZF2 and force HTTPS for specific routes

2019-01-28 10:32发布

问题:

I'm trying to achieve forced https when accessing any page under /account route. I've found this question ZF2 toRoute with https and it works... partially. My routes:

'router' => array(
    'routes' => array(
        'account' => array(
            'type' => 'Scheme',
            'options' => array(
                'route' => '/account',
                'scheme' => 'https',
                'defaults' => array(
                    'controller' => 'Account\Controller\Account',
                    'action' => 'index',
                ),
            ),
            'may_terminate' => true,
            'child_routes' => array(
                'default' => array(
                    'type' => 'Literal',
                    'options' => array(
                        'route' => '/',
                        'defaults' => array(
                            'controller' => 'Account\Controller\Account',
                            'action' => 'index',
                        ),
                    ),
                ),
                'signin' => array(
                    'type' => 'Segment',
                    'options' => array(
                        'route' => '/signin[/:type]',
                        'defaults' => array(
                            'controller' => 'Account\Controller\Account',
                            'action' => 'signin',
                        ),
                        'constraints' => array(
                            'type' => '[a-zA-Z][a-zA-Z0-9-_]*',
                        ),
                    ),
                ),
                'signout' => array(
                    'type' => 'Segment',
                    'options' => array(
                        'route' => '/signout',
                        'defaults' => array(
                            'controller' => 'Account\Controller\Account',
                            'action' => 'signout',
                        ),
                    ),
                ),
                'register' => array(
                    'type' => 'Segment',
                    'options' => array(
                        'route' => '/register[/:step]',
                        'defaults' => array(
                            'controller' => 'Account\Controller\Account',
                            'action' => 'register',
                        ),
                        'constraints' => array(
                            'step' => '[a-zA-Z][a-zA-Z0-9-_]*',
                        ),
                    ),
                ),
            ),
        ),
    ),
),

and a home route from Application module from Skeleton Application (cloned from github). Whenever I access any subroute of /account it throws 404:

http(s)://domain.my/account/signin = 404, wrong
http(s)://domain.my/account/* = 404, wron
https://domain.my/signin = signin page, wrong should be /account/signin
http://domain.my/ = ok, main page
http://domain.my/account = 404, wrong
https://domain.my/ = wrong, account page should be main page

Generally my problem is: the page should be accessed by http or https BUT /account and it subroutes have to be accessed only by https.

EDIT

Ok, I've tried the chained_routes but this is not what I wanted to achieve. I want to do something like this:

User not logged in: types: http://domain.my/account -> redirected to https://domain.my/account/login (I know I can achieve this with $authService->hasIdentity()) then redirect to https://domain.my/account

types: http://domain.my/account/login -> redireted to https://domain.my/account/login

types: http://domain.my/account/edit -> redirected to https://domain.my/account/login then to https://domain.my/account/edit

same with logged user when he access anything from /account route it is redirected to the same url but with https.

回答1:

If you want to redirect a user, you can't do this with routes. Simply said, you have to accept the route match first, then check if the scheme used is https and if not, redirect. This will be controller logic then. So ignore the scheme route in your use case and check https in your controller.

In Zend Framework 1, we had a custom helper Https which you could use to force a page to be redirected to https if the scheme was http:

public function init ()
{
    $this->https->forceHttps(array('index', 'login', 'edit'));
}

public function indexAction ()
{
    // code here
}

public function loginAction ()
{
    // code here
}

public function editAction ()
{
    // code here
}

If you hit index, login or edit on http, you would be redirected to https. If you used https, there was no redirect.

Currently we do not have such plugin for Zend Framework 2, but I think that's the solution you have to look for. Make the feature a controller plugin, so you can reuse it among different controllers. An example for Zend Framework 2 might more like this:

use Zend\Http\Response;

public function loginAction()
{
    // If return value is response, this means the user will be redirected
    $result = $this->forceHttps();
    if ($result instanceof Response) {
        return $result;
    }

    // code here
}

The controller plugin might look like this:

use Zend\Uri\Http as HttpUri;

class ForceHttps extends AbstractPlugin
{
    public function __invoke()
    {
        $request = $this->getController()->getRequest();

        if ('https' === $request->getUri()->getScheme()) {
            return;
        }

        // Not secure, create full url
        $plugin = $this->getController()->url();
        $url    = $plugin->fromRoute(null, array(), array(
            'force_canonical' => true,
        ), true);

        $url    = new HttpUri($url);
        $url->setScheme('https');

        return $this->getController()->redirect()->toUrl($url);
    }
}

Note I have not tested this, so there might be a few bugs in the code. But you should get the idea by this example.



回答2:

I've had a similar problem but approached it using events. Redirecting is a cross cutting concern, if you include in each controller it gets hard to maintain. This code shows how you can redirect all http requests to https. If you only want some you can add logic into the doHttpsRedirect(). An array in the config file showing which actions should redirect would be a simple way of adding this logic.

class Module
{
    ...

    public function onBootstrap(MvcEvent $e){
        $em = $e->getApplication()->getEventManager();
        $moduleRouteListener = new ModuleRouteListener();
        $moduleRouteListener->attach($em);
        $em->attach('route', array($this, 'doHttpsRedirect'));
        ...
    }

    public function doHttpsRedirect(MvcEvent $e){
        $sm = $e->getApplication()->getServiceManager();
        $uri = $e->getRequest()->getUri();
        $scheme = $uri->getScheme();
        if ($scheme != 'https'){
            $uri->setScheme('https');
            $response=$e->getResponse();
            $response->getHeaders()->addHeaderLine('Location', $uri);
            $response->setStatusCode(302);
            $response->sendHeaders();
            return $response;
        }
    }

    ... 
}


回答3:

TBH, from a security perspective if any of the pages are over https all of them should be, as you can't then rely that further requests haven't been man-in-the-middled.

See http://www.troyhunt.com/2013/05/your-login-form-posts-to-https-but-you.html

As for solving you're actual problem, I think you've got a misconception of how the scheme route works, look at this pr from dasprid for an example of using https and chaining routes https://github.com/zendframework/zf2/pull/3999



回答4:

Ok, based on what Jurian Sluiman said and what I have found digging internet I've ended with something like this using controller plugin.

Module config:

return array(    
    'controller_plugins' => array(
        'invokables' => array(
            'ForceHttps' => 'Account\Controller\Plugin\ForceHttps',
        ),
    ),
/* rest of the configuration */

Plugin:

<?php
/**
 * module/Account/src/Account/Controller/Plugin/ForceHttps.php
 *
 * $Id$
 */
namespace Account\Controller\Plugin;

use Zend\Mvc\Controller\Plugin\AbstractPlugin,
    Zend\Uri\Http as HttpUri;

class ForceHttps extends AbstractPlugin
{
    public function __invoke()
    {
        $request = $this->getController()->getRequest();

        // if we're over https then everything is ok
        if ('https' == $request->getUri()->getScheme())
        {
            return;
        }
        // Not secure, crete url and redirect only if the method is GET
        if ('GET' == $request->getMethod())
        {
            $uri = $request->getUri();

            $url = new HttpUri($uri);
            $url->setScheme('https');
            $url->setPort('443');

            return $this->getController()->redirect()->toUrl($url);
        }

        // all other methods should throw error
        throw new \Exception("ERROR [116]: Insecure connection. Please use secure connection (HTTPS)");
    }
}

Controller:

<?php
/**
 * module/Account/src/Account/Controller/AccountController.php
 *
 * $Id$
 */
namespace Account\Controller;

use Zend\Mvc\Controller\AbstractActionController,
    Zend\View\Model\ViewModel,
    Zend\EventManager\EventManagerInterface;

class AccountController extends AbstractActionController
{
    /**
     * Inject an EventManager instance
     *
     * @param EventManagerInterface $eventManager
     * @return void
     */
    public function setEventManager(EventManagerInterface $events)
    {
        parent::setEventManager($events);

        $controller = $this;

        $events->attach('dispatch', function($e) use ($controller) {
            if (($result = $controller->forceHttps()) instanceof Response) {
                return $result;
            }
        }, 100);

        return $this;
    }

    public function indexAction()
    {       
        return new ViewModel(
        );
    }

    /* rest of the actions */
}

In the controller I've used setEventManager cause I've read that this can be replacement for init() function from ZF1. And now when user enters any action of Account controller over http it is redirected to https connection but when he tries to post something to account controller over http connection he gets error. And that's what I wanted.