Έλεγχος πρόσβασης με χρήση φίλτρων

Λοιπόν, πρόκεται να παρακάμψουμε το προεγκατεστημενο σύστημα ελέγχου πρόσβασης του Yii και να το αντικαταστήσουμε με ένα δικό μας. Για να το κάνουμε αυτό πρέπει να καταλάβουμε πρώτα κάποια βασικά πράγματα σχετικά με το πως λειτουργούν κάποια πράγματα στις εφαρμογές του Yii.

Εισαγωγή στα φίλτρα - Φίλτρα κλάσης: Και καταρχήν τα φίλτρα. Ένα φίλτρο μπορεί να είναι μια μέθοδος ή μια κλάση. Θα αναφερθώ και στις δύο περιπτώσεις γιατί ενώ εμείς θα χρησιμοποιήσουμε την δεύτερη, οι εφαρμογές του Yii χρησιμοποιούν εξ’ορισμού…και τις δύο! Ας ξεκινήσουμε από την περίπτωση της κλάσης. Στην περίπτωση αυτή ένα φίλτρο δεν είναι παρά μια κλάση η οποία επεκτείνει την κλάση CFilter. Η κλάση CFilter έχει (μεταξύ άλλων) δύο μεθόδους. Την preFilter() και την postFilter().


class CFilter extends CComponent implements IFilter

{

...

protected function preFilter($filterChain)

{

	return true;

}


protected function postFilter($filterChain)

{

}

...

}

Όταν προσθέσουμε ένα φίλτρο σε έναν Controller, τότε πριν την εκτέλεση ενός action (της αντίστοιχης μεθόδου δηλαδή) του Controller εκτελείται η μέθοδος preFilter και μετά την εκτέλεση του action εκτελείται η postFilter(). Αν η preFilter() επιστρέψει true τότε η εκτέλεση του action προχωρά κανονικά. Αν επιστρέψει false η εκτέλεση του action διακόπτεται. Η postFilter() τυπικά δεν επιστρέφει κάτι μιας και το action έχει ήδη εκτελεστεί πριν από αυτή και άρα δεν μπορεί να το διακόψει. Στα φίλτρα που ορίζουμε απλά κάνουμε override τις δύο αυτές μεθόδους. Στην περίπτωσή μας, όπου θέλουμε να ελέγξουμε την πρόσβαση, θα ασχοληθούμε μόνο με την preFilter().

Φίλτρα μεθόδου: Μπορούμε να ορίσουμε και το φίλτρο σαν μια μέθοδο του Controller. Στην περίπτωση αυτή, αν το φίλτρο μας ονομάζεται π.χ CheckCredentials , η μέθοδος που υλοποιεί το φίλτρο θα πρέπει να ονομάζεται filterCheckCredentials($filterChain). Αν το φίλτρο αποφάσιζε ότι το action πρέπει να εκτελεστεί τότε θα πρέπει μέσα από τη μέθοδο να εκτελεστεί η εντολή:


$filterChain->run()

Όπως υποψιάζεσται ο κώδικας της μεθόδου πριν την παραπάνω εντολή αποτελεί το αντίστοιχο της μεθόδου preFilter() και ο κώδικας της μεθόδου μετά την εντολή αυτή αποτελεί το αντίστοιχο της μεθόδου postFilter().

Το φίλτρο ‘accessControl’ του Yii : Εξ’ορισμού οι εφαρμογές του Yii χρησιμοποιούν ένα φίλτρο ονόματι accessControl για τον έλεγχο πρόσβασης. Το φίλτρο αυτό καλείται μέσω μιας μεθόδου με όνομα filterAccessControl(), η οποία είναι ορισμένη στην κλάση CController. Στην πράξη όμως η μέθοδος αυτή είναι ένα περιτύλιγμα που φορτώνει ένα αντικείμενο κλάσης CAccessControlFilter.


public function filterAccessControl($filterChain)

{

    $filter=new CAccessControlFilter;

    $filter->setRules($this->accessRules());

    $filter->filter($filterChain);

}

Η κλάση αυτή είναι ένα φίλτρο (επέκταση της CFilter με υπέρβαση των μεθόδων preFilter() και postFilter() ). Όπως βλέπετε από τον κώδικα της μεθόδου δημιουργείται ένα τέτοιο αντικείμενο, φορτώνονται οι κανόνες πρόσβασης που έχουμε ορίσει στην μέθοδο accessRules() του Controller και στην συνέχεια εκτελείται το φίλτρο.

Τοποθετώντας φίλτρα σε έναν Controller: Πως τοποθετούμε ένα φίλτρο σε έναν Controller? Με υπέρβαση της μεθόδου filters()! Φίλτρα μεθόδων και κλάσεων ορίζονται και ρυθμίζονται με τον ίδιο τρόπο με μια μικρή διαφορά στον τρόπο αναφοράς του ονοματός τους. Η προσθήκη του φίλτρου accessControl() και γενικά των φίλτρων μεθόδου γίνεται ως εξής:


public function filters()

    {

        return array(

            'accessControl',

        );

    }

Μπορούμε να ορίσουμε ότι ένα φίλτρο θέλουμε να εκτελείται σε συγκεκριμένα μόνο actions. Αυτό γίνεται με χρήση των τελεστών ‘+’ και ‘-’ οι οποίοι προσθέτουν, ή εξαιρούν αντίστοιχα, το φίλτρο σε ορισμένα actions:


public function filters()

    {

        return array(

            	'myfilter - login',

		'myfilter2 + edit,create',

        );

    }

Για τα φίλτρα κλάσης ο τρόπος διαφέρει λίγο. Δείτε τη διαφορά παρακάτω:


public function filters()

    {

        return array(

            'postOnly + edit, create',

            array(

                'application.filters.PerformanceFilter - edit, create', 

                'unit'=>'second',                                       

                'amount'=>42,                                           

            ),

        );

    }

Φορτώνουμε στον Controller ένα φίλτρο ονόματι PerformanceFilter του οποίου η κλάση βρίσκεται στον φάκελο “\protected\filters\” , ορίζουμε ότι το φίλτρο θα εκτελείται για όλα τα actions εκτός των edit και create και τοποθετούμε δύο properties στο φίλτρο (ένα με όνομα ‘unit’ και τιμή ‘second’ και ένα με όνομα ‘amount’ και τιμή ‘42’).

Τα φίλτρα εκτελούνται με την σειρά που ορίζονται στην μέθοδο filters() του Controller.

Ορίζοντας ένα νέο φίλτρο ελέγχου πρόσβασης: Ας ορίσουμε λοιπόν ένα φίλτρο με όνομα AuthorizationFilter. O βασικός του κώδικας θα είναι ο εξής:


<?php 

class AuthorizationFilter extends CFilter {

	

	protected function preFilter($filterChain){		

	}

	

	protected function postFilter($filterChain){

		return true;

	}

}

Η κλάση αυτή θα αποθηκευτεί σε ένα αρχείο AuthorizationFilter.php και θα τοποθετηθεί στον φάκελο "\protected\filters\". Το επόμενο βήμα είναι να αφαιρέσουμε από όλους τους Controller των οποίων θέλουμε να ελέγξουμε την πρόσβαση το φίλτρο accessControl και να τοποθετήσουμε το δικό μας φίλτρο.


public function filters()

	{

		return array(

			array(

				'application.filters.AuthorizationFilter',

			),

		);

	}

Η μέθοδος accessRules() είναι άχρηστη ποια και μπορεί να διαγραφεί!

Ας δούμε τώρα ένα παράδειγμα κώδικα που μπορούμε να χρησιμοποιήσουμε για τον έλεγχο πρόσβασης.


protected function preFilter($filterChain){

	$issLogged = (!Yii:app()->user->isGuest);

	$userAction = Yii::app()->Controller->getAction()->id;

	

	switch($userAction) {

		case 'index'  : return true; break;

		case 'create' : if ($isLogged) return true;

			Yii::app()->Controller->redirect(Yii::app()->user->loginUrl);

			break;

	}

	

	Yii::app()->Controller->redirect(Yii::app()->user->illegalActionURL);

	return false;

}

Ο κώδικας αυτός καταρχάς ελέγχει αν ο χρήστης είναι συνδεδεμένος και ποιά ενέργεια προσπαθεί να εκτελέσει. Κατόπιν, ανάλογα με το αν έχει δικαίωμα ή όχι μπορούμε να επιτρέψουμε την ενέργεια ή να φορτώσουμε κάποια άλλη σελίδα (π.χ ανακατεύθυνση στη σελίδα του login αν δεν είναι συνδεδεμένος ή ανακατεύθυνση σε μια προειδοποιητική σελίδα αν είναι συνδεδεμένος και δεν έχει δικαίωμα πρόσβασης).

Υ.Γ: Ελπίζω να δουλεύει γιατί το έγραψα λίγο βιαστηκα! :rolleyes:

Πολύ καλό writeup, thanks for sharing.

2 πραγματα με προβληματησαν

μήπως πρέπει να έχεις - αντί για + ;

[EDIT: Εν τέλει απλώς δεν διάβασα σωστά τον κώδικα. ολα ΟΚ. εντάξει, πρωί είναι, μην βαράτε]


protected function preFilter($filterChain){

	$issLogged = (!Yii:app()->user->isGuest);

	$userAction = Yii::app()->Controller->getAction()->id;

	

	switch($userAction) {

		case 'index'  : return true; break;

		case 'create' : if ($isLogged) return true;

			// Yii::app()->Controller->redirect(Yii::app()->user->loginUrl); // δεν είναι απαραίτητο αφού κάνεις redirect στο ίδιο URL αμέσως μετά

			break;

	}

	

	Yii::app()->Controller->redirect(Yii::app()->user->loginUrl);

	return false; // αυτο θα τρέξει; δεν θυμάμαι πως στήνει το Yii τα redirects, τελειώνει το action πριν την αναδρομολόγηση?


}

Να προσθέσω για να μην δημιουργηθεί απορία ότι: χρησιμοποιώντας τον τελεστή "+" το φίλτρο εκτελείται μόνο για τα αντίστοιχα actions που ορίζει ο τελεστής, ενώ χρησιμοποιώντας τον τελεστή "-" το φίλτρο εκτελείται για κάθε action εκτός αυτών που ορίζει ο τελεστής.

Σχετικά με την 2η απορία σου:

Ο λόγος που χρησιμοποίησα ένα redirection εδώ:


case 'create' : if ($isLogged) return true;

                        Yii::app()->Controller->redirect(Yii::app()->user->loginUrl);

                        break;

είναι γιατί είχα στο μυαλό μου ότι μπορεί σε περίπτωση που ο χρήστης δεν είναι logged να ανακατευθύνουμε σε διαφορετικό URL από αυτό που ανακατευθύνουμε όταν το φίλτρο θεωρεί ότι έχει ζητηθεί ένα illegal action (δεν βρίσκει το action μέσα στο switch). Bέβαια στον κώδικά μου από κεκτημένη ταχύτητα έβαλα ότι πάει στο loginURL και στις δύο περιπτώσεις…το διορθώνω ευθύς!

Το τελευταίο return δεν ξέρω αν θα τρέξει! Γι αυτό το έβαλα! Φύλαγε τα ρούχα σου… ;D

Αλέξανδρε έχω χρησιμοποιήσει ήδη τα φίλτρα για authorization και είναι όντως καλή ιδέα αλλά μπορεί να πάει και ένα βήμα πιο πέρα. Καλό το access control του yii δε λέω αλλά αν έχεις πολλούς χρήστες και ο καθένας θέλει το μακρύ και το κόντο του τότε θα αναγκαστείς να φτιάξεις μια δενδρική ιεραρχία από δυνατότητες πρόσβασης και να την κάνεις assign στους χρήστες.

Μιλάω για το RBAC (Role Based Access Control) και τη χρήση biz-rules σε αυτό. Είναι λίγο παίδεμα αλλά έχει πολλά ωφέλη αν ακολουθήσεις τη δομή που προτείνεται από το guide: Authentication and Authorization

Οπότε εν τέλει ο κώδικας μέσα σε ένα τέτοιο φίλτρο, θα εκμεταλλεύονταν την συνάρτηση checkAccess της CWebUser κλάσσης κάπως έτσι:




class CheckAccessControl extends CFilter {

	protected function preFilter($filterChain) {

		$var = 'island_id';

		$island_id = Yii::app()->user->island_id;

		if( Yii::app()->user->checkAccess('island_manager', compact($var)) ) {

			return true;

		}

		throw new CHttpException(403, Yii::t('', 'You do not have access to this island'));

		return false;	//we shall not reach this line of code

	}


	protected function postFilter($filterChain) {

		

	}

}



Κάπου σε έχασα…νομίζω! Η checkAccess() χρησιμοποιεί το CAuthManager component. Αν είναι να χρησιμοποιήσεις ένας τυπικό RBAC τότε γιατί να χρησιμοποιήσεις φίλτρο? Αυτό που λέω είναι ότι αν χρησιμοποιείς το CAuthManager, τότε αυτός ο έλεγχος:


if( Yii::app()->user->checkAccess('island_manager', compact($var)) ) 

μπορεί να γίνει κατευθείαν μέσα στα actions του Controller!

Ο σκοπός του φίλτρου που έχω ορίσει εδώ είναι η παράκαμψη του συστήματος ελέγχου πρόσβασης του Yii για την κατασκευή ενός customized συστήματος ελέγχου. Αυτό μπορεί να είναι και μια μορφή RBAC αν θες. Δε σε περιορίζει τίποτα.