Custom login with database

This recipe discusses the common scenario in which you need to validate the client credentials against the user profiles stored in a database. We will also show how to execute more custom logic right after the login, for example setting User Variables and joining a Room from server-side.

We use MySQL for this recipe, but you can easily use any other database of your choice (read this tutorial). We use standard SQL in all code so it can be ported to any other RDBMS.

Implementing a custom login on the server side is a simple process. SFS2X fires the following two login events:

  • USER_LOGIN: fired when a client requests to join a Zone. Here you can validate the client credentials and decide if the user can continue the login process. At this stage the client is represented as a Session object, not as an SFSUser yet.
  • USER_JOIN_ZONE: notified when a client has successfully joined a Zone (and it was turned into an SFSUser).

The example code linked at the bottom of this article comes with a MySQL dump that can be used to generate the database table used by the Extension. These are the steps to setup and test the example application.

  • We need a database called sfs2x, if you don’t have one please create it.
  • Use the provided muppets-table.sql file to import the ‘muppets’ table into the database. This contains a few login accounts that you can use to test the login.
  • Deploy the Extension by copying it in a subfolder of {sfs-install-dir}/SFS2X/extensions/. You can choose any name for the subfolder, for example muppetsExt.
  • Launch the AdminTool, choose the Zone Configurator module and edit the BasicExamples zone. Set the Extension for the Zone as follows:
    • Name: muppetsExt (or the name you have chosen, if different)
    • Type: JAVA
    • Main class: sfs2x.extension.test.dblogin.DBLogin
  • In the General tab of the Zone Configurator make sure to activate the custom login option.
  • At this point you should be ready to restart SFS2X, unless you also need to setup the database connection. If you haven’t done this previously we suggest to follow this other recipe.

You can now startup the provided client and test the Extension.

» The login handler

The LoginEventHandler class is where we check the credentials. In this example we access the DBManager connection and use a PreparedStatement object in order to build the query with parameters and sanitize possible bad characters in the external arguments.

// Grab a connection from the DBManager connection pool
connection = dbManager.getConnection();

// Build a prepared statement
PreparedStatement stmt = connection.prepareStatement("SELECT pword,id FROM muppets WHERE name=?");
stmt.setString(1, userName);

// Execute query
ResultSet res = stmt.executeQuery();

As an alternative to this approach you could use the DBManager.executeQuery(String sql, Object[] params) method, which works similarly without the need to work at the connection level. We didn’t use it for this example because at the time of writing it wasn’t available yet (requires SmartFoxServer 2X version RC1b or later).

When an errror is encountered (e.g. bad password), we raise an exception of type SFSLoginException and provide an additional error code:

if (!getApi().checkSecurePassword(session, dbPword, cryptedPass))
{
    SFSErrorData data = new SFSErrorData(SFSErrorCode.LOGIN_BAD_PASSWORD);
    data.addParameter(userName);

    throw new SFSLoginException("Login failed for user: "  + userName, data);
}

In the case of a bad username or bad password we also specify the name or password used so that this is reported back to the client.

» Handling exceptions

One important aspect to keep in mind when working with a database is proper handling of exceptions. Any call to the database might throw an error if there is a problem with the SQL syntax etc…, so it is critical to make sure that the connection is closed in any cas,e otherwise it will leak and eventually exahust the connection pool. The best way to handle exceptions is the following:

ry
{
    connection = dbManager.getConnection();

    // Your database code goes here
    ...
}
catch(SQLException sqle)
{
    // Here we handle the SQL failure
    ...
}
finally
{
    try
    {
        connection.close();
    }
    catch(SQLException sqle)
    {
        // It shouldn't happen, but if it does it's best to leave a trace in the logs
    }
}

The finally block ensures that the connection gets closed in any case before leaving the method, and the additional try/catch block around the close() method handles the rare possibility that an error occurs when the connection is returned to the pool.

» Post login handler

When the USER_JOIN_ZONE event is notified we are ready to do more work with the user, which is now fully logged in the system. The ZoneJoinEventHandler class sets the “dbID” User Variable with the user id coming from the database and finally joins the user in the main lobby Room.

public class ZoneJoinEventHandler extends BaseServerEventHandler
{
    @Override
    public void handleServerEvent(ISFSEvent event) throws SFSException
    {
        User theUser = (User) event.getParameter(SFSEventParam.USER);

        // dbid is a hidden UserVariable, available only server side
        UserVariable uv_dbId = new SFSUserVariable("dbid", theUser.getSession().getProperty(DBLogin.DATABASE_ID));
        uv_dbId.setHidden(true);

        // The avatar UserVariable is a regular UserVariable
        UserVariable uv_avatar = new SFSUserVariable("avatar", "avatar_" + theUser.getName() + ".jpg");

        // Set the variables
        List<UserVariable> vars = Arrays.asList(uv_dbId, uv_avatar);
        getApi().setUserVariables(theUser, vars);

        // Join the user
        Room lobby = getParentExtension().getParentZone().getRoomByName("The Lobby");

        if (lobby == null)
            throw new SFSException("The Lobby Room was not found! Make sure a Room called 'The Lobby' exists in the Zone to make this example work correctly.");

        getApi().joinRoom(theUser, lobby);
    }
}