Web Application 開 發 利 器 - WebSnap!
第 五 章 、 使 用 者 管 理 及 Sessions
在 以 往 的 WebBroker+InternetExpress 架 構 下 , 我 們 處 理 使 用 者 控 制 或 是 Session 管 理 都 很 麻 煩 ,WebSnap 加 強 了 這 部 份 的 能 力 , 利 用 WebSnap 來 處 理 使 用 者 或 是 Session 是 一 件 非 常 簡 單 的 事 情 , 接 下 來 我 們 就 為 我 們 的 程 式 加 上 這 些 功 能 。
5-1 使 用 者 管 理 及 Sessions 實 作
要 在 WebSnap 程 式 中 使 用 Session 及 使 用 者 控 制 功 能 的 話 , 你 必 需 在 Application Module 中 加 入 三 個 元 件 :TEndUserSessionAdapter 、 TWebUserList 、 TSessionServices 。 TEndUserSessionAdapter 繼 承 至 TEndUserAdapter 並 加 入 了 Session 的 處 理 , 簡 單 的 說 她 只 是 把 使 用 者 的 資 料 存 入 Session 中 而 已 。 TWebUserList 則 負 責 儲 存 使 用 者 資 料 及 驗 證 的 工 作 , TSessionServices 是 整 個 WebSnap Application Session 控 管 的 核 心 元 件 , 當 網 頁 被 啟 動 時 TWebAppComponents 會 呼 叫 TSessionServices 來 產 生 一 個 Session 給 來 訪 者 , 接 著 再 將 這 個 Session 傳 給 WebContext 保 存 。 當 送 出 Response 時 將 SessionID 封 裝 成 Cookie 一 併 送 給 使 用 者 , 接 下 來 的 Request 就 會 以 Cookie 將 這 個 SessionID 送 回 給 我 們 的 程 式 。 有 了 這 個 SessionID 我 們 就 可 以 取 得 上 次 Session 中 的 資 料 了 , 這 些 動 作 會 受 到 Cookie 的 保 存 時 限 與 Session 的 保 存 時 限 所 影 響 , 當 Cookie 過 期 或 是 Session 的 時 限 到 期 亦 或 者 是 你 重 新 啟 動 了 程 式 後 Session 就 視 同 消 失 了 , 以 下 是 WebSnap 處 理 Session 的 流 程 :
( 圖 :6)
當 你 放 好 這 些 元 件 後 , 你 還 要 設 定 TWebUserList 中 的 使 用 者 資 料 , 請 雙 按 她 的 UserItems 特 性 值 來 加 入 使 用 者 資 料 , 並 設 定 其 ID 與 密 碼 資 料 :
這 個 範 例 中 除 了 TEndUserSessionAdapter 會 將 使 用 者 資 料 存 放 在 Session 之 外 , 我 們 也 搭 個 便 車 使 用 一 下 Session 物 件 。 請 你 在 TEndUserSessionAdapter 中 加 入 一 個 AdapterField 名 稱 是 AdaptAge:
接 著 撰 寫 她 的 OnGetValue 事 件 , 加 入 以 下 的 程 式 碼 :
if not VarIsEmpty(Session.Values['Age']) then
Value:=Session.Values['Age']
else Value:='';
上 面 這 一 段 程 式 碼 是 用 來 確 定 Session 中 是 否 有 一 個 Age 的 值 , 有 的 話 就 存 入 Value 變 數 中 。 這 樣 我 們 就 可 以 在 Home Module 的 Script 中 加 入 以 下 的 程 式 來 顯 示 這 個 值 :
<h1> 歡 迎 <%=EndUser.DisplayName %> 你 今 年 是 <%=EndUser.AdaptAge.Value%> 歲 </h1>
定 義 語 系 ( 以 免 被 當 成 其 它 語 系 網 頁 )
<meta http-equiv="Content-Type" content="text/html; charset=big5">
然 後 我 們 要 新 增 一 個 Page Module 來 讓 使 用 者 登 入 系 統 , 請 選 擇 AdapterPageProducer 並 將 PageName 設 為 Login , 接 著 在 裡 面 放 入 一 個 TLoginFormAdapter 元 件 。 由 於 我 們 希 望 搭 Login 的 便 車 讓 使 用 者 填 入 額 外 的 資 料 , 因 此 除 了 TLoginFormAdapter 預 設 的 三 個 AdapterField 元 件 之 外 , 我 們 還 要 額 外 加 入 一 個 AdapterField 元 件 :
完 成 後 我 們 還 得 撰 寫 AdaptAge 的 OnUpdateValue 事 件 , 將 輸 入 的 值 存 入 Session 中 :
procedure TLogin.AdaptAgeUpdateValue(Sender: TObject; Value: Variant);
begin
Session.Values['Age']:=AdaptAge.ActionValue.Values[0];
end;
然 後 我 們 就 可 以 開 啟 Visual Page Designer 來 設 計 我 們 的 登 入 網 頁 了 , 整 個 畫 面 如 下 :
最 後 回 到 Application Module 將 TEndUserSessionAdapter 的 LoginPage 特 性 值 設 為 Login( 登 入 網 頁 的 PageName) 就 完 成 了 。
那 我 們 要 如 何 將 一 個 網 頁 設 定 為 使 用 者 必 須 預 先 登 入 才 能 進 入 的 呢 ? 你 有 兩 種 選 擇 , 一 是 在 建 立 Page Module 時 將 Login Required 選 項 打 勾 , 另 一 個 是 手 動 去 改 變 Page Module 的 特 性 值 , 這 個 範 例 中 我 要 將 Edit 這 個 網 頁 設 定 成 需 要 先 登 入 才 能 訪 問 的 網 頁 , 因 此 我 將 Edit 內 的 程 式 碼 做 下 列 的 修 改 :
WebRequestHandler.AddWebModuleFactory(TWebPageModuleFactory.Create(TEdit, TWebPageInfo.Create(
[wpPublished , wpLoginRequired],'.html'),crOnDemand, caCache));
完 成 後 就 可 以 執 行 程 式 來 看 看 成 果 了 , 正 常 的 話 使 用 者 必 須 要 Login 之 後 才 能 訪 問 Edit 網 頁 , 這 點 倒 是 很 正 常 , 但 你 有 沒 有 發 現 到 一 個 問 題 :
上 圖 中 我 們 看 到 了 使 用 者 可 以 改 變 NextPage 來 選 擇 Login 後 導 向 的 網 頁 , 這 在 很 多 情 況 下 不 適 用 , 因 為 我 們 可 能 不 想 讓 使 用 者 改 變 這 個 設 定 , 我 們 可 能 想 要 在 程 式 中 判 斷 或 是 將 這 個 值 設 死 。 以 目 前 的 TLoginFormAdapter 來 看 , 這 個 NextPage 屬 性 是 保 護 層 級 的 , 那 也 代 表 著 我 們 無 法 改 變 她 , 據 DELPHI R&D 的 說 法 是 Patch 後 就 會 公 開 這 個 屬 性 了 , 目 前 我 們 可 以 使 用 以 下 的 技 巧 來 完 成 這 個 動 作 :
TProtectedLoginFormAdapter=class(TLoginFormAdapter);
… … … … … .
TProtectedLoginFormAdapter(LoginFormAdapter1).NextPage:='Home';
如 果 你 看 過 我 的 另 一 篇 文 章 的 話 , 那 這 個 技 巧 對 你 來 說 應 該 是 蠻 熟 悉 的 。 你 可 以 將 這 段 程 式 碼 放 至 在 Module 的 Activate 事 件 中 , 當 你 成 功 登 入 系 統 並 選 擇 Next Page 為 Code6421 的 首 頁 後 , 你 應 該 會 看 到 這 樣 的 畫 面 :
這 樣 你 應 該 清 楚 了 Session 的 運 用 了 吧 , 那 我 們 可 以 在 Session 中 存 入 那 些 型 態 的 資 料 呢 ? 理 論 上 只 要 是 Variant 所 支 援 的 型 態 就 可 以 存 入 Session 中 。
5-2 保 存 期 限 的 控 制
這 個 範 例 大 概 可 以 滿 足 大 部 份 的 需 求 , 但 你 看 到 以 下 的 畫 面 後 可 能 就 不 太 認 同 了 :
上 圖 中 我 開 啟 了 一 個 網 頁 登 入 後 又 另 外 再 開 啟 另 一 個 IE 網 頁 , 結 果 你 可 以 發 現 到 你 必 須 在 第 二 個 網 頁 中 再 登 入 一 次 , 這 在 許 多 情 況 下 是 不 適 用 的 。 這 是 因 為 Cookie 的 保 存 期 限 設 定 所 造 成 的 後 果 , 我 們 可 以 藉 由 設 定 Cookie 的 保 存 期 限 來 解 決 這 個 問 題 :
uses DateUtils;
… ..
procedure THome.WebAppComponentsBeforeDispatch(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
I:Integer;
begin
for I:=0 to WebContext.Response.Cookies.Count-1 do
begin
if SameText(WebContext.Response.Cookies.Items[I].Name,'WebBrokerSessionID') then
begin
if WebContext.Response.Cookies.Items[I].Expires = -1 then
WebContext.Response.Cookies.Items[I].Expires:=IncDay(Date);
end;
end;
end;
在 上 面 的 程 式 我 們 將 Cookie 的 保 存 期 限 設 為 一 天 , 然 後 你 再 啟 動 程 式 後 就 正 常 了 :
除 了 多 個 IE 視 窗 的 問 題 外 , 如 果 我 們 的 使 用 者 資 料 是 存 放 在 資 料 庫 中 時 要 如 何 做 呢 ? 看 以 下 的 片 段 程 式 你 就 清 楚 了 :
procedure THome.WebUserListBeforeValidateUser(Strings: TStrings;
var UserID: Variant; var Handled: Boolean);
begin
if Strings.Count > 0 then
UserID:=Strings.Values['UserName'];
Handled:=True;
end;
上 圖 中 我 只 是 單 純 的 記 錄 使 用 者 所 輸 入 的 資 料 , 也 就 是 說 不 管 他 是 誰 都 可 以 通 過 驗 證 。 你 可 以 在 這 裡 面 加 入 由 資 料 庫 中 取 得 資 料 並 與 使 用 者 輸 入 的 資 料 作 比 對 的 程 式 碼 , 成 功 的 話 才 把 使 用 者 名 稱 設 給 UserID 。 當 你 將 名 稱 設 給 UserID 後 , 就 代 表 著 該 使 用 者 已 通 過 驗 證 , 在 傳 入 的 Strings 參 數 中 有 兩 個 值 , 一 個 是 UserName , 另 一 個 就 是 Password 了 。
5-3 權 限 的 控 管
除 了 基 本 的 使 用 者 控 制 外 , WebSnap 也 提 供 了 權 限 的 控 管 , 範 圍 由 Page 到 Action 都 有 , 你 可 以 試 著 設 定 某 個 Page 的 xxxxAccess 特 性 值 來 處 理 權 限 的 控 制 。
WebRequestHandler.AddWebModuleFactory(TWebPageModuleFactory.Create(TMasterDetailEdit,
TWebPageInfo.Create([wpPublished {, wpLoginRequired}], '.html'), crOnDemand, caCache));
constructor TWebPageInfo.Create(AAccess: TWebPageAccess; const APageFile, APageName: string;
const ACaption, ADescription, AViewAccess: string);
在 TWebUserList 中 有 一 個 處 理 AccessRight 的 事 件 , 與 上 面 的 程 式 搭 配 後 , 你 就 可 以 建 構 出 完 整 的 使 用 者 控 管 功 能 了 ! 除 了 Adapter 、 WebPageInfo 、 AdapterAction 之 外 , 我 們 也 可 以 設 定 某 個 Adapter Control Items 針 對 權 限 的 不 同 做 出 正 確 的 反 應 。 例 如 你 可 以 設 定 TAdapterDisplayField 的 ViewMode 為 vmToggleOnAccess , 這 樣 這 個 Control 就 會 在 使 用 者 沒 有 Modify 權 限 時 轉 變 為 純 顯 示 , 在 使 用 者 擁 有 Modify 權 限 時 轉 變 為 Edit 。 看 起 來 好 像 很 不 錯 , 事 實 上 她 有 一 個 問 題 , 就 像 是 我 們 的 MasterDetailEdit 網 頁 是 在 使 用 者 一 進 入 網 頁 後 就 是 編 修 模 式 , 一 開 始 沒 啥 錯 誤 , 所 有 的 Control 因 為 使 用 者 沒 有 編 修 的 權 限 全 都 進 入 了 純 文 字 的 顯 示 , 但 你 別 忘 了 , 我 們 將 AdapterMode 設 成 Edit 了 , 這 也 代 表 著 我 們 移 動 記 錄 會 觸 發 Update 的 動 作 。 在 這 個 Update 函 式 的 第 一 行 就 呼 叫 了 CheckModifyAccess , 因 為 使 用 者 沒 有 編 修 的 權 限 , 當 然 例 外 就 跳 出 來 了 , 那 我 們 就 不 能 使 用 這 個 功 能 了 嗎 ? 呵 ! 山 不 轉 路 轉 , 我 們 可 以 在 MasterDetailEdit.OnBeforeDispatchPage 事 件 中 加 上 處 理 權 限 的 程 式 碼 :
procedure TMasterDetailEdit.WebPageModuleBeforeDispatchPage(
Sender: TObject; const PageName: String; var Handled: Boolean);
var
UserItem:TWebUserItem;
EditMode:string;
begin
UserItem:=Home.WebUserList1.UserItems.FindUserName(Home.EndUserSessionAdapter1.UserID);
EditMode:='';
if Assigned(UserItem) then
begin
if UserItem.AccessRights = wdmData.dsAdaptOrder.ModifyAccess then
EditMode:='Edit';
end;
AdapterFieldGroup1.AdapterMode:=EditMode;
AdapterGrid1.AdapterMode:=EditMode;
end;
然 後 將 所 有 的 AdapterEditColumn 以 及 AdapterDisplayField 的 ViewMode 都 改 成 vmToggleOnAccess 就 可 以 正 確 的 執 行 了 。 其 實 這 並 不 算 是 Bug , 只 是 使 用 的 方 式 不 同 罷 了 , 但 這 樣 還 是 不 足 以 完 成 整 個 MasterDetailEdit 的 權 限 控 制 , 使 用 者 還 是 可 以 使 用 那 些 按 紐 。 雖 然 會 產 生 例 外 , 也 不 會 更 動 到 資 料 , 但 感 覺 上 總 是 不 太 對 , 其 實 我 們 可 以 藉 由 設 定 DataSetAdapter.Actions 中 的 AdapterAction.ExecuteAccess 特 性 值 及 ActionButton.HiddenOptions 特 性 值 為 [bhoHiddOnNoExecuteAccess] , 這 樣 就 可 以 視 權 限 來 決 定 隱 藏 或 顯 示 按 紐 , 範 例 中 的 MasterEditDetail 網 頁 已 經 有 完 整 的 權 限 控 制 了 , 你 可 以 用 code6421 密 碼 是 1234 登 入 後 就 可 以 修 改 了 , 如 果 沒 有 登 入 的 話 , 你 就 只 能 夠 看 到 純 文 字 的 顯 示 。
5-4 將 Session 存 放 於 檔 案 中
控 制 Cookie 的 保 存 期 限 是 一 種 較 簡 單 的 方 法 , 但 是 這 種 方 式 有 個 致 命 的 缺 點 。 當 我 們 的 Web 程 式 被 關 閉 後 ( 指 整 個 程 式 被 從 記 憶 體 中 移 除 , 例 如 你 將 Web Server 或 電 腦 重 新 啟 動 ) , 因 為 Session 是 存 放 於 記 憶 體 之 中 , 所 以 也 跟 著 程 式 的 停 止 而 消 失 了 。 這 時 使 用 者 所 保 留 的 Cookie 也 就 沒 有 作 用 了 , 要 解 決 這 個 問 題 , 我 們 有 兩 種 選 擇 , 較 簡 單 的 方 法 是 在 程 式 停 止 之 前 , 將 整 個 Sessions 全 數 存 入 資 料 庫 中 , 下 次 啟 動 時 再 由 資 料 庫 讀 取 後 , 回 存 至 Sessions 物 件 。 這 種 方 法 有 個 缺 點 , 因 為 我 們 是 存 放 整 個 Sessions , 因 此 可 能 會 存 入 一 些 多 餘 的 資 料 , 例 如 暫 時 性 的 變 數 、 或 是 一 些 根 本 就 不 值 得 保 存 的 資 料 , 這 很 浪 費 記 憶 體 與 空 間 。 第 二 種 方 式 是 只 存 放 目 前 所 使 用 的 Session , 這 個 方 式 較 前 者 好 , 但 還 是 有 一 樣 的 問 題 , 只 是 問 題 的 情 況 較 簡 單 , 因 為 我 們 只 存 放 一 個 Session , 因 此 浪 費 的 部 份 也 只 限 於 這 個 Session 物 件 中 , 比 起 第 一 種 方 法 來 的 好 多 了 。 這 一 節 所 使 用 的 範 例 程 式 是 Demo2 , 請 你 開 啟 範 例 程 式 對 照 著 看 。
整 個 程 式 的 概 念 是 來 自 於 Nick Hodges , 我 取 用 了 部 份 的 程 式 碼 , 將 她 整 合 至 我 的 範 例 程 式 中 。 首 先 我 先 介 紹 在 Home Page Module 中 的 兩 個 工 具 函 式 :
// Write a session to a stream
procedure THome.SaveSession(AID: TSessionID; aStream: TStream);
var
TempSessions: TSessions;
TempItem: TSessionItem;
begin
TempSessions := TSessions.Create;
try
TempItem := TSessionItem.Create(TempSessions);
if Sessions.GetSession(AID, TempItem) then
TempSessions.SaveToStream(aStream)
else
Assert(False, 'Session not found');
finally
TempSessions.Free;
end;
end;
// Update or Add all name/value pairs from the saved session to a new or existing session
procedure THome.RestoreSession(AID: TSessionID; aStream: TStream);
var
TempSessions: TSessions;
Item: TSessionItem;
I: Integer;
begin
if aStream.Size <> 0 then // if there is no data there, don't do anything
begin
TempSessions := TSessions.Create;
try
TempSessions.LoadFromStream(aStream);
if AID = '' then
// Create a new session
AID := SessColn.Sessions.StartSession
else
Assert(Sessions.SessionExists(AID), 'Could not find session ' + AID);
Item := TempSessions.Items[0] as TSessionItem;
for I := 0 to Item.Items.Count - 1 do
Sessions.SetItemValue(AID, Item.Items.Names[I], Item.Items.Variants[I]);
finally
TempSessions.Free;
end;
end;
end;
SaveSession 函 式 的 功 用 是 取 出 Sessions 中 符 合 傳 入 的 SessionID 的 Session 取 出 , 由 於 Session 物 件 本 身 並 未 提 供 SaveToStream 的 功 能 , 這 個 功 能 位 於 Sessions 物 件 之 中 , 因 此 Nick 建 立 了 一 個 暫 時 性 的 Sessions 物 件 , 並 將 取 得 的 Session 複 製 到 暫 存 的 Session 物 件 之 中 , 最 後 再 呼 叫 SaveToStream 來 存 入 Session 物 件 至 Stream 中 。
RestoreSession 則 與 SaveSession 完 全 相 反 , 她 的 功 用 是 由 Stream 中 取 出 Session 物 件 , 接 著 將 她 回 存 到 現 行 的 Session 中 。 有 了 這 兩 個 工 具 函 式 後 , 我 們 撰 寫 程 式 就 方 便 多 了 。 接 下 來 我 們 先 規 劃 一 下 要 如 何 運 用 這 兩 個 函 式 建 立 可 將 Session 保 存 至 資 料 庫 的 程 式 。 下 面 是 我 們 程 式 的 流 程 圖 :
當 使 用 者 第 一 次 登 入 時 , 她 必 須 要 到 填 寫 個 人 資 料 的 網 頁 中 填 寫 資 料 , 在 這 個 網 頁 中 我 們 製 作 了 一 個 確 定 的 按 紐 , 執 行 後 我 們 就 利 用 資 料 庫 以 及 SaveSession 函 式 來 將 Session 存 入 資 料 庫 。 這 樣 使 用 者 第 二 次 登 入 時 , 系 統 就 可 以 由 資 料 庫 中 取 得 上 次 的 Session 資 料 , 接 著 使 用 RestoreSession 函 式 將 她 還 原 回 Session 物 件 。 我 們 建 構 一 個 新 的 Table , 在 其 中 建 立 三 個 欄 位 , USER_ID 、 SESSION_ID 、 SESSION_INFO , USER_ID 用 來 儲 存 使 用 者 登 入 的 ID , SESSION_ID 則 是 儲 存 使 用 者 登 入 後 系 統 所 產 生 的 SessionID , SESSION_INFO 是 用 在 儲 存 Session 物 件 上 。
接 著 我 們 要 撰 寫 儲 存 Session 至 資 料 庫 的 程 式 碼 , 我 們 選 擇 寫 在 使 用 者 填 入 個 人 資 料 並 按 下 確 定 按 紐 的 OnExecute 事 件 中 :
procedure TProfile.AdaptSaveProfileExecute(Sender: TObject;
Params: TStrings);
procedure SaveSession;
var
S:TStream;
begin
if not dmData.tbSessionFile.Active then
dmData.tbSessionFile.Open;
if dmData.tbSessionFile.Locate('USER_ID',WebContext.EndUser.DisplayName,[]) then
dmData.tbSessionFile.Edit
else
dmData.tbSessionFile.Append;
dmData.tbSessionFile.FieldByName('USER_ID').Value:=WebContext.EndUser.DisplayName;
dmData.tbSessionFile.FieldByName('SESSION_ID').Value:=Session.SessionID;
S:=dmData.tbSessionFile.CreateBlobStream(dmData.tbSessionFile.FieldByName('SESSION_INFO'),bmWrite);
try
Home.SaveSession(Session.SessionID,S);
dmData.tbSessionFile.Post;
finally
S.Free;
end;
end;
begin
Adapter1.UpdateRecords((WebContext.AdapterRequest as IActionRequest));
SaveSession;
end;
在 使 用 者 登 入 後 , 如 果 判 定 使 用 者 是 第 二 次 登 入 的 話 , 我 們 要 由 資 料 庫 將 Session 取 出 並 還 原 , 我 選 擇 將 她 寫 在 WebUserList.OnBeforeValidateUser 事 件 中 :
procedure THome.WebUserListBeforeValidateUser(Strings: TStrings;
var UserID: Variant; var Handled: Boolean);
procedure LoadSession(AUserID:string);
var
S:TStream;
begin
if WebUserList.UserItems.Count = 0 then
exit;
if not dmData.tbSessionFile.Active then
dmData.tbSessionFile.Open;
if dmData.tbSessionFile.Locate('USER_ID',AUserID,[]) then
begin
S:=dmData.tbSessionFile.CreateBlobStream(dmData.tbSessionFile.FieldByName('SESSION_INFO'),
bmRead);
try
RestoreSession(Session.SessionID,S);
finally
S.Free;
end;
end;
end;
var
uItems:TWebUserItem;
begin
//process user login
if not dmData.tbEmployee.Active then
dmData.tbEmployee.Open;
if dmData.tbEmployee.Locate('FirstName;EmpNo',VarArrayOf([Strings.Values['Username'],
Strings.Values['Password']]),[]) then
begin
uItems:=WebUserList.UserItems.Add as TWebUserItem;
uItems.UserName:=Strings.Values['Username'];
uItems.Password:=Strings.Values['Password'];
uItems.AccessRights:='';
LoadSession(uItems.UserName);
end;
end;
這 樣 我 們 就 完 成 了 保 存 Session 的 程 式 了 , 這 個 程 式 有 個 小 缺 點 , 那 就 是 儲 存 的 範 圍 還 是 太 廣 了 些 , Session 中 可 能 會 有 一 些 暫 存 型 的 資 料 , 我 們 並 不 想 保 留 這 些 資 料 , 但 這 個 程 式 還 是 會 將 這 些 資 料 一 併 存 入 。 在 日 後 的 文 章 中 我 會 重 新 撰 寫 這 個 程 式 , 讓 她 只 存 我 們 所 需 要 的 資 料 。
本 章 後 記
這 一 節 我 們 討 論 了 使 用 者 管 理 及 Session 等 技 術 , 到 這 裡 為 止 這 篇 文 章 的 上 半 段 也 結 束 了 , 接 著 我 們 將 會 進 入 另 外 一 個 階 段 , 詳 細 探 索 WebSnap 的 細 部 行 為 及 一 些 更 進 階 的 應 用 。