The code listings to follow have each line numbered for easy
reference. If you'd like to download these files and try
running them yourself, use the following perl one-liner to
remove the numbers from the start of each line:
perl -p -i.orig -e 's/^=\s+\d+\s+= ?//' simple.pl
In this particular case, 'simple.pl' will be edited in place (line numbers removed),
and the original file backed up as 'simple.pl.orig'.
The file 'simple.pl' (Listing 1) is the application script.
Line 3 tells perl where to look for my custom module files. Line 4
loads the module that actually defines the run modes (pages)
for my application, and line 12 starts it running. Lines 7-10
set some instance-specific run parameters. In this case, we're
defining 'simple.ini' to be our configuration file (Listing 2).
It's basically just name = value pairs. Using these will help us avoid hard-coding
anything in our scripts that may need to be changed. For now,
all that's in here is info needed for establishing a connection
to the database.
Normally, you would create a web app by inheriting from class CGI::Application.
In this case, class MyLib::Simple actually inherits
from MyLib::Login. MyLib::Login inherits from CGI::Application.
This lets us put all of the login/logout related functions into
a seperate module. Plus, if we later develop a second web app (MyLib::Simple2, for
example) it can easily reuse all of the same login code. Imagine having to maintain
10 different web apps. Let's say you want to switch databases from MySql to Postgres.
This approach allows you to modify one single Login.pm module, rather than
10 seperate app-specific modules. See? Code reuse is good!
Since the first thing Simple.pm does is load Login.pm (line 22), let's start
with Login.pm (Listing 4).
We'll come back to Listing 3 later.
Line 97 loads CGI::Application. Lines 99-104 load all of the plugin modules
we want to use. Here's a quick overview of what each one does:
'AutoRunmode' will allow us to use shorter URLs to refer to our web pages.
For example, instead of this:
http://localhost/cgi-bin/WebApp/simple.pl?rm=index
we can instead use this:
http://localhost/cgi-bin/WebApp/simple.pl/index
In other words, it will allow us to extract the desired run mode from
the PATH_INFO environment variable. It also lets us define run mode methods
using attributes, like this:
sub mypage : Runmode {
Otherwise, we'd have to use the runmodes() method to register each page we want to define.
This would normally be done in the setup method, but since we're using AutoRunmode, our
setup method (lines 107-114) is pretty short.
The DBH plugin gives us easy access to perl's DBI module (database interface).
The Session plugin is a wrapper around CGI::Session. This will help us maintain
state from one page view to the next (in other words, it helps us provide
persistent data).
The Authentication module provides methods for logging in and out. Let's say we
have 50 people browsing our site at the same time, and each one has placed items
in their shopping cart. Authentication allows us to identify individual users.
Otherwise, there would be no way to determine which cart belongs to which user.
The ConfigAuto module helps us read parameters from our config file.
Line 105 loads the MD5 module, which we'll use later to encrypt passwords.
Lines 107-114 define our setup method. The most important thing here is line 111,
which tells CGI::Application to parse the PATH_INFO environment variable.
The AutoRunmode plugin needs this to work properly.
Line 116 begins our cgiapp_init method. As the name implies, this is where we
do most of our 'initialization'. Line 119 uses the cfg method from the ConfigAuto
plugin to read our config file and store all our name-value pairs into a hash
called %CFG.
Line 121 tells CGI::Application where to look for template files (HTML::Template
will need to know this later on).
Lines 124-128 initialize our database connection. The dbh_config method is defined
by the DBH plugin. Rather than hard-code the necessary parameters, we're giving
it the data we read from our config file. Note that this lets us avoid hard coding
our mysql password inside our script. If we need to run our web app on several
different servers, we might have a different database on each. In this case,
we'd have a different config file for each server, but the perl code would be the same
for all of them.
Lines 130-142 initialize the session configuration. There are lots of different
ways to do this. You could, for example, choose to save session data to a file
instead of to a database. Or you could choose to use Data::Dumper to serialize
your data instead of Storable. Or, you could use Postgres instead of MySql.
But in this case, I've decided to use MySql, so I specify the 'mysql' driver.
Storable is a good serializer because it's fast and uses compression, but
the result is not humanly readable like Data::Dumper. If you run mysql and
type 'select * from sessions' you'll see lots of strange characters!
So Data::Dumper might be better if you're trying to debug problems with
your database, but the two formats are incompatible, so it's not easy to
switch back and forth between the two (if you want to swap methods, I'd
recommend dropping the 'sessions' table and recreating it each time).
In Line 133, self->query will return the current CGI object, and $self->dbh
will return the current database handle (the one we just configured in lines 124-128).
Line 136 defines the default expiration time for our session. This means
that once someone logs in, they will automatically be logged out after 1 hour
of inactivity.
Each session created will be assigned a unique 32 character id code.
This id will be written into a browser cookie. Lines 137-141 show
how to configure cookie-related parameters. I've commented these out,
because I prefer to use the defaults.
Lines 145-160 configure parameters for the Authentication plugin.
In line 146, we specify the DBI driver because we're using a database.
Line 147 gives the Authentication plugin a copy of our database handle.
Line 148 gives it the name of the table to use for finding usernames and
passwords. Lines 150 and 151 define which columns to use within that table.
Line 151 also specifies that passwords will be encrypted using MD5.
Line 155 says that we'll save our login state inside a session.
If a user is not logged in, but tries to access a protected page,
the Authentication plugin will automatically redirect the user to the
login page. Once the user enters a valid username and passsword, they
get redirected back to the protected page they originally requested.
If a session expires, they get automatically logged out. For this to
work properly, the Authentication plugin has to know which methods
to call to perform the login/logout functions. These are defined
in lines 156-157.
For added security, I wanted to use SSL to prevent a password entered
on the login page from being transmitted over the internet as plain text.
So I make sure the login page is accessed using https, rather than http
(more on this later).
But https is generally slower than http, so once logged in, I wanted
to switch back to regular http. There's no easy way to do this!
My work-around is to introduce a post-login run mode (line 158).
This is a run mode that gets called after a user successfully logs in.
What does this special run mode do? It basically figures out which
run mode (page) the user really wanted to see, then redirects the browser
to that page using http (not https).
Line 159 specifies a subroutine
to use to generate a login form. Note that the Authentication plugin
comes with a default form that you can use. I'm including this one
just to demonstrate how to go about creating one of your own, in case
you really want to. The default one actually looks much better than
mine, so you might wish to comment out lines 157-159 in order to see it!
The best solution is to write a custom login form of your own, that looks
the way you want. The one I present here is intended only to demonstrate
the basic functionality.
Lines 163-165 define which runmodes require a successful login.
The Login.pm module doesn't define any content - all of the actual
web pages are in Simple.pm. So I define the 'mustlogin' page
here as a kind of place-holder. It's a dummy page that forces you
to login, but immediately redirects you back to the default start page
(usually the index page). The mustlogin runmode is defined in lines 174-178.
The teardown method (lines 169-172) is used to close the database connection.
The 'okay' method (lines 180-192) exists only to switch from https back to http.
It assumes that the target run mode is stored in a cgi parameter named 'destination',
but if for some reason this is not the case, it will default back to the index page.
The login method (lines 194-213) basically just displays the login form (line 211).
But first, it checks to make sure you're not already logged in (lines 198-204),
and second, it makes sure you're connecting with https. If you try to
access the login page with http, it will automatically redirect you using https.
The login form is generated by my_login_form (lines 215-238). Actually,
most of the form is pregenerated in the form of a template
(see Listing 7).
Line 217 loads this template, lines 234-236 insert values into the
template parameters, and line 237 generates the final HTML. The 'destination'
parameter is important, because it contains the URL of the page to go to
once the user has successfully logged in. This gets tricky, because there
are two ways to log in. On the index page, there's a link which says
"click here to log in". If you click that link, and log in successfully,
you'll be taken back to the index page. If you try to access a protected page
before logging in, you'll automatically get redirected to the login page, but it
will use the 'destination' parameter to remember where you were trying to go,
and take you there once you do login.
So, my_login_form first tries to get a value for 'destination' from the CGI
query object (in case it was passed as a hidden variable). If that fails,
it tries looking at the PATH_INFO environment variable (in case it's being
passed as part of the URL). If all else fails, it defaults to the index page.
In the event the login attempt fails, you get redirected back to the login
form, where it asks you to try again.
Adding SSL into this process gets tricky. The login method will ensure
that the login page has a URL like this:
https://localhost/cgi-bin/WebApp/simple.pl/login
But for the username/password data to be encrypted, it's important that the
page that this form gets submitted to also have an https in the URL.
So if you look at line 341 (inside the login_form.html template) you'll
see the "action" part of the form tag is set to point to the 'mustlogin'
method. But once you DO successfully login, the Authentication plugin
is going to run the post-login run mode 'okay', which redirects us back
to the indended destination, minus the https URL.
Note that depending on which browser you're using, and what security
settings you have enabled, you may see popups asking you to accept
a security certificate, or that you're entering/leaving a secure area, etc.
These are all normal and can be safely ignored.
Lines 240-247 define the logout method. Note that logging out actually
deletes the current session (see line 244).
Lines 249-257 define the default error run mode: if any run mode fails
to eval, CGI::Application will redirect you to this run mode. It gives
you a nice way to trap errors in a sane way.
Lines 259-268 define the AUTOLOAD method. This will get called if you
try to access a non-existant run mode. Having this in place gives
a user a nice error message if they accidentally type in a URL wrong.
Now we're ready to return to Listing 3.
Since MyLib::Simple inherits
from MyLib::Login, it has access to all the methods we've just discussed,
in addition to all the methods defined by CGI::Application.
Lines 24-35 define cgiapp_init. Line 27 calls the cgiapp_init method
we saw before, in Login.pm. The "SUPER::" tag means "call this method
from my parent class". We need to do this because we need the database
connection, session config, etc. like before - but we also want to
do some additional, application specific initialization. For example,
Simple.pm defines two private run modes (pages that require a user
to login in order to view). We specify those in lines 30-33. But without
line 27, the cgiapp_init in Simple.pm would over-ride the one in Login.pm,
and be run INSTEAD of that one, not IN ADDITION to that one, like we want.
So just remember, every web app we create can inherit from Login.pm to
get the same login functionality, and each will have its own cgiapp_init
to do "local initialization", but each will need to call SUPER::cgiapp_init
to do Login.pm's "global initialization" stuff (database connect, session config,
etc.). The alternative is to copy & paste the code each time, but then if you
ever want to make a change, you'll have to change it in each copy. This
way you can simply change it in one place and globally effect every application
that depends on it (for example, if you decide you'd rather use SHA1 instead
of MD5 - just change Login.pm).
Lines 37-46 define our index page. Notice that it has the attribute "StartRunmode"
instead of "Runmode". That means it will be the page loaded if no other page
is specified. So these three URLs are all equivalent:
http://localhost/cgi-bin/WebApp/simple.pl?rm=index
http://localhost/cgi-bin/WebApp/simple.pl/index
http://localhost/cgi-bin/WebApp/simple.pl
The first URL explicitly sets the runmode to 'index', the second URL
sets the run mode to 'index' via the PATH_INFO environment variable,
and the third URL defaults to 'index' as the start run mode because no
other mode was specified.
So what does the index run mode do? It loads the index.html template
(Listing 5, line 39),
populates a few template parameters (lines 41-43) - note that you can
define several values inside a single param statement - then returns
the final HTML for rendering (line 45).
Note that we call $self->authen->username. If someone is logged in, this
will return their username. Otherwise, it will return null. We can test
the USER value inside the template to display different messages accordingly
(lines 292-297). So a user that is NOT logged in will see a "click here
to login" message, and a user that IS logged in will see a "click here
to log out" message.
Finally, the index page displays links to other pages: two public, and
two private. All of these pages use the same default.html template
(Listing 6), but the
parameters like NAME and MESSAGE vary for each one.
If you'd like to test the "error run mode", uncomment line 66 then
try clicking the link for "public2". The "die" statement will
generate an error message, and the error run mode will trap it.
To test the "autorunmode", try asking for a nonexistant page, like this:
http://localhost/cgi-bin/WebApp/simple.pl/bogus
You should be able to visit pages public and punlic2 without logging in.
If you click on the link for private or private2, you'll get redirected to
the login page (URL will switch to https), once you submit your username
and password you'll get redirected to the private page (the URL will
go back to http). If you click on "login" first, you'll be taken back
to the index page. The, if you click on a link to a private page, it will
take you there directly. If you do nothing for 1 hour, then try to
again access a private page, it will ask you to login again (because
the session will have expired).
Note that the templates contain a < META > tag with an expiration date
several years old. This is a hack to trick the browser into NOT caching
the page. Otherwise, you might still be able to see a private page after
logging out, because the browser will pull it from the local page cache.
If you force the page to reload, you should be prompted to login again.
Note that if this were a real application, the Login.pm module would
include a "register" run mode to add new users, a "reset" method
to let users change their password, a "forgot" method in case someone
can't recall their password, etc.
I'll leave that as an exercise for the reader. The tricky part is
logging in and out and getting the SSL to work, and hopefully I've
been able to help with that.
corrected typo in perl one-liner. Also corrected typo regarding default login form.