Revamped session handling, deprecating the old "persist or not" scheme and

switching to one where the application tells us how many days the session
should last. Added a mechanism for checking whether a session is still valid
and refreshing it if so.


git-svn-id: https://samskivert.googlecode.com/svn/trunk@1990 6335cc39-0255-0410-8fd6-9bcaacd3b74c
This commit is contained in:
mdb
2006-12-06 19:36:41 +00:00
parent 5e61ec0641
commit b1a1a31097
3 changed files with 161 additions and 185 deletions
@@ -37,13 +37,10 @@ public interface Authenticator
* repository against which the user-supplied data is to be checked.
* @param username the username supplied by the user.
* @param password the plaintext password supplied by the user.
* @param persist if true, the cookie will expire in one month, if
* false, the cookie will expire in 24 hours.
*
* @throws AuthenticationFailedException if the user failed to pass
* the authentication check.
*/
public void authenticateUser (
User user, String username, Password password, boolean persist)
public void authenticateUser (User user, String username, Password password)
throws AuthenticationFailedException;
}
@@ -36,9 +36,8 @@ import com.samskivert.util.StringUtil;
import com.samskivert.util.Tuple;
/**
* The user manager provides easy access to user objects for servlets. It
* takes care of cookie management involved in login, logout and loading a
* user record during an authenticated session.
* The user manager provides easy access to user objects for servlets. It takes care of cookie
* management involved in login, logout and loading a user record during an authenticated session.
*/
public class UserManager
{
@@ -51,16 +50,14 @@ public class UserManager
new PasswordAuthenticator();
/**
* A totally insecure authenticator that authenticates any user.
* <em>Note:</em> Applications that make use of this authenticator
* should make sure the user has already been authenticated through
* some other means.
* A totally insecure authenticator that authenticates any user. <em>Note:</em> Applications
* that make use of this authenticator should make sure the user has already been authenticated
* through some other means.
*/
public static class InsecureAuthenticator implements Authenticator
{
// documentation inherited
public void authenticateUser (
User user, String username, Password password, boolean persist)
public void authenticateUser (User user, String username, Password password)
throws InvalidPasswordException
{
// don't care
@@ -68,14 +65,13 @@ public class UserManager
}
/**
* An authenticator that requires that the user-supplied password
* match the actual user password.
* An authenticator that requires that the user-supplied password match the actual user
* password.
*/
public static class PasswordAuthenticator implements Authenticator
{
// documentation inherited
public void authenticateUser (
User user, String username, Password password, boolean persist)
public void authenticateUser (User user, String username, Password password)
throws AuthenticationFailedException
{
if (!user.passwordsMatch(password)) {
@@ -85,27 +81,25 @@ public class UserManager
}
/**
* Prepares this user manager it for operation. Presently the user manager
* requires the following configuration information:
* Prepares this user manager it for operation. Presently the user manager requires the
* following configuration information:
*
* <ul>
* <li><code>login_url</code>: Should be set to the URL to which to
* redirect a requester if they are required to login before accessing
* the requested page. For example:
* <li><code>login_url</code>: Should be set to the URL to which to redirect a requester if
* they are required to login before accessing the requested page. For example:
*
* <pre>
* login_url = /usermgmt/login.ajsp?return=%R
* </pre>
*
* The <code>%R</code> will be replaced with the URL encoded URL the
* user is currently requesting (complete with query parameters) so
* that the login code can redirect the user back to this request once
* they are authenticated.
* The <code>%R</code> will be replaced with the URL encoded URL the user is currently
* requesting (complete with query parameters) so that the login code can redirect the user
* back to this request once they are authenticated.
* </ul>
*
* @param config the user manager configuration properties.
* @param conprov the database connection provider that will be used
* to obtain a connection to the user database.
* @param conprov the database connection provider that will be used to obtain a connection to
* the user database.
*/
public void init (Properties config, ConnectionProvider conprov)
throws PersistenceException
@@ -119,8 +113,7 @@ public class UserManager
// fetch the login URL from the properties
_loginURL = config.getProperty("login_url");
if (_loginURL == null) {
Log.warning("No login_url supplied in user manager config. " +
"Authentication won't work.");
Log.warning("No login_url supplied in user manager config. Authentication won't work.");
}
// look up any override to our user auth cookie
@@ -159,12 +152,11 @@ public class UserManager
}
/**
* Fetches the necessary authentication information from the http
* request and loads the user identified by that information.
* Fetches the necessary authentication information from the http request and loads the user
* identified by that information.
*
* @return the user associated with the request or null if no user was
* associated with the request or if the authentication information is
* bogus.
* @return the user associated with the request or null if no user was associated with the
* request or if the authentication information is bogus.
*/
public User loadUser (HttpServletRequest req)
throws PersistenceException
@@ -182,11 +174,10 @@ public class UserManager
}
/**
* Fetches the necessary authentication information from the http
* request and loads the user identified by that information. If no
* user could be loaded (because the requester is not authenticated),
* a redirect exception will be thrown to redirect the user to the
* login page specified in the user manager configuration.
* Fetches the necessary authentication information from the http request and loads the user
* identified by that information. If no user could be loaded (because the requester is not
* authenticated), a redirect exception will be thrown to redirect the user to the login page
* specified in the user manager configuration.
*
* @return the user associated with the request.
*/
@@ -206,29 +197,25 @@ public class UserManager
}
/**
* Attempts to authenticate the requester and initiate an authenticated
* session for them. An authenticated session involves their receiving a
* cookie that proves them to be authenticated and an entry in the session
* database being created that maps their information to their userid. If
* this call completes, the session was established and the proper cookies
* were set in the supplied response object. If invalid authentication
* information is provided or some other error occurs, an exception will be
* thrown.
* Attempts to authenticate the requester and initiate an authenticated session for them. An
* authenticated session involves their receiving a cookie that proves them to be authenticated
* and an entry in the session database being created that maps their information to their
* userid. If this call completes, the session was established and the proper cookies were set
* in the supplied response object. If invalid authentication information is provided or some
* other error occurs, an exception will be thrown.
*
* @param username The username supplied by the user.
* @param password The password supplied by the user.
* @param persist If true, the cookie will expire in one month, if false,
* the cookie will expire at the end of the user's browser session.
* @param persist If true, the cookie will expire in one month, if false, the cookie will
* expire at the end of the user's browser session.
* @param req The request via which the login page was loaded.
* @param rsp The response in which the cookie is to be set.
* @param auth The authenticator used to check whether the user should be
* authenticated.
* @param auth The authenticator used to check whether the user should be authenticated.
*
* @return the user object of the authenticated user.
*/
public User login (String username, Password password, boolean persist,
HttpServletRequest req, HttpServletResponse rsp,
Authenticator auth)
HttpServletRequest req, HttpServletResponse rsp, Authenticator auth)
throws PersistenceException, AuthenticationFailedException
{
// load up the requested user
@@ -245,31 +232,29 @@ public class UserManager
}
// run the user through the authentication gamut
auth.authenticateUser(user, username, password, persist);
auth.authenticateUser(user, username, password);
// give them the necessary cookies and business
effectLogin(user, persist, req, rsp);
effectLogin(user, persist ? PERSIST_EXPIRE_DAYS : NON_PERSIST_EXPIRE_DAYS, req, rsp);
return user;
}
/**
* Attempts to authenticate the requester and initiate an authenticated
* session for them. A session token will be assigned to the user and
* returned along with the associated {@link User} record. It is assumed
* that the client will maintain the session token via its own means.
* Attempts to authenticate the requester and initiate an authenticated session for them. A
* session token will be assigned to the user and returned along with the associated {@link
* User} record. It is assumed that the client will maintain the session token via its own
* means.
*
* @param username The username supplied by the user.
* @param password The password supplied by the user.
* @param persist If true, the cookie will expire in one month, if false,
* the cookie will expire at the end of the user's browser session.
* @param auth The authenticator used to check whether the user should be
* authenticated.
* @param username the username supplied by the user.
* @param password the password supplied by the user.
* @param expires the number of days in which this session should expire.
* @param auth the authenticator used to check whether the user should be authenticated.
*
* @return the user object of the authenticated user.
*/
public Tuple<User,String> login (String username, Password password,
boolean persist, Authenticator auth)
public Tuple<User,String> login (
String username, Password password, int expires, Authenticator auth)
throws PersistenceException, AuthenticationFailedException
{
// load up the requested user
@@ -286,37 +271,33 @@ public class UserManager
}
// run the user through the authentication gamut
auth.authenticateUser(user, username, password, persist);
auth.authenticateUser(user, username, password);
// register a session for this user
String authcode = _repository.registerSession(user, persist);
String authcode = _repository.registerSession(user, expires);
return new Tuple<User,String>(user, authcode);
}
/**
* If a user is already known to be authenticated for one reason or
* other, this method can be used to give them the appropriate
* authentication cookies to effect their login.
* If a user is already known to be authenticated for one reason or other, this method can be
* used to give them the appropriate authentication cookies to effect their login.
*
* @param expires the number of days in which to expire the session cookie, 0 means expire at
* the end of the browser session.
*/
public void effectLogin (User user, boolean persist,
HttpServletRequest req, HttpServletResponse rsp)
public void effectLogin (
User user, int expires, HttpServletRequest req, HttpServletResponse rsp)
throws PersistenceException
{
// register a session for this user
String authcode = _repository.registerSession(user, persist);
// stick it into a cookie for their browsing convenience
String authcode = _repository.registerSession(user, Math.max(expires, 1));
Cookie acookie = new Cookie(_userAuthCookie, authcode);
// strip the hostname from the server and use that as the domain
// unless configured not to
if (!"false".equalsIgnoreCase(
_config.getProperty("auth_cookie.strip_hostname"))) {
// strip the hostname from the server and use that as the domain unless configured not to
if (!"false".equalsIgnoreCase(_config.getProperty("auth_cookie.strip_hostname"))) {
CookieUtil.widenDomain(req, acookie);
}
acookie.setPath("/");
// expire in one month if persistent, else at the end of the
// session
acookie.setMaxAge((persist) ? (30*24*60*60) : -1);
acookie.setMaxAge((expires > 0) ? (expires*24*60*60) : -1);
rsp.addCookie(acookie);
}
@@ -378,4 +359,10 @@ public class UserManager
/** Prune the session table every hour. */
protected static final long SESSION_PRUNE_INTERVAL = 60L * 60L * 1000L;
/** Indicates how long (in days) that a "persisting" session token should last. */
protected static final int PERSIST_EXPIRE_DAYS = 30;
/** Indicates how long (in days) that a "non-persisting" session token should last. */
protected static final int NON_PERSIST_EXPIRE_DAYS = 1;
}
@@ -47,24 +47,21 @@ import com.samskivert.util.Tuple;
import com.samskivert.servlet.SiteIdentifier;
/**
* Interfaces with the RDBMS in which the user information is stored. The
* user repository encapsulates the creating, loading and management of
* users and sessions.
* Interfaces with the RDBMS in which the user information is stored. The user repository
* encapsulates the creating, loading and management of users and sessions.
*/
public class UserRepository extends JORARepository
{
/**
* The database identifier used to obtain a connection from our
* connection provider. The value is <code>userdb</code> which you'll
* probably need to know to provide the proper configuration to your
* connection provider.
* The database identifier used to obtain a connection from our connection provider. The value
* is <code>userdb</code> which you'll probably need to know to provide the proper
* configuration to your connection provider.
*/
public static final String USER_REPOSITORY_IDENT = "userdb";
/**
* Creates the repository and opens the user database. The database
* identifier used to fetch our database connection is documented by
* {@link #USER_REPOSITORY_IDENT}.
* Creates the repository and opens the user database. The database identifier used to fetch
* our database connection is documented by {@link #USER_REPOSITORY_IDENT}.
*
* @param provider the database connection provider.
*/
@@ -81,16 +78,15 @@ public class UserRepository extends JORARepository
* @param password the password for the new user.
* @param realname the user's real name.
* @param email the user's email address.
* @param siteId the unique identifier of the site through which this
* account is being created. The resulting user will be tracked as
* originating from this site for accounting purposes ({@link
* SiteIdentifier#DEFAULT_SITE_ID} can be used by systems that don't
* desire to perform site tracking.
* @param siteId the unique identifier of the site through which this account is being
* created. The resulting user will be tracked as originating from this site for accounting
* purposes ({@link SiteIdentifier#DEFAULT_SITE_ID} can be used by systems that don't desire to
* perform site tracking.
*
* @return The userid of the newly created user.
*/
public int createUser (Username username, Password password,
String realname, String email, int siteId)
public int createUser (
Username username, Password password, String realname, String email, int siteId)
throws UserExistsException, PersistenceException
{
// create a new user object...
@@ -106,8 +102,7 @@ public class UserRepository extends JORARepository
/**
* Looks up a user by username.
*
* @return the user with the specified user id or null if no user with
* that id exists.
* @return the user with the specified user id or null if no user with that id exists.
*/
public User loadUser (String username)
throws PersistenceException
@@ -118,8 +113,7 @@ public class UserRepository extends JORARepository
/**
* Looks up a user by userid.
*
* @return the user with the specified user id or null if no user with
* that id exists.
* @return the user with the specified user id or null if no user with that id exists.
*/
public User loadUser (int userId)
throws PersistenceException
@@ -130,15 +124,14 @@ public class UserRepository extends JORARepository
/**
* Looks up a user by a session identifier.
*
* @return the user associated with the specified session or null of
* no session exists with the supplied identifier.
* @return the user associated with the specified session or null of no session exists with the
* supplied identifier.
*/
public User loadUserBySession (String sessionKey)
throws PersistenceException
{
User user = load(_utable, "sessions",
"where authcode = '" + sessionKey + "' AND " +
"sessions.userId = users.userId");
User user = load(_utable, "sessions", "where authcode = '" + sessionKey + "' " +
"AND sessions.userId = users.userId");
if (user != null) {
user.setDirtyMask(_utable.getFieldMask());
}
@@ -165,12 +158,10 @@ public class UserRepository extends JORARepository
}
/**
* Lookup a user by email address, something that is not efficient and
* should really only be done by site admins attempting to look up a
* user record.
* Lookup a user by email address, something that is not efficient and should really only be
* done by site admins attempting to look up a user record.
*
* @return the user with the specified user id or null if no user with
* that id exists.
* @return the user with the specified user id or null if no user with that id exists.
*/
public ArrayList<User> lookupUsersByEmail (String email)
throws PersistenceException
@@ -179,12 +170,11 @@ public class UserRepository extends JORARepository
}
/**
* Looks up a list of users that match an arbitrary query. Care should be
* taken in constructing these queries as the user table is likely to be
* large and a query that does not make use of indices could be very slow.
* Looks up a list of users that match an arbitrary query. Care should be taken in constructing
* these queries as the user table is likely to be large and a query that does not make use of
* indices could be very slow.
*
* @return the users matching the specified query or an empty list if there
* are no matches.
* @return the users matching the specified query or an empty list if there are no matches.
*/
public ArrayList<User> lookupUsersWhere (final String where)
throws PersistenceException
@@ -198,13 +188,12 @@ public class UserRepository extends JORARepository
}
/**
* Updates a user that was previously fetched from the repository.
* Only fields that have been modified since it was loaded will be
* written to the database and those fields will subsequently be
* marked clean once again.
* Updates a user that was previously fetched from the repository. Only fields that have been
* modified since it was loaded will be written to the database and those fields will
* subsequently be marked clean once again.
*
* @return true if the record was updated, false if the update was
* skipped because no fields in the user record were modified.
* @return true if the record was updated, false if the update was skipped because no fields in
* the user record were modified.
*/
public boolean updateUser (final User user)
throws PersistenceException
@@ -218,19 +207,16 @@ public class UserRepository extends JORARepository
}
/**
* 'Delete' the users account such that they can no longer access it,
* however we do not delete the record from the db. The name is
* changed such that the original name has XX=FOO if the name were FOO
* originally. If we have to lop off any of the name to get our
* prefix to fit we use a minus sign instead of a equals side. The
* password field is set to be the empty string so that no one can log
* in (since nothing hashes to the empty string. We also make sure
* their email address no longer works, so in case we don't ignore
* 'deleted' users when we do the sql to get emailaddresses for the mass
* mailings we still won't spam delete folk. We leave the emailaddress
* intact exect for the @ sign which gets turned to a #, so that we can
* see what their email was incase it was an accidently deletion and we
* have to verify through email.
* 'Delete' the users account such that they can no longer access it, however we do not delete
* the record from the db. The name is changed such that the original name has XX=FOO if the
* name were FOO originally. If we have to lop off any of the name to get our prefix to fit we
* use a minus sign instead of a equals side. The password field is set to be the empty string
* so that no one can log in (since nothing hashes to the empty string. We also make sure
* their email address no longer works, so in case we don't ignore 'deleted' users when we do
* the sql to get emailaddresses for the mass mailings we still won't spam delete folk. We
* leave the emailaddress intact exect for the @ sign which gets turned to a #, so that we can
* see what their email was incase it was an accidently deletion and we have to verify through
* email.
*/
public void deleteUser (final User user)
throws PersistenceException
@@ -259,8 +245,7 @@ public class UserRepository extends JORARepository
String oldName = user.username;
for (int ii = 0; ii < 100; ii++) {
try {
user.username = StringUtil.truncate(
ii + "=" + oldName, 24);
user.username = StringUtil.truncate(ii + "=" + oldName, 24);
_utable.update(conn, user, mask);
return null; // nothing to return
} catch (SQLException se) {
@@ -277,38 +262,27 @@ public class UserRepository extends JORARepository
}
/**
* Creates a new session for the specified user and returns the randomly
* generated session identifier for that session. If a session entry
* already exists for the specified user it will be reused.
* Creates a new session for the specified user and returns the randomly generated session
* identifier for that session. If a session entry already exists for the specified user it
* will be reused.
*
* <p> Temporary sessions are set to expire in two days (the assumption is
* that the browser will be given a non-persistent cookie and we should
* prune the session table entry after some reasonable amount of time),
* persistent sessions expire after one month.
* @param expireDays the number of days in which the session token should expire.
*/
public String registerSession (final User user, boolean persist)
public String registerSession (User user, int expireDays)
throws PersistenceException
{
// figure out when to expire the session
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, persist ? 30 : 2);
final Date expires = new Date(cal.getTime().getTime());
// look for an existing session for this user
Tuple<Integer,String> session = execute(
new Operation<Tuple<Integer,String>>() {
public Tuple<Integer,String> invoke (
Connection conn, DatabaseLiaison liaison)
final String query = "select sessionId, authcode from sessions " +
"where userId = " + user.userId;
Tuple<Integer,String> session = execute(new Operation<Tuple<Integer,String>>() {
public Tuple<Integer,String> invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
Statement stmt = conn.createStatement();
try {
String query = "select sessionId, authcode from sessions " +
"where userId = " + user.userId;
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
return new Tuple<Integer,String>(
rs.getInt(1), rs.getString(2));
return new Tuple<Integer,String>(rs.getInt(1), rs.getString(2));
}
return null;
} finally {
@@ -317,6 +291,11 @@ public class UserRepository extends JORARepository
}
});
// figure out when to expire the session
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, expireDays);
Date expires = new Date(cal.getTime().getTime());
// if we found one, update its expires time and reuse it
if (session != null) {
update("update sessions set expires = '" + expires + "' " +
@@ -326,11 +305,29 @@ public class UserRepository extends JORARepository
// otherwise create a new one and insert it into the table
String authcode = UserUtil.genAuthCode(user);
update("insert into sessions (authcode, userId, expires) values('" +
authcode + "', " + user.userId + ", '" + expires + "')");
update("insert into sessions (authcode, userId, expires) values('" + authcode + "', " +
user.userId + ", '" + expires + "')");
return authcode;
}
/**
* Validates that the supplied session key is still valid and if so, refreshes it for the
* specified number of days.
*
* @return true if the session was located and refreshed, false if it no longer exists.
*/
public boolean refreshSession (String sessionKey, int expireDays)
throws PersistenceException
{
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, expireDays);
Date expires = new Date(cal.getTime().getTime());
// attempt to update an existing session row, returning true if we found and updated it
return (update("update sessions set expires = '" + expires + "' " +
"where sessionId = " + sessionKey) == 1);
}
/**
* Prunes any expired sessions from the sessions table.
*/
@@ -341,9 +338,8 @@ public class UserRepository extends JORARepository
}
/**
* Loads the usernames of the users identified by the supplied user
* ids and returns them in an array. If any users do not exist, their
* slot in the array will contain a null.
* Loads the usernames of the users identified by the supplied user ids and returns them in an
* array. If any users do not exist, their slot in the array will contain a null.
*/
public String[] loadUserNames (int[] userIds)
throws PersistenceException
@@ -352,9 +348,8 @@ public class UserRepository extends JORARepository
}
/**
* Loads the real names of the users identified by the supplied user
* ids and returns them in an array. If any users do not exist, their
* slot in the array will contain a null.
* Loads the real names of the users identified by the supplied user ids and returns them in an
* array. If any users do not exist, their slot in the array will contain a null.
*/
public String[] loadRealNames (int[] userIds)
throws PersistenceException
@@ -397,12 +392,11 @@ public class UserRepository extends JORARepository
}
/**
* Configures the supplied user record with the provided information
* in preparation for inserting the record into the database for the
* first time.
* Configures the supplied user record with the provided information in preparation for
* inserting the record into the database for the first time.
*/
protected void populateUser (User user, Username name, Password pass,
String realname, String email, int siteId)
protected void populateUser (
User user, Username name, Password pass, String realname, String email, int siteId)
{
user.username = name.getUsername();
user.setPassword(pass);
@@ -413,8 +407,8 @@ public class UserRepository extends JORARepository
}
/**
* Inserts the supplied user record into the user database, assigning
* it a userid in the process, which is returned.
* Inserts the supplied user record into the user database, assigning it a userid in the
* process, which is returned.
*/
protected int insertUser (final User user)
throws UserExistsException, PersistenceException
@@ -444,8 +438,8 @@ public class UserRepository extends JORARepository
}
/**
* Loads up a user record that matches the specified where clause. Returns
* null if no record matches.
* Loads up a user record that matches the specified where clause. Returns null if no record
* matches.
*/
protected User loadUserWhere (String where)
throws PersistenceException
@@ -474,8 +468,7 @@ public class UserRepository extends JORARepository
{
Statement stmt = conn.createStatement();
try {
String query = "select userId, " + column +
" from users " +
String query = "select userId, " + column + " from users " +
"where userId in (" + ids + ")";
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
@@ -502,9 +495,8 @@ public class UserRepository extends JORARepository
}
/**
* Take the passed in int array and create the a string suitable for using
* in a SQL set query (I.e., "select foo, from bar where userid in
* (genIdString(userIds))"; )
* Take the passed in int array and create the a string suitable for using in a SQL set query
* (I.e., "select foo, from bar where userid in (genIdString(userIds))"; )
*/
protected String genIdString (int[] userIds)
{