Managing sessions with Delphi 6 Web services (updated)

By: Other Guy

Abstract: Building an e-business application? Then you'll need techniques like these to manage session states and authentication. By Daniel Polistchuck.

Hello folks! We are about to dive into one of the hottest topics of our times: Web services!

But wait -- aren't there already a lot of articles about Web services and SOAP? Probably... but this one is different. We will be talking about a specific programming technique that is very important if you seek to write stateful Web Services.

Why stateful? Isn't a Web service an object that is instantiated on the Internet with its methods called from all over the globe?

Well, yes, almost. The problem is that a Web service, since it uses HTTP as its transport protocol, is stateless. So it's up to us developers to write some kind of state-maintenance mechanism.

In this article we will be doing just that.

The solution

To implement state in a stateless framework, we must save the state (context) between method calls using some kind of persistence layer. An easy, lightweight one may be created using MyBase, the TClientDataset file-based database. The general schema for the solution looks like this:

Starting is easy.. you've read about this in Nick Hodges's Shakespearean insult generator Web service article. But we can repeat it here for clarity's sake. Please follow the steps below:

  1. Register Delphi 6 (not just because it's the right thing to do, but because that's how you get the eXtreme Components collection with the Invokable Wizard).
  2. Select File | New | Other | Web Services | Soap Server Application from Delphi 6's main menu.
  3. Choose ISAPI/NSAPI Dynamic Link Library.

Although you could normally use any kind of Web application to write your Web Service, if you ever really implement a session/TClientDataset-dependent one, please use an ISAPI DLL for your production code. The reason is that we must have a single TClientDataset instance shared by all the client connections, protected by a Critical Section from concurrent accesses. This way we don't lose session information when two clients connect to the Web Service at the same time. (Thanks to Deepak Shenoy for calling my attention to this bit of technical arcana.)

Now you have an empty Web service. The files that were generated by Delphi up to this point are these:

Project1.dpr: This is your project source file. Save it as AuthServer.dpr.

Unit1.pas: Save it as UwmAuthServer. Name your WebModule wmAuthServer. This is your Web Service WebModule with Delphi 6's SOAP implementation three core components:

  • THTTPSoapDispatcher: Responsible for the response of SOAP messages delegating their handling to an Invoker component.
  • THTTPSoapPascalInvoker: Upon interpretation of a SOAP message, executes the corresponding Pascal code which you implemented using your Invokable Interface (explained below).
  • TWSDLHTMLPublish: Produces WSDL (Web Service Definition Language) code from the Invokable Interfaces you register within your application.

The following step is to provide it with some code. (Or did you think Delphi would do it for you automatically?)

If you have successfully registered your copy of Delphi 6, you will be able to use the great Invokable Interface Class Wizard and Delphi will provide you with two units: one for the interface and another for the implementation class. Just select File | New | Other | Web Services | Invokable Wizard from Delphi 6's main menu:

As you can see, using the wizard is pretty simple:

  1. Fill in the base name of the class and interface you will be using. In our case, use Authenticator.
  2. Accept the default generated base unit identifier.
  3. Select InvokableClass in the drop down. (The other option, TInterfacedObject, is not so easy to use. You would have to provide a factory and register it later...oh no! I am wrong -- the Invokable Wizard does this for you!)
  4. Press Generate.

Now you get a couple of units added to your project automatically:

Here is the interface unit:

{ Invokable interface declaration unit for IAuthenticator }

unit AuthenticatorIntf;

interface

uses
  Types, XSBuiltIns;

type
  IAuthenticator = interface(IInvokable)
    ['{BC2B44FB-8161-4BE3-8836-50F1DEBCE7D9}']
    // Declare your invokable logic here using standard Object Pascal code
    // Remember to include a calling convention! (usually stdcall)
  end;

implementation

uses
  InvokeRegistry;

initialization
  InvRegistry.RegisterInterface(TypeInfo(IAuthenticator), '', '');

end.

And here is the implementation unit:

{ Invokable implementation declaration unit for TAuthenticator,
  which implements IAuthenticator }

unit AuthenticatorImpl;

interface

uses
  AuthenticatorIntf, InvokeRegistry;

type
  TAuthenticator = class(TInvokableClass, IAuthenticator)
    // Make sure you have your invokable logic implemented in IAuthenticator
    // first, then use CodeInsight(tm) to fill in this implementation
    // section by pressing Ctrl+Space, marking all the interface
    // declarations for IAuthenticator, and pressing Enter.
    // Once the declarations are inserted here, use ClassCompletion(tm)
    // to write the implementation stubs by pressing Ctrl+Shift+C
  end;

implementation

initialization
  InvRegistry.RegisterInvokableClass(TAuthenticator);

end.

Up until this point we have done everything as it should be done for each and every Web service. The cool part begins now!

We must decide which methods to add to the IAuthenticator interface. Let's add the following:

  • Login: Log a user/password pair into the Web Service. Upon success, returns a session handle.
  • Logout: The opposite of Login. Duh!
  • AddSessionData: Lets the Web consumer add custom data to the current session.
  • GetSessionData: Retrieves data for the Web consumer.

Here is the final declaration of the IAuthenticator interface:

type
  IAuthenticator = interface(IInvokable)
    ['{BD4E3554-A4BB-40FA-B5A7-60D2B599F101}']
    function Login (const User, Password : String;
      var SessionHandle: String) : boolean; stdcall;
    function AddSessionData (const SessionHandle : String; const Data : String) : Boolean; stdcall;
    function GetSessionData (const SessionHandle : String) : String; stdcall;
    function IsValidSession (const SessionHandle : String) : boolean; stdcall;
    function Logout (const SessionHandle : String): boolean; stdcall;
  end;

Please be careful to use stdcall or cdecl instead of Object Pascal's default register calling convention, or else there won't be RTTI enough left for the Web Service infrastructure to manipulate your interface.

Now you can simply press Ctrl-Shift-C and start coding. Right?

Wrong!

Ever seen an interface with code? I haven't. You must switch to the AuthenticatorIntf.pas unit and use Ctrl-Space as much as you want up to the point when all the interface methods are declared in the class definition. Then press Ctrl-Shift-C and behold the wonders of CodeCompletion. Your class declaration is now complete and the methods are correctly declared in the implementation part of your unit.

Before we start writing the implementation of our methods, let's create a new TDataModule named dtmSessions and drop a TClientDataset component named cdsSessions onto it.

Add three FieldDefs to it: SessionHandle, SessionUser and SessionData, all three with DataType ftString and Size 255.

Add the following code to the OnCreate event handler of the TDataModule:

procedure TdtmSessions.DataModuleCreate(Sender: TObject);
begin
  cdsSessions.FileName := ExtractFilePath(ParamStr(0))+ 'AuthSessions.cds';
  if not FileExists (cdsSessions.FileName) then
    cdsSessions.CreateDataSet
  else
    cdsSessions.Open;
end;

This ensures that the ClientDataset is either created or opened, as necessary.

To avoid problems with multiple TDataModules in your Web Application, do not forget to remove dtmSessions DataModule from the auto-create list of your project. To use this DataModule, let's create and free it ourselves in the initialization and finalization parts of its unit:

initialization
  dtmSessions := TdtmSessions.Create(nil);
finalization
  dtmSessions.Free;
end.

To avoid concurrency problems, let's declare a TCriticalSection object in the AuthenticatorImpl.pas implementation part:

implementation
uses
  SyncObjs, //for the Critical Section
  DB, //Locate constants
  SysUtils, //GuidToString function
  ActiveX, //CoCreateGuid call
  UwmAuthServer,
  UdtmSessions; //The Sessions DataModule 

var
  CS : TCriticalSection;

As with the TDataModule above, let's take care of the TCriticalSection life-cycle in the initialization and finalization of its unit:

initialization
  InvRegistry.RegisterInvokableClass(TAuthenticator);
  CS := TCriticalSection.Create;
finalization
  CS.Free;
end.

Now let's code each of our Web service methods:

Login

Here's the code:

function TAuthenticator.Login(const User, Password: String;
  var SessionHandle: String): Boolean;
begin
  //Here you would check for the validity of the User/Password pair
  //The implementation below is just a simple substitute of a better
  //mechanism.
  Result := True;
  if (CompareText (User,'daniel')<>0) or (password <> 'test') then
  begin
    Result := False;
    Exit;
  end;
  //let's see if the user is already logged in
  CS.Enter;
  try
    if dtmSessions.cdsSessions.Locate('SessionUser',User,[loCaseInsensitive]) then
      raise Exception.Create('User already logged in');
    //from here on, we will assume that the login is ok, let's create a session!
    SessionHandle := GetStringGuid;
    dtmSessions.cdsSessions.Insert;
    dtmSessions.cdsSessions.FieldByName('SessionHandle').AsString := SessionHandle;
    dtmSessions.cdsSessions.FieldByName('SessionUser').AsString := User;
    dtmSessions.cdsSessions.Post;
    dtmSessions.cdsSessions.SaveToFile;
  finally
    CS.Leave;
  end;
end;

Notice that we are protecting the access to our TClientDataset data (even in the call to its Locate method, since it's not thread-safe) with a critical section. Critical sections work by blocking every thread from the same process from entering its protected area concurrently. When a second (or third, or fourth...) thread tries to enter a critical section, the operating system (Windows here, but it could as easily be Linux) sets its state to WAIT until the critical section is left by the first thread.

The GetStringGuid method implementation follows:

function TAuthenticator.GetStringGuid: String;
var
  GUID : TGUID;
begin
  CoCreateGuid(GUID);
  Result := GUIDToString(GUID);
end;

Logout

Here comes the code:

function TAuthenticator.Logout(const SessionHandle: String): Boolean;
begin
  //Locating the session
  if not LocateSession(SessionHandle) then
  begin
    Result := False;
    Exit;
  end;
  //deleting the session
  CS.Enter;
  try
    wmAuthServer.cdsSessions.Delete;
    wmAuthServer.cdsSessions.SaveToFile;
  finally
    CS.Leave;
  end;
  Result := True;
end;

The LocateSession implementation follows:

function TAuthenticator.LocateSession (const SessionHandle : String): boolean;
begin
  //Locating the session
  Result := IsValidSession(SessionHandle);
end;

Oh no! It uses IsValidSession! What is that?

IsValidSession

The code:

function TAuthenticator.IsValidSession(
  const SessionHandle: String): Boolean;
begin
  CS.Enter;
  try
    Result := dtmSessions.cdsSessions.Locate('SessionHandle',SessionHandle,[]);
  finally
    CS.Leave;
  end;
end;

Difficult, eh?

AddSessionData

And the corresponding code is:

function TAuthenticator.AddSessionData(const SessionHandle: String;
  const Data: String): Boolean;
begin
  //checking if session is already established
  Result := True;
  if not LocateSession(SessionHandle) then
  begin
    Result :=False;
    Exit;
  end;
  //ok, let's add the data
  CS.Enter;
  try
    wmAuthServer.cdsSessions.Edit;
    wmAuthServer.cdsSessions.FieldByName('SessionData').AsString := Data;
    wmAuthServer.cdsSessions.Post;
    wmAuthServer.cdsSessions.SaveToFile;
  finally
    CS.Leave;
  end;
end;

GetSessionData

The code:

function TAuthenticator.GetSessionData(
  const SessionHandle: String): String;
begin
  Result := '';
  if not LocateSession(SessionHandle) then
    Exit;
  Result := wmAuthServer.cdsSessions.FieldByName ('SessionData').AsString;
end;

And that's it. We're finished.

Oh, I almost forgot...the client.

That's the easy part. The client may be part of another article. But here's a nice screen shot from one I wrote:

If you would like to get the full source code for the examples (and the client) used in this article, get it here. It's in CodeCentral. If you have any further questions, send me an email at daniel@qualtech.com.br or danpol@pobox.com.

Cheers!

Daniel Polistchuck
IT Director
QualTech IT - Brazil
daniel@qualtech.com.br



Server Response from: ETNASC01