この記事は Yii 1.1 用に書いた Yiiのアクセスコントロールの簡単な作り方 の Yii2 版です。
YiiにはRBAC(ロールベースアクセスコントロール)と呼ばれる機能が備わっています。これは、ユーザーのロールを階層的に定義して、それぞれのロールに権限を持たせるという、非常に本格的なものです。
使い方としては、1 と比べてずいぶん気軽なメソッド名になりました。
if (!Yii::$app->user->can('manage')) {
throw new ForbiddenHttpException('アクセスできません');
}
また、Yii 2 になって、AccessControl アクションフィルタもビヘイビア内のオブジェクト構成情報で完結する綺麗なかたちになり、なにかと一貫性が高まってカスタマイズしやすくなりました。
use yii\filters\AccessControl;
class SomeController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'rules' => [
[
'allow' => true,
'roles' => ['manage'],
],
],
],
];
}
が、まだまだ定義が...
使いやすいのはいいけど、機能の開発中にデータベースにロール階層を定義したり、ロールを構成するPHPコードをメンテしたりするのは面倒です。簡単なアプリケーションやプロトタイピング段階では、逆にデメリットになってしまいます。
そこでまたまた、かんたんRBACです。Yii 1 のときは CWebUser をすり替えました。Yii 2 ではもっと手軽にやれます。
RBAC のメカニズムのキーは、Yii::$app->user->can(...)
です。このメソッドは Yii::$app->authManager
を使う前提で実装されています。
というわけで、Applicationのコンフィグに authManager
が必要です。このデフォルトのコンポーネントは勝手に生成されないので、なにか明示的に作る必要があります。
config/web.php
をこう変更しましょう。(Application の構成情報のことで、もしかしたら別の名前のファイルかもしれません)
return [
// ...
'components' => [
// ...
'authManager' => [
'class' => '\app\AuthManager',
],
// ...
],
];
本当なら、 \yii\rbac\ManagerInterface
のすべてのメソッドを実装するクラスとして、yii\rbac\DbManager
か yii\rbac\PhpManager
あるいはその互換実装が必要なのですが、このインターフェース、メソッド数がけっこう多い。
実はこの ManagerInterface 、\yii\web\User
の can()
が checkAccsess
メソッドを呼んでいるだけで、他に外から使わているメソッドはひとつもないのです。
public function checkAccess($userId, $permissionName, $params = []);
他のメソッドの大多数は、ユーザーがロール階層構築に利用するために定義されていて、こっちから呼び出さなければ使われることはありません。いくらか呼び出されているメソッドも、内部的にロール階層構築に使う以外の目的では使われていません。
※ 個人的には、ManagerInterface
の中から checkAccsess()
だけ実装したインターフェースを取り出して、User
は ManagerInterface
ではなくその小さなインターフェースに依存するという設計が、インターフェース分離原則的に適切だったんじゃないかと思います。
と、いうことは... authManager
はこんなダックタイプでも動くわけで...
<?php
namespace app;
use yii\base\Component;
class AuthManager extends Component
{
/**
* @param string|integer $userId
* @param string $permissionName
* @param array $params
* @return boolean
* @throws \yii\base\InvalidParamException
*/
public function checkAccess($userId, $permissionName, $params = [])
{
// TODO: なにか実装する
// TODO: 想定外の $permissionName なら InvalidParamException を投げる
}
}
じゃあ、どう実装するか
public function checkAccess($userId, $permissionName, $params = [])
{
if (empty($userId)) {
return false;
}
$dbUser = \app\models\UserActiveRecord::findOne($userId);
switch ($permissionName) {
case 'admin':
return $dbUser->is_admin;
case 'manage':
return $dbUser->is_admin || $dbUser->is_manager;
default:
throw new \yii\base\InvalidParamException("Unknown role '{$permissionName}'.");
}
}
ロール2つで済むならこんな感じで完了です。
データベースにアクセスしているので何度も問い合わせられたら困ると思うかもしれませんが、可否を調べた結果は \yii\web\User
がしばらく保持します。必要以上に呼ばれることはありません。
これを使い、ユーザーが manage
できるかどうか、できるならパス、もしかしたらログインしたらできるかもしれない場合はログインへ、ログインしてるのにダメなら HTTP の 403 エラー確定、という制限はこう書けます。
$user = Yii::$app->user;
if (!$user->can('manage')) {
if ($user->isGuest) {
$user->loginRequired();
} else {
throw new ForbiddenHttpException('oops');
}
}
で、こんなのがコントローラーの action1
にも action2
にも書いてある場合、 AccessControl
フィルタビヘイビアでまとめるとこうなります。
class SomeController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'rules' => [
[
'actions' => ['action1', 'action2'],
'allow' => true,
'roles' => ['manage'],
],
],
],
];
}
おてがるですね。
ダックタイプな実装にすり替えても動くのがミソです。インターフェースを合わせないと将来の変更が心配な人は、yii\rbac\PhpManager
あたりのコードを継承しておいてもいいんじゃないでしょうか。