Simple Database Login
From EsWiki
Any ElectroServer4 application that wants to persist user data will need to authenticate username and password during login in some fashion. This tutorial gives one way to do this that is suitable for a total userbase of a few thousand, even if hundreds of them are attempting to log on during a short time.
Contents |
Source Code
Full source code and an extension ready to just drop into your ES4 is available in SimpleDBLoginZip.zip.
Overview
We will use a Derby database that is in the ES4's classpath (in server/db/UserInfo but this is easily customizable to any folder on the ES4's hard drive that the ES4 has write access to). This database will have a table that holds all usernames and passwords. When the ES4 starts, the database will be queried and all the usernames and passwords will be cached in a ConcurrentHashMap in a managed object. When a user logs in the LoginEventHandler will check the username and password from the LoginRequest. If the username is found in the cached map, and the password matches, the login is approved. If the username is found but the password doesn't match, the login fails. If the username is not found, it must be a new user, so the login is approved and the username and password are added to a LinkedBlockingQueue in the managed object which persists the data to the table in the database as time permits.
Classes needed:
- Controller, which extends Thread and is the Java Object that is the managed object
- ControllerFactory which is the ManagedObjectFactory that initializes an instance of Controller and allows plugins to acquire that instance
- UserObject, which is a Java Object that contains a username and password.
- SimpleDBLoginHandler, which is our LoginEventHandler
We will also need multiple jar files for the lib folder, a database.properties file, and some files for the SQL. The SQL can be hard coded instead of read from a file but reading the SQL from files is usually easier to maintain.
UserObject
Create a Java Object that will hold a username and password.
database.properties
Create a text file named "database.properties" that will go in the config file of your extension. Note: this is in the deployed extension, not in server/config. It will need lines similar to these:
database.driverClassName=org.apache.derby.jdbc.EmbeddedDriver database.url=jdbc:derby:./db/UserInfo;create=true database.maxActive=1 database.maxIdle=1 database.maxWait=5000
This specifies that the database is embedded into ES4, so it's not a separate service, that it is found in server/db/UserInfo, and if it isn't found there to create it.
Controller
This is the most complicated class of this example. The managed object that is used by the LoginEventHandler is an instance of this class, and it will handle all the database manipulation. It exposes methods so any plugin in the extension that needs database access can do so. The full source code is in the zip file. If your project requires additional database accesses, you can either add exposed methods to this class or add a method similar to initUserMap to your plugin, after getting a local copy of the dbi instance from Controller.
Notice that this class extends Thread. This allows it to process the LinkedBlockingQueue that is the list of usernames and passwords to be persisted to the database, as time permits. Doing it this way means that a new user's login is not held up waiting for the database access to finish, and that if a lot of users register in a short time you won't have all your ES4's threads tied up waiting for database accesses to finish. The drawback is that if the ES4 is stopped while there are new users in the queue, those new users will not be persisted, however since this would be very soon after the new users logged in, it will be understandable that a few seconds worth of transactions right before a crash might be lost.
Notice also that the SQL queries are being read from files. These SQL files are simple text files with SQL commands in them, stored in the extension's config/sql folder. The SQL is executed using JDBI, binding the parameters, which protects the database from SQL injection attacks. For example, if we simply built an SQL string and executed it, a new user could enter a password that included a semicolon and more SQL code to do all sorts of nasty things to the database.
ControllerFactory
The ControllerFactory class implements ManagedObjectFactory and ManagedObjectFactoryLifeCycle (or just extends BaseManagedObjectFactory). In the init method, load the database.properties file, then create an instance of Controller. See the zip file for full source code.
SimpleDBLoginHandler
SimpleDBLoginHandler extends BaseLoginEventHandler. In the init method, it acquires the managed object.
@Override
public void init(EsObjectRO esObjectRO) {
controller = (Controller) getApi().acquireManagedObject("ControllerFactory", null);
}
The only other method needed is executeLogin:
@Override
public ChainAction executeLogin(final LoginContext context) {
String userName = context.getUserName();
String password = context.getPassword();
EsObject response = context.getResponseParameters();
if (controller.doesUsernameExist(userName)) {
if (!controller.doesPasswordMatch(userName, password)) {
response.setString("err", "PasswordDoesNotMatch");
logger.debug("{} failed login (password did not match)", userName);
return ChainAction.Fail;
} else {
// successful login!
}
} else {
// TODO: optional, add checks here for valid userName, password
controller.registerNewUser(userName, password);
}
logger.debug("{} logged in successfully", userName);
return ChainAction.OkAndContinue;
}
Extension.xml
Extension.xml will need to specify the ControllerFactory and the LoginEventHandler.
<?xml version="1.0" encoding="utf-8" ?>
<Extension>
<Name>SimpleDBLogin</Name>
<ManagedObjects>
<ManagedObject>
<Handle>ControllerFactory</Handle>
<Type>Java</Type>
<Path>com.electrotank.examples.simpledblogin.ControllerFactory</Path>
</ManagedObject>
</ManagedObjects>
<EventHandlers>
<LoginHandlers>
<LoginHandler>
<Handle>LoginEventHandler</Handle>
<Type>Java</Type>
<Path>com.electrotank.examples.simpledblogin.SimpleDBLoginHandler</Path>
</LoginHandler>
</LoginHandlers>
</EventHandlers>
</Extension>
Deployment
You will find a copy of the extension in SimpleDBLoginZip.zip in SimpleDBLoginZip/server/dist/ext. Simply drop the entire SimpleDBLogin folder into server/extensions. If you make changes to the Java source, you will need to replace SimpleDBLoginZip/server/dist/ext/SimpleDBLogin/lib/SimpleDBLogin.jar. If you make changes to any of the .sql files or to database.properties files those will need to be updated in the server/extensions deployment as well.
Reboot ES4 and then log on to the web admin interface. On the Extensions tab, click the + next to the name of the extension, then add a server level component that is the LoginEventHandler. Reboot ES4 again and you are ready to test.
Testing
Any client that logs in using both a username and password will work. The function that sends the LoginRequest will be similar to this:
public function onConnectionEvent(e:ConnectionEvent):void {
if (e.getAccepted()) {
log("Connection accepted");
var name:String = ...get the name from the UI...
var pass:String = ...get the password from the UI...
lr.setUserName(name);
lr.setPassword(pass);
//send it
es.send(lr);
} else {
log("Connection failed: "+e.getEsError().getDescription());
}
}
