Webアプリケーションでのアクセス権の分離システムの最適化

Webアプリケーションでのアクセス権の分離システム実装に関する最後の記事を書いた後、多くの興味深いコメントが登場しました。 彼らは主にあなたがそれをさらに良くすることができると議論しました。



実際、このシステムは現在最適化されておらず、トラフィックの多いサーバーでは使用できません(以前の記事はレビュー用に作成されているため)。

修正してみましょう。



この記事では、以下を検討します。

1.ビットフィールド、最適化

2.データベーステーブルの非正規化でシリアル化する

3. Zend ACLのようなシステムがどのように機能するかを学びます。





どこで止めたの



データベース内の各オブジェクトについて、ユーザーまたはグループによる特定のアクションの権利を保存しているという事実に立ち止まりました。

これは、視覚的には2次元のテーブル(アクション、グループ)のように見えます。

message_view message_create message_delete message_edit
User21 + + +
禁止 - - - -
ユーザー +
管理者 + + + +
最初に思い浮かんだのは、グループ内の各ユーザーのビットを組み合わせる可能性です。



最初の変換。 ビットマスクを使用します。



ユーザーとグループはいくつでも構いません。 そして、オブジェクトに対するアクションの数は一定です。

したがって、特定のユーザーのすべてのアクションをデータベースのバイナリマスクの形式で一度に記録できます。



プラスの代わりに1があり、アクション0がありません。



私たちは何に勝ちますか?

Webアプリケーションのさまざまなアクションは、通常64を超えません。したがって、ほとんどの場合、64ビットで十分です。 (8バイト)

比較のために、このようなデータベースの各セルは40バイトを占有します。 これは、各アクションを個別に保存する場合と同じです。この場合のみ、ユーザーごとに複数のレコードがあります。 顔に保存:)

新しいテーブルは次のようになります。

許す 許さない
User21 1101 0000
禁止 0000 1111
ユーザー 1000 0000
管理者 1111 0000
人にとってはそれほど明白ではありませんが、コンピューターにとってはより明確になりました。



これで(前回の記事で説明した)データベースは次のようになります。

rights_action
ObjectRightsID:INT(pk) UserGroupID:INT(fk) 許可:VARBINARY(4) 許可しない:VARBINARY(4)
rights_group(グループをユーザーに設定)
UserRightsID:INT(pk) UserGroupID:INT
サブオブジェクトの権限をオブジェクトのグローバルカテゴリと組み合わせる場合、単純なOR(許可の場合はA | B)およびSUB(許可の場合はA&!B)操作を使用できます。



ここで、プログラムの一部が混乱しないように、プログラマーのインターフェースの問題に対処します。

アクションのビットの定義を紹介します。

public $actions=array();



/* SetAction



  

@param {string} ActionName -

@param {int} bitNumber -

@return {Object ObjRights} -

*/

public function SetAction($ActionName, $bitNumber){

//

if (array_search($bitNumber, $ this ->actions))

return false ;

//

if (isset($ this ->actions[$ActionName]))

return false ;

//

$clone = clone $ this ;

$clone->actions[$ActionName]=$bitNumber;



return $clone;

}




前回の記事から小さなクラスのリファクタリングを作成しましょう。 まず、ユーザークラスとアクションを分離します。 これは、アクションがユーザーに関連付けられるべきではなく、オブジェクトに関連付けられるため、必要です。 (たとえば、メッセージオブジェクトには読み取り、削除、および編集アクションがあります。アカウントオブジェクトにはマージ、表示などのアクションがあります)



アクションビットが重複しないようにするにはどうすればよいですか?

各アクションがビット番号ではなく文字列(たとえば、 'message_view')によって決定された場合、プログラマーにとってすべてが完全に明確でした。 特定の合意(たとえば、アクションObject name_action nameを定義する)があり、これにより、これが属するオブジェクトと、交差のない複数のオプションが明確になりました。



データベースの互換性を維持するには、個別の一般的なデータベースオブジェクトタイプ->アクション->ビット番号を使用するか、1つのファイルをすべてに使用することに同意します。



そのようなファイルの例を示します。

//

$MessageRights= new ObjRights();



//

$MessageRights=$MessageRights->SetAction( 'message_view' ,1)->SetAction( 'message_read' ,2)->SetAction( 'message_edit' ,3)->SetAction( 'message_delete' ,4)->SetAction( 'message_create' ,5);



//

$MessageRights=$MessageRights->SetAction( 'comment_view' ,6)->SetAction( 'comment_create' ,7)->SetAction( 'comment_delete' ,8);



//... , .

//

$UserRights= new ObjRights();



$UserRights=$UserRights->SetAction( 'user_edit' ,1)->SetAction( 'user_delete' ,2)->SetAction( 'user_create' ,3);






今度は、ユーザーに対処し、既製の実用的なプログラムを入手します。

ユーザーの場合、何も変わりません。 唯一の変更点は、異なるクラスのユーザー権利オブジェクトを個別に使用する必要があることです。

//

class UsrRights{

public $groupID=array();

function __construct($rightsID){

// () ,

$result=mysql_query( "SELECT `group_rights`.groupID FROM `group_rights` WHERE `group_rights`.rightsID = $rightsID" );



$ this ->groupID = array();

while ($t=mysql_fetch_assoc($result)){

// ID

$ this ->groupID[] = $t[ 'groupID' ];

}

mysql_free_result($result);

}

}



//

class ObjRights{

public $actions=array();

public $groupallow=array(),$groupdisallow=array();

/* SetAction



  

@param {string} ActionName -

@param {int} bitNumber -

@return {Object ObjRights} -

*/

public function SetAction($ActionName, $bitNumber){

//

if (array_search($bitNumber, $ this ->actions))

return false ;

//

if (isset($ this ->actions[$ActionName]))

return false ;

//

$clone = clone $ this ;

$clone->actions[$ActionName]=$bitNumber;



return $clone;

}



/* include_right



  

@param {int} RightsID -

@return {Object ObjRights} -

*/

public function include_right($RightsID){

$clone=clone $ this ;

$result=mysql_query( "SELECT * FROM `action_rights` WHERE `action_rights`.rightsID = $RightsID" );

while ($t=mysql_fetch_assoc($result)){

//

$clone->calculate_allow($t[ 'groupID' ],$t[ 'allow' ],$t[ 'disallow' ]);

}

mysql_free_result($result);



return $clone;

}



/* calculate_allow

,

  

@param {int} GroupID -

@param {string} AllowMask -

@param {string} DisallowMask -

*/

private function calculate_allow($GroupID, $AllowMask, $DisallowMask){

if (isset($ this ->groupallow[$GroupID])){

// - -

$len=min(strlen($ this ->groupallow[$GroupID]),strlen($AllowMask));

for ($i=0;$i<$len;$i++)

//Allow |= Mask

$ this ->groupallow[$GroupID]{$i}=chr(ord($ this ->groupallow[$GroupID]{$i})|ord($AllowMask{$i}));



// - -

$len=min(strlen($ this ->groupdisallow[$GroupID]),strlen($DisallowMask));

for ($i=0;$i<$len;$i++)

//Disallow |= Mask

$ this ->groupdisallow[$GroupID]{$i}=chr(ord($ this ->groupdisallow[$GroupID]{$i})|ord($DisallowMask{$i}));

} else {

$ this ->groupallow[$GroupID]=$AllowMask;

$ this ->groupdisallow[$GroupID]=$DisallowMask;

}

}

/* isAllow



  

@param {Object UsrRights} UserRights - ,

@param {string} ActionName -

@return {bool} - ?

*/

public function isAllow($UserRights, $ActionName){

// ?

if (!isset($ this ->actions[$ActionName]))

return false ;

//

$Actionbit=$ this ->actions[$ActionName];



foreach ($UserRights->groupID as $grpname){

//

if ($ this ->checkgrp($grpname,$Actionbit))

// ,

return true ;

}

return false ;

}



/* checkgrp



  

@param {int} GroupID -

@param {int} bit -

@return {bool} -

*/

private function checkgrp($GroupID, $bit){

// ?

if (isset($ this ->groupallow[$GroupID])){

// Allow & NOT Disallow & bit

if ((ord($ this ->groupallow[$GroupID]{$bit>>3})&(~ord($ this ->groupdisallow[$GroupID]{$bit>>3}))&(1<<($bit&7)))!=0){

return true ;

}

}

return false ;

}

}




* This source code was highlighted with Source Code Highlighter .




このライブラリの使用は簡単です。 最初に許可された権利をオブジェクトのクラスに分配し(上記の例)、次にユーザーを初期化(権利のグループに識別子を設定)し、オブジェクトで必要な権利を確認します:

//

$MessageRights = new ObjRights();

$MessageRights = $MessageRights->SetAction( 'message_view' ,0)->

SetAction( 'message_read' ,1)->

SetAction( 'message_edit' ,2)->

SetAction( 'message_delete' ,3)->

SetAction( 'message_create' ,4);

//...



// ()

$CurrentUserRights = new UseRights($CurrentUser->rightsID);



//...



// , .

$PageRights = $MessageRights->include_right($MainPage->rightsID);



//, ?

if ($PageRights->isAllow($CurrentUserRights, 'message_view' )){

//, . ?



//

foreach ($MainPage->Messages as $msg){

// (parent), (child)

$MsgRights = $PageRights->include_right($msg->rightsID);



//

if ($MsgRights->isAllow($CurrentUserRights, 'message_view' )){

// , ?

if ($MsgRights->isAllow($CurrentUserRights, 'message_edit' ))

$msg->editable_flag = 1;

// ?

if ($MsgRights->isAllow($CurrentUserRights, 'message_delete' ))

$msg->delete_flag = 1;



DrawMessage($msg);

}

}

}






その結果、最初の記事の例と比較して、データベースへのアクセスを数回加速し、呼び出しの回数を減らしました。



しかし、さらに高速化できますか?

はい、さらにいくつかのコード最適化を行うことができますが、可視性を失わないように(そして、あまりにも多くのコードが大量に山積みされています:))。

アルゴリズムを改善して、逆方向に進みます...



2番目の変換。 データベースから既製のPHPオブジェクトを取得します。



次に、ユーザートラフィックの多いWebサイトビルダーがコードを最適化する方法について説明します。

PHPの作成者は、既製のデータ構造(配列、オブジェクト)を保存および取得できる2つの関数を作成しました。

これらは、シリアル化関数と非シリアル化関数です。



そして、彼らはどのように私たちを助けますか?

これで、コード内で、権限を持つ(変更されない)各メッセージ(オブジェクト)を選択すると、rights_actionテーブルから追加の選択が行われます。 各オブジェクトについて-この選択は、変更する(オブジェクトに新しい権限を追加する)ことを決定するまで同じです。 オブジェクトの権利の1つの変更については、1000/100万以上の読み取り値があります。

必要な形式にいくつかの追加の変換が必要なのはなぜですか。 これは、各読み取りが行われる方法です。 したがって、これは1回のみ(権限の生成/変更時)に実行でき、完成した結果を保存できます。 結果を保存して復元するには、serialize / unserialize関数が役立ちます。



ライブラリを変更しましょう。
public function include_right($grp){

// (child)

$clone=clone $ this ;



// ,

$result=unserialize($grp);



// array('GroupID'=>array('allow_bits','disallow_bits'),'GroupID2'=>array('allow_bits','disallow_bits'),...)

foreach ($result as $groupID=>$allow)

// ,

$clone->include_action($groupID,$allow[0],$allow[1]);



return $clone;

}




より簡単で視覚的になりました!

確かに、この場合、(マスクごとに)権限を変更する問題があります。 このための関数を書きましょう:

public function export_object_rights(){

return serialize($this->selfrights);

}



public function allow_group($GroupID, $ActionName){

if (!isset($ this ->actions[$ActionName]))

return false ;

$bit=$ this ->actions[$ActionName];



if (isset($ this ->groupallow[$GroupID])){

$ this ->groupallow[$GroupID]{$bit>>3}|=(1<<($bit&8));

} else {

$ this ->groupallow[$GroupID]{$bit>>3}=(1<<($bit&8));

for ($i=0;$i<($bit>>3);$i++)

$ this ->groupallow[$GroupID]{$i}=chr(0);

}



if (isset($ this ->selfrights[$GroupID])){

$ this ->selfrights[$GroupID][0]{$bit>>3}|=(1<<($bit&8));

} else {

$ this ->selfrights[$GroupID]=array();

$ this ->selfrights[$GroupID][0]{$bit>>3}=(1<<($bit&8));

for ($i=0;$i<($bit>>3);$i++)

$ this ->selfrights[$GroupID][0]{$i}=chr(0);

$ this ->selfrights[$GroupID][1]= "" ;

}

}



public function reset_group($GroupID, $ActionName){

if (!isset($ this ->actions[$ActionName]))

return false ;

$bit=$ this ->actions[$ActionName];



if (isset($ this ->groupallow[$GroupID])){

$ this ->groupallow[$GroupID]{$bit>>3}&=255^(1<<($bit&8));

}



if (isset($ this ->groupdisallow[$GroupID])){

$ this ->groupdisallow[$GroupID]{$bit>>3}&=255^(1<<($bit&8));

}



if (isset($ this ->selfrights[$GroupID])){

$ this ->selfrights[$GroupID][0]{$bit>>3}&=255^(1<<($bit&8));

$ this ->selfrights[$GroupID][1]{$bit>>3}&=255^(1<<($bit&8));



$nul= true ;

for ($i=0;$i<strlen(selfrights[$GroupID][0]);$i++)

if (selfrights[$GroupID][0]{$i})

$nul= false ;



for ($i=0;$i<strlen(selfrights[$GroupID][1]);$i++)

if (selfrights[$GroupID][1]{$i})

$nul= false ;



if ($nul)

unset(selfrights[$GroupID]);

}

}



public function disallow_group($GroupID, $ActionName){

if (!isset($ this ->actions[$ActionName]))

return false ;

$bit=$ this ->actions[$ActionName];



if (isset($ this ->groupdisallow[$GroupID])){

$ this ->groupdisallow[$GroupID]{$bit>>3}|=(1<<($bit&8));

} else {

$ this ->groupdisallow[$GroupID]{$bit>>3}=(1<<($bit&8));

for ($i=0;$i<($bit>>3);$i++)

$ this ->groupallow[$GroupID]{$i}=chr(0);

}



if (isset($ this ->selfrights[$GroupID])){

$ this ->selfrights[$GroupID][1]{$bit>>3}|=(1<<($bit&8));

} else {

$ this ->selfrights[$GroupID]=array();

$ this ->selfrights[$GroupID][1]{$bit>>3}=(1<<($bit&8));

for ($i=0;$i<($bit>>3);$i++)

$ this ->selfrights[$GroupID][1]{$i}=chr(0);

$ this ->selfrights[$GroupID][0]= "" ;

}

}




* This source code was highlighted with Source Code Highlighter .






気味が悪いね。 私もこのコードをチェックしませんでした:)、私はただ怖かったです-すべてがとても気味が悪いです。 赤と白のこのコードは、アーキテクチャ内の何かが間違っていることを示しています。

私たちに余分な不要な尾を切り落とすことを提案します! コードを読みやすく、より汎用的にします。



エクストラテールモード!



構造は次のようになります。

array(/ * GroupID * / 1 => array(allow => 0b100101000100100010 / * bits * /、disallow => 0b100010010000010001010)、4 => ...)



この形式では、シリアル化が行われ、オブジェクトに書き込まれます。

しかし、最初に、アクションを操作するのが困難です(PHPファイルを編集して新しいアクションを追加する必要があります)。

第二に、ビットによる検索はそれほど簡単ではありません。 そして、前の記事で説明した元のバージョンに戻りましょう。



つまり、アクセス権は次のようになります。

array(/ * GroupID * / 1 => array(/ * Actions * / 'message_read' => 1 / * + * /、 'message_edit' => 0 / *-* /)、4 => ...)

このため:

1)単一のコマンド$ array [GroupID] [ActionName]で目的のアクションの検索が行われます。 (これにより、検索プロセスの速度が向上することを確認してください)

2)アクションを追加する必要はありません。 それらはすべて私たちの施設に保管されます。

3)選択は、ユーザーグループに対してのみ1サイクルで行われます。

4)ユーザーグループ自体が人間の形をとることができます。

明らかなマイナス点は、構造がより多くのスペースを占有することです。 悪い? はい。ただし、最適化では常にCPUの負荷またはメモリ量のいずれかです。 はい、データベースのメモリをあまり消費しませんので、始めましょう。

//

class ObjRights{

public $groups=array();

public $selfgroups=array();



/* */



/* include_right



  

@param {serialize array} RightsID -

@return {Object ObjRights} -

*/

public function include_right($RightsID){

$clone=clone $ this ;



// SQL-,

$clone->selfgroups = unserialize($RightsID);



foreach ($clone->selfgroups as $groupID=>$actions){

if (isset($clone->groups[$groupID])){

// ,

foreach ($actions as $actname=>$allow){

if (isset($clone->groups[$groupID][$actname]))

// 1 - allow, 0 - disallow

$clone->groups[$groupID][$actname]&=$allow;

else

$clone->groups[$groupID][$actname]=$allow;

}

} else

// ,

$clone->groups[$groupID]=$actions;



}



return $clone;

}



/* isAllow



  

@param {array} UserRights - ,

@param {string} ActionName -

@return {bool} - ?

*/

public function isAllow($UserRights, $ActionName){



foreach ($UserRights as $groupname){

//

if (isset ($ this ->groups[$groupname]) &&

isset ($ this ->groups[$groupname][$ActionName]) &&

$ this ->groups[$groupname][$ActionName])

return true ;

}

return false ;

}



/* export_rights

. (serialize array)

  

@return {string} - serialize array

*/

public function export_rights(){

return serialize($ this ->selfgroups);

}





/* */



/* allow



  

@param {int} GroupID -

@param {string} ActionName -

*/

public function allow($GroupID, $ActionName){

if (!isset($ this ->selfgroups[$GroupID]))

$ this ->selfgroups[$GroupID]=array();

$ this ->selfgroups[$GroupID][$ActionName] = 1;

}



/* disallow



  

@param {int} GroupID -

@param {string} ActionName -

*/

public function disallow($GroupID, $ActionName){

if (!isset($ this ->selfgroups[$GroupID]))

$ this ->selfgroups[$GroupID]=array();

$ this ->selfgroups[$GroupID][$ActionName] = 0;

}



/* reset



  

@param {int} GroupID -

@param {string} ActionName -

*/

public function reset($GroupID, $ActionName){

if (isset($ this ->selfgroups[$GroupID]) && isset($ this ->selfgroups[$GroupID][$ActionName]))

unset($ this ->selfgroups[$GroupID][$ActionName]);

if ($ this ->selfgroups[$GroupID] === array())

unset ($ this ->selfgroups[$GroupID]);

}

}




* This source code was highlighted with Source Code Highlighter .




データベースでは、これらのすべてのレコードはテーブルオブジェクトの個別のフィールドに直接格納されます。 つまり、このように:

ページ
pageID page_name page_rights

(直列化形式の権利)
メッセージ
messID pageID message_rights

(直列化形式の権利)
message_header message_text


さらに何ができますか?





JSONのファンの場合は、serialize / unserialize => json_encode / json_decodeの代わりに使用できます。理論的には、より速く動作し、データベース内のスペースを節約します。



より人道的なグループ名が好きな人には、すべてのGroupIDをintからstringに変更する必要はありません。



ユーザーがログインしている場合は、$ _SESSIONを使用して、すべての権利$ User-> current_user->権利(およびその他のユーザー情報)を記憶し、データベースにアクセスして不要なアクションを実行しないようにすることができます。 唯一のマイナスは、ユーザーがアクセス権を変更するには、ログインする必要があることです。



実施例



ビットマスク:

実施例

test0.php

rights0.php



シリアル化:

実施例

test.php

rights.php



あとがき



権利を扱うための非常によく似たメカニズムがZendによって提供されています(Serializeの最後の例)。

ここですでに基本を学びました(機能の仕組みと脚の成長元)。

Zend_Aclは、同様の対話記述ライブラリを提供します。 Zend_Aclの正確な使用方法については、 インターネットで読むことができます。 この記事の後、すぐに理解できると思います。



psこのトピックに関する最後の記事にコメントを残してくれた皆さんに感謝します。 あなたがいなければ、この記事はなかったでしょう。



All Articles