The method that I use to accomplish this is pretty complex, (but has some really cool functionality and is pretty tough to break) and it requires creating four tables: session, user, groups, and user_groups. Before I start getting into the guts of all this let me say that i do not use native PHP sessions; I rolled my own session manager before PHP had built-in session support, and I've used my session manager (in one incarnation or another) ever since.
The logical place to start with any database-intensive application is with the database structure. So here goes...
My session table looks like this:
DROP TABLE IF EXISTS session;
CREATE TABLE session (
session_id char(32) NOT NULL default '',
host char(16) NOT NULL default '',
cookie enum('0','1') NOT NULL default '0',
user_id int(8) NOT NULL default '0',
updated int(12) unsigned NOT NULL default '0',
browser char(128) NOT NULL default '',
created int(12) NOT NULL default '0',
PRIMARY KEY (session_id),
KEY session_matrix (updated)
) TYPE=MyISAM;
The session_id is a random string from 8-32 characters and is always set PRIOR to 'log in'. I have a nifty function for generating the random string; perhaps we'll get to that later. Without getting to the dirty details of how I manage sessions, let's just say... I try to set a cookie, but since not every one has cookies enabled on their web browser, a session identifier may also be passed as hidden form field data, or URL-appended. (Which is why there's a cookie field in my session table...) The 'host' field corresponds to the $SERVER['REMOTE_ADDR'] global variable (client IP address), and I always check that and the browser ($SERVER['HTTP_USER_AGENT'], or web browser identification string) in addition to the session_id to help prevent people trying to 'hijack' sessions. Remember, the session_id must be passed with every request from client browser to the server. The user_id column represents the users unique integer-based identifier. I suppose you could use the user_name column, but I elect to use integer values since they can be compared more quickly...
My user table looks like this:
DROP TABLE IF EXISTS user;
CREATE TABLE user (
user_id int(8) unsigned NOT NULL auto_increment,
user_name char(32) NOT NULL default '',
password char(32) NOT NULL default '',
last_session char(32) NOT NULL default '',
first_name char(32) NOT NULL default '',
last_name char(32) NOT NULL default '',
address_1 char(64) NOT NULL default '',
address_2 char(64) NOT NULL default '',
city char(64) NOT NULL default '',
state char(48) NOT NULL default '',
postal_code char(14) NOT NULL default '',
country char(48) NOT NULL default '',
home_phone char(24) NOT NULL default '',
bus_phone char(24) NOT NULL default '',
cell_phone char(24) NOT NULL default '',
pager char(24) NOT NULL default '',
fax char(24) NOT NULL default '',
email char(64) NOT NULL default '',
taxpayer_id char(16) NOT NULL default '',
title char(64) NOT NULL default '',
department char(64) NOT NULL default '',
extension char(8) NOT NULL default '',
last_login int(12) unsigned NOT NULL default '0',
last_logout int(12) unsigned NOT NULL default '0',
user_created int(12) unsigned NOT NULL default '0',
PRIMARY KEY (user_id),
UNIQUE KEY user_matrix (user_name)
) TYPE=MyISAM;
Notice there are two unique columns: the user_name and the PRIMARY_KEY user_id. Like I said earlier, it's simply because I choose to use integer-based comparisons rather than string-based comparisons because they're faster. I use the md5() function to hash passwords rather than the MySQL PASSWORD() function because I'm working on PostgreSQL-compatible version of all this and I'd prefer not to have a lot of 'branching' code where database queries are made. So using md5() gives me better cross-database support at the end of the day. Your mileage may vary...
The next table is for 'groups'. Every user needs to be a member of at least 1 'group', so I can have content that can be accessed at a group level; such as 'Administrators' or 'Remote Users', etc... Here's the structure for the groups table that I use:
DROP TABLE IF EXISTS groups;
CREATE TABLE groups (
groups_id int(8) unsigned NOT NULL auto_increment,
groups_name char(32) NOT NULL default '',
groups_description char(128) NOT NULL default '',
groups_dir char(128) NOT NULL default '',
groups_created int(12) unsigned NOT NULL default '0',
PRIMARY KEY (groups_id),
UNIQUE KEY groups_matrix (groups_name)
) TYPE=MyISAM;
Like the users table, each group has a groups_id and groups_name which are UNIQUE. For the same reasons, speed when running queries.
And ultimately, there needs to be a table which performs the 'mapping' of users to groups. Hence, my user_groups table which allows 'users' to mapped to one or more 'groups':
DROP TABLE IF EXISTS user_groups;
CREATE TABLE user_groups (
user_id int(8) unsigned NOT NULL default '0',
groups_id int(8) unsigned NOT NULL default '0',
created int(12) unsigned NOT NULL default '0',
expires int(12) unsigned NOT NULL default '0',
warning int(12) unsigned NOT NULL default '0',
UNIQUE KEY user_groups_matrix (user_id,groups_id)
) TYPE=MyISAM;
Note that the UNIQUE key in this table consists of both the user_id and the groups_id, which ultimately allows any 'user' to be a member of any 'group'... Also, this provides for users membership in specific groups to expire.
If you've noticed that I never use any temporal data types (DATE, TIME, or DATETIME) columns, that's a good catch. Every date since Jan 1, 1970 can be measured in UNIX time (the number of seconds since Jan 1 1970), and since PHP provides infinite formatting utilities for dates, I've elected to use UNIX timestamps rather than temporal data types. They probably compare a bit faster than temporal data types, since conversion to a UNIX timestamp needs to occur within the database engine or PHP intertpreter before a comparison can be made.
Try and digest this much, and we'll take it from there...