You can either install the project from existing sources or
create it from scratch. If use the sources,
download the ZendBooksAuth.zip archive,
extract it into the desired location
and modify to suit your installation:
public/.htaccess reset RewriteBase if necessary
public/index.php replace '/usr/local/share/ZendLibrary' if necessary,
This modification of the Books application introduces some new features:
authentication requirement for modify/delete/create
AJAX calls using Dojo employed in action functions
The model portion uses the Books and Users class representing
the books and users tables, respectively.
In particular, this sample project assumes you have set
up Dojo according to the scheme described in
the Php AJAX + Dojo document.
Database Setup
This application assumes the common MySQL database
setup for testing:
host: localhost
dbname: test
username: guest
password: <empty>
The test database holds the
books table with the usual content.
Now however, there is an additional admins table used for authentication.
The table is very simple:
CREATE TABLE admins (
id int NOT NULL auto_increment,
username VARCHAR(50) NOT NULL,
password VARCHAR(50) NOT NULL,
PRIMARY KEY(id),
UNIQUE(username)
);
INSERT INTO admins (username,password) VALUES ('admin', SHA1('foobar'));
There is only one admin.
In order to setup the tables, change to the
ZendBooksAuth/db directory and run
mysql -u guest test < tables.sql
Check out the contents of the user table by running:
mysql -u guest test
mysql> select * from admins;
Authentication
Zend authentication means that the client established an
identity
An object of type Zend_Auth is employed to do the authentication test.
It is created by the statement:
$auth = Zend_Auth::getInstance();
An authenticated access is one which returns true for the
hasIdentity()
expression.
$auth->hasIdentity()
This identity is, by default, held as session-based information
and so is valid throughout the browser session unless negated.
In order to better control the identity for, say, different
classes of users, it is useful to specify the the storage
of the identity. This is done by:
Zend provides other storage mechanisms like database storage,
but we'll use session storage.
The $authname parameter makes
the "session space" a dedicated space separate from others.
We use the name "admins" to indicate our intention
of authenticating admin users, but the name could be anything.
The identity is negated very simply using the clearIdentity
function:
where
the function getAuthAdapter which is defined in
a "helper" class, My_Helper.php.
The class My_Helper.php corresponds, according to Zend Framework
logic, to the actual file
situated in the library folder:
library/My/Helper.php
<?php
class My_Helper {
public static function getAuthAdapter($username, $password, $table) {
$db = Zend_Registry::get('db');
$authAdapter = new Zend_Auth_Adapter_DbTable($db);
$authAdapter->setTableName($table);
$authAdapter->setIdentityColumn('username');
$authAdapter->setCredentialColumn('password');
$authAdapter->setCredentialTreatment('SHA1(?)');
$authAdapter->setIdentity($username);
$authAdapter->setCredential($password);
return $authAdapter;
}
public function getAuth($authname) {
$auth = Zend_Auth::getInstance();
$auth->setStorage(new Zend_Auth_Storage_Session($authname));
return $auth;
}
}
?>
Zend auto-loads this class by virtue of the statement we make in the
front controller (public/index.php):
$autoloader->registerNamespace("My_");
In this case, the "admins" parameter refers to
the admins table in the database. The
username and password information are
provide by a login form. Together the three parameters
are used to create the $authAdapter object.
Finally, $authAdapter is used in the authenticate
method:
$result = $auth->authenticate($authAdapter);
A successful authentication will make the following true:
$result->isValid()
and the identity is established, meaning that
all subsequent calls to
$auth->hasIdentity()
will return true.
Retrieving identity information
The function to use to obtain identity information is:
$auth->getIdentity()
Assuming only what we have done in the steps above, the
value of this will be the username for a successful
authentication. It is often useful to have other information
available as well, such as the id of the authenticated user.
In order to achieve this modification, we must do the following
exta steps after successful validation:
The getResultRowObject function,
used in this way, stores into
the identity the entire table row from the MySQL admins
table. The password field is null'd out for additional
security. Having done this, we can obtain both the
username and id of the authenticated user as member data:
The essence of the authentication used for Create/Update/Delete
invocations is to first check if the client has an identity
through the helper function:
private function isAuthenticated() {
$auth = My_Helper::getAuth($this->authname);
return $auth->hasIdentity();
}
The class variable $this->authname is "admins".
Every action which must be protected starts off with this line:
if (! $this->isAuthenticated()) { ...
If authentication fails,
then the login action is activated:
$this->_forward('login');
The function used is this:
public function loginAction() {
$this->view->navMenu = array($this->listing);
$this->view->authname = $this->authname;
$this->_forward('login','validate');
}
This action sets up the passing
of authname and defers to the login
action in
the validate controller:
It's the job of the validate controller to generate a login
form, establish the identity and reload the initial activation
so that we can take up "where we left off", now authenticated.
ValidateController and AJAX usage
The full code of ValidateController can be seen here:
( click to show )
The validate/login action is this:
public function loginAction() {
$this->view->headScript()->appendFile( "/dojolib/dojo/dojo.js" );
$baseUrl = $this->getRequest()->getBaseUrl();
$this->view->headScript()->appendFile( $baseUrl . "/js/validate.js" );
$this->view->baseUrl = $baseUrl;
}
function validate(baseUrl) {
dojo.xhrPost(
{
url: baseUrl + "/validate/check",
form: 'loginform',
load: function(response){
if (!response)
location.reload()
else {
alert(response)
dojo.byId('loginform').password.value = ""
}
},
error: function(response) {
alert("error" + response)
}
}
)
}
The validate function calls the validate/check action
through an AJAX call. The check action does all the hard work:
A successful validation will send no reponse data whatsoever and an unsuccessful
one will send information which can be posted for the user. This, then makes
sense of the Dojo AJAX call's load response.
Index Controller/View code
Regarding the index controller and its actions,
the scripts show.phtml, showcart.phtml,
create.phtml, and modify.phtml are identical to those
in
Zend Books. The changes
to the remaining files are discussed below.
One other novelty is the use of the automatically called
Zend function postDispatch.
This works like the opposite
of init it is called after each action before activating
the view script.
IndexController
The full code of IndexController can be seen here:
( click to show )
Here is a detailed outline of the modifications/additions
to IndexController in
Zend Books. Added member
functions and data appear bolded.
class IndexController extends Zend_Controller_Action {
private $authname = "admins";
public function init() { /* no change */ }
public function indexAction() {
// the cart session space name differs
// the dojo file is included
$this->view->headScript()->appendFile( "/dojolib/dojo/dojo.js" );
}
public function showAction() { /* no change */ }
public function showcartAction() { /* no change */ }
public function addcartAction() { /* no change */ }
public function clearcartAction() { /* no change */ }
public function isAuthenticated() {
$auth = My_Helper::getAuth($this->authname);
return $auth->hasIdentity();
}
public function loginAction() {
$this->view->navMenu = array($this->listing);
$this->view->authname = $this->authname; // send to view
$this->_forward('login','validate');
}
public function logoutAction() {
$auth = My_Helper::getAuth($this->authname);
$auth->clearIdentity();
$this->_helper->redirector("index");
}
// append "Logout" choice to menu once "logged in"
public function postDispatch() {
if ($this->isAuthenticated()) {
$auth = My_Helper::getAuth($this->authname);
$username = $auth->getIdentity()->username;
$logout = array("label"=>"Logout ($username)",
"url" => array("action" => "logout"));
$this->view->navMenu =
array_merge($this->view->navMenu,array($logout));
}
}
public function modifyAction() {
if (! $this->isAuthenticated()) {
$this->view->navMenu = array($this->listing);
$this->_forward('login','validate');
return;
}
// the rest is the same as before
}
public function updateAction() {
if (! $this->isAuthenticated()) {
return;
}
// the rest is the same as before
}
private function check($book) { /* same as before */ }
public function createAction() {
if (! $this->isAuthenticated()) {
$this->view->navMenu = array($this->listing);
$this->_forward('login','validate');
return;
}
$this->view->pageTitle = "Create";
}
public function addAction() {
if (! $this->isAuthenticated()) {
return;
}
// the rest is the same as before
}
public function checkauthAction() { // AJAX-based action
$this->_helper->layout->disableLayout();
$this->_helper->viewRenderer->setNoRender();
if (! $this->isAuthenticated()) {
$this->_response->setBody("Failed Access");
}
}
public function deleteAction() { // this is new
if (! $this->isAuthenticated()) {
$this->view->navMenu = array($this->listing);
$this->_forward('login');
return;
}
$this->_helper->redirector('index');
}
public function delAction() {
if (! $this->isAuthenticated()) {
return;
}
// the rest is the same as before
}
}
The JavaScript del function has
also been modified significantly from the previous
ZendBooks application.
It now makes an AJAX call to the /index/checkauth. If the
authentication fails, it calls /index/delete which is simply
a vehicle to call /index/login to authenticate and return
to /index/index upon success.
Once authentication has been completed, delete will act as it did
before after successfully completing the /index/checkauth
test.
public/js/del.js
function del(theForm,baseUrl) {
dojo.xhrGet(
{
url: baseUrl + "/index/checkauth",
load: function(response){
if (response) {
location.replace(baseUrl+"/index/delete")
} else {
if (confirm("Are you really, really sure ?")) {
theForm.submit();
}
}
},
error: function(response) {
alert("error" + response)
}
}
)
}
Observe how the actions addAction, updateAction, and delAction
which actually do the changes are protected even though
they are not called directly. This is a security
measure done in order
to prevent them from being activated "outside" the
intended activation procedure.
index view
Regarding the view functions, the only real change is
to the index/index activation script in that
the del function is called differently: