Authenticating users is just half of the story. Even if you are writing some Web community hub or, God forbid, social network, and identifying users is crucial part of your business rules, you almost always need to control which users can access which part of the application's functionality. It can be said that there's no real point in authentication without access control accompanying it, which is, user authorization.
In this chapter, we'll see how Yii 2 can help us in preventing or allowing users to access the functionality of the web application. We will focus on the following four features of Yii:
As a code example, we'll implement a simple scheme of access control to our example CRM application based on the preceding four features.
Let's start simple.
We already learned about rudimentary access control in the previous chapter, when we wrote the code to render the authentication indicator like the following:
if (Yii::$app->user->isGuest)
// render the indicator for guests
else
// render the indicator for authenticated users
So, we are able to differentiate the content based on whether the user is authenticated or not.
Similarly, we cannot just differentiate the content, but totally prohibit visitors from entering some routes without authentication. For this we need to learn about two features of Yii: exception handling and the hook methods of the controller.
Similar to the beforeSave()
method we used in the previous chapter, Controller
classes also have hook methods. They are as follows:
Both of these hooks get the $action
argument, which represents the controller action being executed. This object is of little use, really, being just the container for the following:
Please note that in the case where beforeAction()
returns false
, the client browser will get a blank page without any explanations. This in fact can happen quite often when writing these hooks and forgetting to return a Boolean value from them! The same will happen if you forget to return $result
from afterAction()
.
While it's certainly interesting to know what this action really is, right now its brief description is perfectly sufficient. A close inspection of what role an action plays in route handling will be done in Chapter 12, Route Management. Most of the time, there's no need to care about the actions.
All possible hooks, how they work, and how we can use them for our benefit will be discussed in the Chapter 10, Events and Behaviors.
Given the definition of the beforeAction()
method, it is obvious that we can restrict access to some parts of the application in the simplest possible way:
public function beforeAction($action) { $parentAllowed = parent::beforeAction($action); $meAllowed = !Yii::$app->user->isGuest; return $parentAllowed and $meAllowed; }
First we check whether the parent implementation allows the execution of the action in question. We then check whether the current user is unauthenticated by querying the isGuest
property of the user
component. If both these conditions are true, then we allow the execution of the action.
What checks does the yiiwebController.beforeAction()
method do? It protects you from
Cross-Site Request Forgery (CSRF) attacks. In Yii 2, each and every form generated by the ActiveForm
widget, by default, includes a special token as the additional request parameter. Only if the expected token from the form is equal to the token included in the client request will the request be processed. Of course, this check is unnecessary (and is not being done) for the GET
, HEAD
, and OPTIONS
requests (the last one being extremely rare). You can also disable this check (don't do this) via the enableCsrfValidation
setting directly in the application configuration.
By the way, in the main layout file we made in Chapter 3, Automatically Generating the CRUD Code, and discussed in detail in Chapter 4, The Renderer, there is a call to the Html::csrfMetaTags()
method that we haven't even mentioned yet. This call is here exactly to support the CSRF protection. More than that, with the enableCsrfValidation
setting set to true
, you must call this method or else you will lose the ability to submit any HTML form generated by the ActiveForm
widget.
Most of the time you wouldn't do any serious harm if you don't call parent::beforeAction()
, but you better do it, because if you forget to do this in the form-submitting action you'll lose an important line of security.
However, this way of blocking is not really interesting. First, it does not show the client any feedback about what really happened. Second, such a usage of beforeAction()
is already anticipated in Yii 2 in the form of a special concept called
filters.
Let's postpone the explanation of filters for a while and look at how we can show the client's browser what really happened with its request.
Yii employs its own handler for unhandled exceptions, which is really usable right out of the box without any further improvements. Basically, if you throw an exception of any Exception
class descendant somewhere in your controller action and not handle it, Yii will show the following error message to the client:
However, here's what it will show if you throw the yiiwebNotFoundHttpException
:
Note that it correctly shows the status code and the error message according to the specification of HTTP status codes. The actual status code returned will also be 404.
In fact, yiiwebNotFoundHttpException
is a wrapper around the base yiiwebHttpException
, which you can throw to emit arbitrary status code and message to the client, as follows:
throw new HttpException(406, 'Pretty rare error, usually you should never see it.'),
Don't forget to write use yiiwebHttpException
at the beginning of your script. Here is what will be shown:
Note that the message provided in the exception constructor is shown underneath the main message of the 406 status code, which is autogenerated according to HTTP specification and is not changeable. The status code actually returned from the server will also be 406.
So, just by throwing the various kinds of HTTP exceptions you can block the user request with a descriptive message. Note, however, that nothing prevents someone from getting the following output when using yiiwebHttpException instead of its specialized subclasses:
Well, indeed anyone will think this is a server error. Even while Yii really returns status code 200, the response will be the error page. In case anyone tries to throw the HttpException
with the status code 302, for example, the status code will be 302 but no redirection will happen. This is just the static page error handler in all cases.
Here is the list of all descendants of HttpException
that Yii 2 defines for its users' convenience:
Exception |
HTTP status code shown |
---|---|
|
400 Bad Request |
|
409 Conflict |
|
403 Forbidden |
|
410 Gone |
|
405 Method Not Allowed |
|
406 Not Acceptable |
|
404 Not Found |
|
429 Too Many Requests (from RFC 6585 additional HTTP status codes, see http://tools.ietf.org/html/rfc6585) |
|
401 Unauthorized |
|
415 Unsupported Media Type |
Another good feature of Yii exception handling is its error reporting in case the debugging mode is enabled.
Inside the index.php
entry point script, before a call to require()
for Yii library, the constant YII_DEBUG
will be defined with the value of true
:
define('YII_DEBUG', true);
If an unhandled exception like the following one is thrown in the code (this is different from the HttpException
):
throw new LogicException('I am unhandled exception and I am proud of it'),
Then here's what will be shown instead of the generic "Exception" page:
This is a really amazing stacktrace page. It's so long that to fit it here we had to tear the non-unique pieces out of it. It gives several layers of code before the failed line with code highlighting, filenames, line numbers, and links to the documentation pages for the Yii classes and methods mentioned (!). It shows the server runtime configuration and also cookies sent by the client.
Okay, now here's the guard case that prevents unauthenticated users from using the controller action it is in:
if (Yii::$app->user->isGuest) throw new ForbiddenHttpException;
This is the simplest line of access control available for us in Yii 2. Of course, any check can be used as long as the HttpException
descendant is being thrown. This is so fundamental that Yii has a special built-in AccessControl
filter for it. We'll learn about the AccessControl
filter later. Let's learn about what a filter is in Yii terminology.
An action filter, in short, is the beforeAction()
and afterAction()
methods packed into a single class, pluggable to the Controller
instances through the Controller.behaviors()
method.
The benefits of this approach are as follows:
For the class to become the action filter for some controller, it needs to be:
ActionFilter
class.behaviors()
method.The ActionFilter
itself is the special case of the behaviors that are described in Chapter 10, Events and Behaviors. For now, it's sufficient to understand that a behavior is some class containing some methods that can be declared to be an integral part of some other class, extending its feature set. If you are familiar with the concept of "traits" from PHP 5.4, this is it.
We'll not look at how to create our own ActionFilter
, as it's quite unnecessary given that we already know how the beforeAction()
and afterAction()
methods work. Let's briefly look at the built-in Yii 2 filters instead. Detailed information about their usage can be read from their corresponding documentation pages.
Filter class |
What it does |
---|---|
|
This filter prevents or allows access to controller actions based on the HTTP method used for request. For example, you can allow only POST requests for your log in and log out actions with this filter. This is the only action filter that doesn't inherit the |
|
This filter caches the rendering result of the given controller's actions. It has a sufficiently complex and flexible configuration to control where, for how long, and which particular routes should be cached. Visit http://www.yiiframework.com/doc-2.0/yii-filters-pagecache.html for more information. |
|
This filter is conceptually equivalent to |
|
This filter prevents or allows access to the actions depending on the set of rules in a very flexible and expressive syntax. It is so powerful that often there's no need to implement any other form of access control in the controller at all. This filter can control access using the HTTP method, user authentication status, user role (to be defined later), action and/or controller ID, user IP, or even a custom callback provided. It responds with 403 Forbidden if the action is not to be executed. Visit http://www.yiiframework.com/doc-2.0/yii-filters-accesscontrol.html for more information, but we'll be discussing this filter in this chapter anyway. |
|
This filter is a very handy filter for large applications. It automatically changes the application language and response format based on the HTTP headers and query parameters send by the client browser. Visit http://www.yiiframework.com/doc-2.0/yii-filters-contentnegotiator.html for more information. |
|
This filter prevents the access for users who exceeded their rate limit, that is, amount of page requests in some time range. The user identities (explained in the previous chapter) need to implement the |
As this chapter is about access control, let's look at some examples of the usage of VerbFilter
and AccessControl
. The
AccessControl
filter is so important and powerful that we'll look at it later in more detail after we explore the user roles system.
Here's how Gii protects the controller autogenerated by the VerbFilter
:
public function behaviors() { return [ 'verbs' => [ 'class' => VerbFilter::className(), 'actions' => [ 'delete' => ['post'], ], ], ]; }
The behaviors()
method must return an array of configurations for the filters to be attached to the controller. In this case, we attach the VerbFilter
at the arbitrarily chosen verbs
key. This VerbFilter
is configured so that the delete
action can be accessed only by the POST request. All other actions are unguarded, because the update form request accepts the data being POSTed and renders the HTML page for the form.
Here's how we can quite naively protect our login
and logout
actions using the AccessControl
filter:
public function behaviors() { return [ 'access' => [ // 1 'class' => AccessControl::className(), // 2 'only' => ['login', 'logout'], // 3 'rules' => [ [ 'actions' => ['login'], // 4 'roles' => ['?'], // 5 'allow' => true, // 6 ], [ 'actions' => ['logout'], // 7 'roles' => ['@'], // 8 'allow' => true, // 9 ] ] ] ]; }
In the preceding code, we are using the behaviors
property of our Controller
to attach the yiifiltersAccessControl
class to it, which will be already configured as specified.
Let's understand the preceding code line-by-line from the top to bottom:
access
is used to return back to the calling method from the behaviors()
method.AccessControl
filter.only
variable denotes that only the login
and logout
actions, which translate to actionLogin()
and actionLogout()
methods of the controller, are used. The default for access control filter is to deny everything not explicitly allowed, so we need to restrict it here.login
action.?
means guest users.logout
action.@
means authenticated users.The specific configuration parameters for each rule can be read in the documentation and/or source of the yiifiltersAccessRule
class (http://www.yiiframework.com/doc-2.0/yii-filters-accessrule.html).
18.117.187.62