DA21 – 验证和应用程序安全(Delphi)

 
DA21 – 验证和应用程序安全 (Delphi)
本文描述了在 Data Abstract 4.0 'Vinci' Delphi 版本中如何处理登陆和用户验证 ..NET 版本见 DA24.
导论
绝大多数系统需要保障一定级别的安全 , 如加密 , 简单的登陆注销 , 当获取数据或将数据更新回数据源时基于更复杂的用户及角色的逻辑应用 .
LoginSample 项目是一个全面讲解如何实现应用程序安全和验证的范例 . 注意 : 我们不在这个范例中讨论加密 , 这些只不过需要设置 RemObjects SDK 消息组件的一些属性而已 .
在客户端 , 用户首先需要提供用户名 , 密码以及数据库连接名称登陆 .
由于服务端可以运行于 MSSQL Northwind 数据库和 Interbase/Firebird Employee.gdb, 所以第三个参数也是必要的 .
登陆成功后 , 用户可以查询 Orders 数据 . 依赖于不同的用户 , 数据通过特定 Customer 筛选 . 同时对于登陆用户的不同 , 管理数据的功能也或开放或禁用 .
我们将检验服务端和客户端项目看看这些是如何实现的 .
服务端的 RODL 文件
如果你打开服务端项目并启动 Service Builder, 你将看到如下 RODL 文件 :
DA21 – 验证和应用程序安全(Delphi)_第1张图片
可见 , 服务端开发了两个服务 : LoginService DataService.
LoginService
LoginService 是本例需要编码的第一个服务 , 其开放的接口如下 :
{ ILoginService }
  ILoginService = interface(IDARemoteService)
    [ '{16A949AC-0759-409D-A81B-5CB67352D4E2}' ]
    function GetConnectionNames: String;
    function Login(const UserName: String;
                   const Password: String;
                   const ConnectionName: String;
                  out LoginInfo: TDALoginInfo): Boolean;
    procedure Logout;
 end;
LoginService.GetConnectionNames
GetConnectionNames 返回服务端定义的数据库连接列表 . 方法返回的字符串使用逗号间隔连接名称 , "IBX,ADO" 并加载到客户端项目的适当 ComboBox .
DA21 – 验证和应用程序安全(Delphi)_第2张图片
在客户端主窗体的 OnCreate 事件中调用这个方法 :
procedure TClientForm.FormCreate(Sender: TObject);
 var i : integer;
 begin
    { Gets the list of the connections defined on the server }
    cbConnectionName.Items.CommaText :=
       (svcLoginService as ILoginService).GetConnectionNames;
    cbConnectionName.ItemIndex := 1 ;
    EnforceSecurity;
 end;
OnCreate 事件中提供附加的链接列表同时也确保客户端应用程序可以连接到服务端 . 如果由于某些原因客户端无法与服务端通讯 , 将显示 " 不能建立与服务端的连接 " 错误 , 客户端应用程序立即退出 . 事件处理的最后一行代码调用 EnforceSecurity 方法 , 简单的设置按钮的可用或禁用状态限制逻辑操作 .
这是 EnforceSecurity 方法的实现 :
procedure TClientForm.EnforceSecurity;
 begin
    if not LoggedIn and dtOrders.Active then dtOrders.Close;
 
    if not LoggedIn
      then Caption:=Application.Title+ ' - Lot Logged In '
      else Caption:=Application.Title+ ' - Session ' +LoginInfo.SessionID;
 
    bbLogout.Enabled := LoggedIn;
    bbLogin.Enabled := not LoggedIn;
    bbQueryOrders.Enabled:=LoggedIn and HasPrivilege(priv_QueryOrders);
    bbAdminUsers.Enabled:=LoggedIn and HasPrivilege(priv_AdminUsers);
 end;
这个方法在很多地方调用 , 以简单高效的方式保证用户界面与数据同步 . 接下来我们将分析 LoggedIn 属性和 HasPrivilege 方法 .
LoginService.Login
这个方法可能是本范例最重要的基础方法 . 其需要三个输入参数 (UserName, Password ConnectionName) 并返回两个值 :TDALoginInfo 类型的 LoginInfo 以及一个 Boolean .
LoginInfo 定义在 Data Abstract RODL 文件并声明如下 :
{ TDALoginInfo }
  TDALoginInfo = class(TROComplexType)
 private
    [..]
 published
    property SessionID: String read fSessionID write fSessionID;
    property UserID: String read fUserID write fUserID;
    property Privileges: TDAStringArray
      read GetPrivileges write fPrivileges;
    property Attributes: TDAStringArray
      read GetAttributes write fAttributes;
    property Data: Binary read GetData write fData;
 end;
Login 方法需要向客户端返回数据用于不需要做频繁的网络往返就可以配置用户界面和执行安全检查 . TDALoginInfo.SessionID 属性返回到客户端并作为区别于其他用户的值保持下来 . RemObjects SDK 的老版本中这个值必须赋给 , 因为其一直都是在服务端初始化的 . RemObjects SDK 最近版本中 , 没有必要在第一次调用时服务端生成新的 Session 值传递给客户端了 .
服务端 ,Login 方法代码如下 :
function TLoginService.Login(const UserName, Password,
 ConnectionName: String; out LoginInfo: TDALoginInfo): Boolean;
var ds : IDADataset;
    isadmin : boolean;
    position : TDAField;
begin
  { Aquires the connection specified by the parameter ConnectionName.
    Notice how the property AquireConnection of this service is set
    to FALSE. The reason is that we cannot aquire a connection before
    the execution of this method. }
  Connection := Schema.ConnectionManager.NewConnection(ConnectionName);
 
  { Validates the login }
  ds := Schema.NewDataset(Connection, ds_ValidateLogin,
                          [ 'UserName' , 'Password' ],
                          [UserName, Password]);
 
 result := (ds.RecordCount= 1 );
 
 if not result then begin
    { Invalid login. The temporary session is not to be kept. }
    DestroySession;
    Exit;
 end
 else begin
    { Successful login. We send back information to the client }
    LoginInfo := TDALoginInfo.Create;
    LoginInfo.UserID := UserName;
    LoginInfo.SessionID := GUIDToString(Session.SessionID);
 
    position := ds.FieldByName( 'Position' );
 
    { In this demo, we assume that only Buchanan Steven can administer
      users in the Northwind database or any employee having the "Admin"
      job in the Employee database (i.e. Terri Lee). Others can only
      query for the orders. }
 
    isadmin := (SameText(position.AsString, 'Admin' )
            or SameText(position.AsString, 'Sales Manager' ));
 
    if isadmin
      then LoginInfo.Privileges.Add(priv_AdminUsers);
 
    LoginInfo.Privileges.Add(priv_QueryOrders);
 
    { We also store the name of the connection for this user and a
      value to indicate if he/she is an administrator to enforce
      server side security in the other service }
 
    Session.Values[sess_ConnectionName] := ConnectionName;
    Session.Values[priv_CanQueryAllOrders] := isadmin;
 end;
end ;
特别注意当代码设置返回值为 True 并初始化输出参数 LoginInfo 的部分 .
在这部分中你将看到这样的代码行 :
LoginInfo.SessionID := GUIDToString(Session.SessionID);
Session 属性可用于所有的 RemObjects 服务 , 如果你的服务使用了 Session Manager 将会被初始化 . 如下截图展示了 LoginService 的数据模块属性 :
DA21 – 验证和应用程序安全(Delphi)_第3张图片
如果没有设置 SessionManager 属性 ,Session 属性将返回 Null. 另外值得注意的事情是我们在登陆处理中只传递了数据库练级名称 . 如果你查看 Login 方法实现代码的最后两行 , 将会看到我们随同其他值一起将 ConnectionName 保存到了用户 Session . 这两个值将在 DataService 中用于决定连接到那个数据库以及执行一些安全操作 .
TDALoginInfo 类型应该作为模板使用 , 当你创建自己的登陆服务时其不是必须的 . 你可以创建更多或更少的属性的自定义类型 .
客户端代码在 bbLogin 按钮中触发调用 Login 方法 , 代码如下 :
procedure TClientForm.bbLoginClick(Sender: TObject);
var res : boolean;
begin
  FreeAndNIL(fLoginInfo);
 res := (svcLoginService as ILoginService).Login(eUserName.Text,
ePassword.Text, cbConnectionName.Text, fLoginInfo);
 if not res
    then MessageDlg( 'Invalid login!' , mtWarning, [mbOK], 0 );
  { Enables or disables buttons according to privileges }
  EnforceSecurity;
end ;
session LoginService.Logout 的其他思考
当客户端调用远程服务的方法时 , ClientID 属性值包含在请求之中 . 这是透明的而且在 BIN SOAP 消息组件中同时支持 . 这允许服务在 SessionManager 中查找预先存在的客户端 Session 或当特定客户端 ID 不存在时创建一个新的 Session. 这样这个新的 Session 将具有一个 SessionID( 匹配到关联 TROMessage.ClientID ClientID). 如果 Sessin 没有发现将临时生成一个保存在内存中 .Session 将保存在 SessionManager 中直到调用了 DestroySession 方法 .
Session 查找在 TRORemoteDataModule ( 你服务的父类 ) DoOnActivate 方法中完成 . 这个方法及其对应的 DoOnDeactivate 方法将在每次远程方法调用前后自动调用 .
procedure TRORemoteDataModule.DoOnActivate(
       aClientID: TGUID; const aMessage : IROMessage);
begin
 if (csDesigning in ComponentState) then Exit;
 
 fSession := NIL;
 if not CustomRequireSession(aMessage) then Exit;
 
 if Assigned(fSessionManager) then begin
    fNewSession := FALSE;
    fDestroySession := FALSE; // Resets the flags
 
    fSession := fSessionManager.FindSession(aClientID);
 
    if (fSession=NIL) then begin
 
      if RequiresSession then
        RaiseError(err_SessionNotFound,
                   [GUIDToString(aClientID)],
                    EROSessionNotFound)
      else begin
        fSession := fSessionManager.CreateSession(aClientID);
        fNewSession := TRUE;
      end;
    end;
 end
 else begin
    if RequiresSession then
      RaiseError( 'SessionManager required, but not assigned' );
 end;
end ;
可见 , 服务要求 SessionManager 查找 Session 的代码只有一行
fSession := fSessionManager.FindSession(aClientID);
如果 Session 没有找到 ,RequiresSession 设置为 True 时将抛出一个异常 , 否则自动生成一个新的 Session.
RequiresSession RemObjects 服务的非常重要的属性 , 保证除非 Session 以前被创建并保存在 SessionManager ( 通常使用如上面的 Login 方法 ) 否则不能调用任何服务方法 .
查看本范例中我们编写的两个服务的这个属性值 :
DA21 – 验证和应用程序安全(Delphi)_第4张图片
LoginService RequireSession 属性设置为 False 暗示任何客户端随时都可以调用这个服务 . DataService 则不允许调用任何方法除非提前生成 Session 并保存在 SessionManager .
一旦服务方法调用完毕 , TRORemoteDataModule.DoOnDeactivate 被调用 . 这个方法实现如下 :
procedure TRORemoteDataModule.DoOnDeactivate(aClientID: TGUID);
 var
    lSessionID: TGUID;
 begin
    if (csDesigning in ComponentState) then Exit;
 
    if Assigned(fSessionManager) and (fSession<>NIL) then begin
 
      if fDestroySession then begin
        if NewSession then begin
          fSessionManager.DeleteTemporarySession(fSession)
        end
        else begin
          lSessionID := fSession.SessionID;
          fSessionManager.ReleaseSession(fSession, false);
          fSessionManager.DeleteSession(lSessionID, false)
        end;
      end
      else begin
        fSessionManager.ReleaseSession(fSession, NewSession);
      end;
    end;
 end;
如上面所述 , 这个方法负责在 SessionManager 中存储或删除 Session, fDestroySession 标志位决定执行什么动作 , 其值可以通过调用 DestroySession 方法设置为 False.
如果在 Login 方法中登陆失败将调用 DestroySession:
function TLoginService.Login(const UserName, Password,
 ConnectionName: String; out LoginInfo: TDALoginInfo): Boolean;
 [..]
 if not result then begin
    { Invalid login. The temporary session is not to be kept. }
    DestroySession;
    Exit;
 end
  [..]
或在 Logout 方法中 :
procedure TLoginService.Logout;
begin
  DestroySession;
end ;
总结 : 当远程调用方法并且为服务指定了 SessionManager , 服务都将去试图查找一个已存在的 Session. 如果没有查找到则建立一个临时的 Session. 你可以通过服务的 Session 属性存取这个 Session 并调用 DestroySession 方法删除 .
如果你不希望创建临时 Session 并且要确保 Session 已经提前创建 , 只需要设置 RequiresSession TRUE.
DataService
DataService 非常简单 . 其除了从 IDARemoteService 继承的方法外没有引入任何方法 . 我们实现了这个服务的两个事件处理 :OnBeforeAcquireConnection OnBeforeGetDatasetData.
DataService.OnBeforeAcquireConnection
这个事件在将 AcquireConnection 设置为 Ture 并且当服务向 ConnectionManager 请求数据库连接时触发 . 你可以运行时重新设置 ConnectionName 连接到不同的数据库 , 或在其值为空时使用 ConnectionManager 中的默认连接 . 这里我们要使用在执行 LoginService.Login 方法时保存在 Session 中的连接名称 :
procedure TDataService.DARemoteServiceBeforeAcquireConnection(
 Sender: TDARemoteService; var ConnectionName: String);
begin
  { This value has been stored during the Login method }
  ConnectionName := Session.Values[sess_ConnectionName]
end ;
DataService.OnBeforeGetDatasetData
这个事件我们执行一些服务端的安全逻辑 , 如本文开始时提到的 . 使用以 priv_CanQueryAllOrders 为存储 ID Boolean 类型的 Session ( Login 方法 ), 我们或者获取所有的 Orders 记录或者硬编码对特定客户做筛选 . 代码如下 :
procedure TDataService.DARemoteServiceBeforeGetDatasetData(
 const Dataset: IDADataset; const IncludeSchema: Boolean;
 const MaxRecords: Integer);
var filtervalue : variant;
begin
  { This is an example of how to use session values to enforce
    additional server side security. }
  if (Session.Values[priv_CanQueryAllOrders]=FALSE) then begin
 
    if Supports(Connection, IInterbaseConnection)
      then filtervalue := '1006' // IB filter
      else filtervalue := 'FRANK' ; // MSSQL filter
 
    Dataset.Where.AddCondition( 'CustomerID' , cEqual, filtervalue);
 end;
end ;
当存取将 RequireSession 设置为 True 的服务时设计时的考虑
最后一个重点考虑的事情是留意在设计时存取设置 RequireSession True 的服务 . 如果你启动你的服务并试图打开客户端窗体中的 dtOrders 数据表 , 你将得到一个 "Session {xyz} not found" 的错误提示 . 虽然在设计时也能得到安全验证是件好事情 , 但是却在开发客户端应用程序时引入了一定的问题 .
我们如何在需要登陆但是必须要启动客户端应用程序才能登陆的情况下存取 DataService 服务呢 ? 答案是前期已经拖放的 TDADesignTimeCall 组件 . 这个组件允许你在设计时调用远程方法 , 我们只需要使用这个组件调用 LoginService.Login 并以 Nancy Davolio 的身份登陆 . 双击组件方法将执行 , 这时 Delphi IDE 就像一个登录到服务端的客户端 .
DA21 – 验证和应用程序安全(Delphi)_第5张图片
总结
RemObjects SDK Data Abstract 组合提供了很多预先定义好的功能创建安全系统 . 我们希望本文能让你理解 TROMessage.ClientID Session 如何协同运行 , 以及为什么返回像 TDALoginInfo 的结构在减少网络往返次数的情况下实现客户端安全 .
 

你可能感兴趣的:(DA21 – 验证和应用程序安全(Delphi))