SOHA - 面向服务的 HTML 应用程序(会话和安全)






4.85/5 (9投票s)
SOHA(面向服务的HTML应用程序)的会话管理、导航、安全和数据共享等方面
引言
在本文的第一部分中,我们讨论了面向服务的HTML应用程序(SOHA)的关键区别特征:它拥有松散耦合的前端和服务;后端服务是与表示层无关的业务逻辑并提供数据访问;Web服务器提供静态的HTML/CSS/JavaScript/资源文件,没有任何服务器端页面生成HTML;而在客户端,DOM基于Ajax数据构建,无需依赖浏览器插件即可创建丰富的交互性。
我们还讨论了一些演示代码和演示页面,展示了jQuery在不借助服务器端页面(如ASP, ASP.NET MVC, JSP, PHP, Ruby on Rails, ColdFusion等)的情况下,创建和更改不同用户偏好设置及布局/主题的强大功能。它在运行时按需动态加载HTML、CSS和JavaScript。这些是构建SOHA(面向服务的HTML应用程序)的基础——全部通过Web服务器上的静态文件实现。
由于在SOHA中没有为服务器端页面技术和客户端浏览器插件保留角色,除了在第一部分中讨论的页面动态性之外,我们在这种架构中面临一些挑战,例如:会话管理、安全性、导航、页面间数据传输以及通过Ajax进行的客户端/服务交互等。本文的这一部分将详细介绍解决这些挑战的方案。
SOHA 会话管理
在某种程度上,SOHA旨在构建一个无需浏览器插件的富互联网应用(RIA),也就是说,不依赖于Flash Player、Silverlight和JavaFX。常规的RIA网站完全基于Flex或Silverlight构建,它也没有任何服务器端页面,前端依赖于来自服务的数据。当我们用HTML/CSS/JavaScript替换ActionScript/C#/Java中的代码时,这种面向服务的特性保持不变,SOHA的会话管理与常规RIA基本相同。
正如RIA所做的那样,有多种技术可以处理SOHA中的会话管理。我偏好并经过测试的方法是,让服务承担会话管理和用户认证的责任,客户端逻辑将根据服务响应采取行动。
由于服务通常以RESTful风格设计并通过HTTP(s)访问,因此服务在会话建立后使用cookie作为会话密钥是合理的。对于后续通过Ajax进行的服务调用,浏览器会确保将有效的cookie附加到请求中。在应用服务器处理请求之前,它通常会通过一个“安全过滤器”来验证cookie。如果cookie无效或未与任何会话关联,它将返回HTTP 401(未授权),然后客户端的Ajax代码可以采取适当的行动,导航到公共区域或通知用户需要登录。如果cookie有效并与一个活动会话关联,则认证完成;控制权将传递给请求的服务方法。
在此过程中,cookie由服务设置并由服务使用,客户端代码从不接触它们,并且服务完全控制cookie的生命周期。服务通常在用户登录后设置它们,并在每次后续的已认证调用中重置cookie。在安全的Web应用中,cookie通常是会话cookie,它在用户注销、关闭浏览器或会话超时时过期。值得注意的一点是,客户端逻辑只响应服务返回的结果,它不关心cookie的值。客户端唯一需要处理cookie的时候是在应用程序加载时,它帮助服务检测cookie是否启用,因为服务依赖cookie作为会话密钥。
通常,服务会话时长为20分钟,需要与客户端协调以提供更好的用户体验来管理会话时间。为了避免用户空闲时间过长时出现HTTP 401(未授权)错误,我们也需要一个客户端会话。以下是客户端会话的工作方式,以实现用户友好的会话管理:
- 客户端会话计时器在用户登录后启动,并在每次安全的Ajax调用返回时重置(因为服务也重置了它的计时器);
- 客户端会话在服务会话超时之前超时。例如,当服务在20分钟后超时时,客户端计时器会在18分钟时超时;
- 当客户端会话在服务会话结束前超时时,它会提示用户其会话即将过期,并提供延长会话或注销的选项,然后启动第二个客户端计时器,该计时器将在90秒后超时;
- 在90秒内,如果用户选择延长会话,客户端会发起一个对服务的调用,比如
KeepAlive
,让服务和客户端都有机会重置各自的会话计时器; - 90秒后,第二个计时器超时,这意味着用户忽略了“即将过期”的提示,然后客户端会代表用户发起一个
logout
服务调用,并以编程方式导航到应用程序的公共区域。
以下是用于登录和启动客户端会话的一些伪代码:
function onBeforeLoginSubmit()
{
if ($('#sohaLoginForm').valid())
{
var loginObj = {};
$("#headerLoginForm").find("input").each(
function () {
loginObj[(this).name] = (this).value;
}
);
$.cbexp.beginWaitFormSubmit("sohaLoginForm");
$.cbexp.postJson(AUTH_SERVICE_PUBLIC_URL +
"/login", loginObj, onLoginResult, onLoginError);
}
return false;
}
function onLoginResult(response)
{
if (response == null || response.status.code != 200)
{
$.cbexp.sohaNavigate("login.html?mode=error");
}
else //authenticated
{
$.cbexp.setSOHASecureToken(response.sohaSecureToken);
$.cbexp.navigateWithToken("home.html");
}
$.cbexp.endWaitFormSubmit("sohaLoginForm");
}
function onLoginError(xhr, ajaxOptions, thrownError)
{
$.sohaError.showServiceError("sohaLoginForm",null, xhr.status);
$.cbexp.endWaitFormSubmit("sohaLoginForm");
}
当 home.html 加载时,它会启动客户端计时器,对于所有受保护的HTML文件,逻辑保持为 true
。
function startSecurePageProcessing()
{
if ($.idleTimeout == undefined)
return;
$.idleTimeout('#idletimeout', '#idletimeout a', {
idleAfter: $.cbexp.idleAfter, //18 minutes
warningLength: $.cbexp.warningLength, // 1 minute
onTimeout: function ()
{
$(this).slideUp();
$.cbexp.logout(null, function () {
$.cbexp.sohaNavigate("login.html?mode=timeout");
});
},
onIdle: function ()
{
$(this).slideDown(); // show the warning bar
},
onCountdown: function (counter)
{
$(this).find("span").html(counter); // update the counter
},
onResume: function ()
{
// Ignored the error handling for server session not refreshed
$(this).slideUp(); // hide the warning bar
$.cbexp.postJson(SESSION_SERVICE_SECURE_URL +
'/keepalive', $.cbexp.getNavTokenPostData())// refresh the server session
}
});
}
这种方法确保了会话时间以一种用户友好且安全的方式进行管理。但在SOHA中,由于不同的页面是用静态XHTML模板构建的,当我们以编程方式从 login.html(公共区域页面)导航到 home.html(受保护区域页面)时,login.html 的DOM被销毁,并为 home.html 重新构建。此时,home.html 可用的所有认证信息只有会话cookie,但客户端代码不关心cookie的值,那么 home.html 如何能假定用户已经登录呢?
浏览器加载安全页面有两种特殊用例:一种是用户将 home.html 的URL添加为书签,在注销或关闭浏览器后,他们尝试直接访问该书签页面,像 home.html 这样的受保护页面不能假定用户已经登录。另一种情况是,用户在一个浏览器标签页中登录,然后在另一个新标签页(同一浏览器)中以不同用户身份登录,当他们切换回第一个标签页时,受保护的XHTML页面不应假定用户仍然通过了身份验证。
在SOHA中,任何受保护的页面,如 home.html,在加载时都绝不能假定用户已通过身份验证。这是SOHA会话管理的导航安全部分;我们将在下一节中详细讨论。
SOHA 导航安全
在SOHA中确保已认证用户安全地在不同页面间导航,是一个独特的架构挑战。因为SOHA是建立在一系列静态
XHTML模板文件之上的,没有服务器端页面的处理管道来帮助验证对静态
资源(此处为HTML文件)的请求是否经过了身份验证和授权。事实上,当浏览器请求一个SOHA中的受保护HTML页面时,该请求从未被验证过,还记得Web服务器只是一个没有运行任何服务器端逻辑的Apache吗(更多细节见第一部分)?Web服务器总是无任何限制地提供所请求的HTML文件。这就需要由SOHA控制器,即每个受保护页面引用的脚本,来确保用户在页面加载时已经过身份验证和授权。
SOHA导航安全的基本思想是,每当一个受保护的页面加载时,它都应该假定用户尚未登录,需要运行一个站点范围的共享安全脚本,通过调用服务来检查身份验证(因为服务设置并验证会话cookie)。如果服务识别了从客户端发送的身份验证信息,它会授权访问;否则,将用户重定向到一个公共区域页面以输入用户凭据。
无论受保护的页面如何加载,包括超链接导航、编程导航、从浏览器历史记录加载以及通过书签访问,这种“未认证”的假设都保持不变。这个假设要求安全脚本在每次加载新的受保护页面时都运行,因此我们需要为所有受保护的XHTML文件禁用浏览器缓存。
为受保护页面禁用浏览器缓存
为了确保安全脚本在每次页面加载时都能运行,包括以编程方式导航、用户输入URL、通过书签访问或刷新页面,需要为所有受保护的页面禁用浏览器缓存。这可以通过在HTML文件中设置以下meta标签来完成:
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Expires" content="0">
上面这种简单的方法在Internet Explorer、Firefox、Chrome中效果很好,但在Mac上的Safari中效果不佳。我们使用以下技巧来解决这个问题,这是一种纯客户端技术,不涉及服务器端页面:它只是注册一个页面卸载事件处理程序,当Safari看到这个事件处理程序时,它会停止从本地缓存加载HTML。
$(window).bind('unload', $.cbexp.unloadingPage);
页面加载时运行安全检查
以下是安全检查的伪代码,它在每次受保护页面加载时运行。它向后端服务请求身份验证信息,然后授权或拒绝访问受保护的页面。
$(function ()
{
if (typeof (preInitPage) == 'function')
preInitPage();
var sohaSecureToken = $.cbexp.getSOHASecureToken();
if (null == sohaSecureToken || undefined == sohaSecureToken)
{
denyAccessThenRedirect("This is a secure page, please login first.");
}
else
{
$.cbexp.postJson(SESSION_SERVICE_SECURE_URL +
'/authenticate', sohaSecureToken, onAuthenticateResult, onAuthenticateError);
}
});
function denyAccessThenRedirect(errorMessage)
{
$.cbexp.prepareRedirect();
alert(errorMessage);
$.cbexp.redirectNewUser();
}
function onAuthenticateResult(response, textStatus, XMLHTTPResponse)
{
if (response.status.code != 200)
{
denyAccessThenRedirect("Your session is invalid,
please login first. (Status Code: " + response.status.code + ")");
}
else
{
$('#pageFooter').load('shared/_footer.html');
$('#pageHeader').load('shared/_header.html', null,
function() {
$.cbexp.initSecurePageHeader();
startSecurePageProcessing();
});
}
}
function onAuthenticateError(xhr, ajaxOptions, thrownError)
{
$.cbexp.showServiceError("authenticate", null, xhr.status);
denyAccessThenRedirect("Your session can not be validated,
please login first.(Status Code: " + xhr.status + ")");
}
如果我们重新审视第一个用例,当用户尝试访问一个已添加书签的受保护页面时,上面的 onDocumentReady
事件处理程序将调用 authenticate
服务。如果用户处于一个有效的会话中(登录后访问书签),服务将以状态码200响应,表示已认证,然后访问被授权,共享的页眉/页脚被加载和初始化,接着执行页面特定的处理方法 startSecurePageProcessing
。当用户在有效会话中刷新页面时,逻辑保持不变。然而,如果用户在注销或浏览器重启后尝试访问书签页面,authenticate
服务将返回非200的状态码(因为cookie无效),我们将用户重定向到一个公共区域的登录页面,并显示消息“您的会话无效,请先登录。”
上述身份验证检查解决了当用户尝试访问已添加书签的受保护页面URL时的问题,它确保了在安全检查结果的基础上拒绝访问并要求用户登录。
现在我们来讨论第二个用例,当不同的用户在一个新的浏览器标签页中登录时,会话cookie被新的cookie替换。当切换回第一个标签页时,之前已认证用户的安全上下文不再有效。SOHA通过一种名为 sohaSecureToken
的技术来检测这种安全上下文的变化,所有源代码都已在上面列出。
使用SOHA安全令牌实现更安全的导航
SOHA安全令牌是除cookie中的会话密钥之外的额外认证信息,它是SOHA中实现更安全的页面间导航所必需的。SOHA安全令牌的想法源于用例2(上一节已讨论),它包含关键的认证信息,其原则是:一个安全的服务会话不能仅通过cookie可靠地识别,我们需要cookie之外的其他信息来保护服务会话。
SOHA安全令牌通过引入一个理念提高了架构的安全性:会话cookie必须与一个安全令牌的值匹配,才能确认请求是安全的。安全令牌的值由服务基于会话密钥通过哈希算法生成,并且服务在用户登录时将它们一起设置。当且仅当会话cookie存在且有效,并且请求中包含了SOHA安全令牌时,服务安全过滤器才能判断用户是否已通过身份验证。当其中任何一个不存在或无效时,它将返回未授权。当两者都存在但不匹配时,它也会响应未授权。只有当安全哈希算法能够将cookie值与SOHA安全令牌匹配时,它才会授权访问安全的服务方法。以下是其工作原理的一些细节:
- 在服务的数据库用户账户表中,为每个用户在账户初始创建时存储一个“盐”值;
- 当用户成功登录时,服务建立一个会话,将会话密钥设置为哈希后的用户ID,并将其设置在一个会话cookie中;
- 在登录方法返回给客户端之前,它会取“盐”值和会话密钥,通过一个安全的哈希算法生成SOHA安全令牌的值,然后将该令牌值设置在响应体的
sohaSecureToken
字段中,以状态码200返回给客户端; - 当登录成功的响应到达客户端时,
onLoginResult
事件处理程序(在上面的源代码中列出)将获取sohaSecureToken
的值,并通过调用$.cbexp.setSOHASecureToken
将其存储在客户端; - 当需要安全地导航到一个新URL时,客户端代码调用
$.cbexp.navigateWithToken(newPageBaseURL)
,这个方法会接收baseURL
,然后将sohaSecureToken
附加到查询字符串中。例如,$.cbexp.navigateWithToken("home.html")
的结果是windows.location.href = "home.html?sohaSecureToken=1m0z3n4g6d7hji90";
至此,登录操作在服务端和客户端都已完成,浏览器发出对home.html的请求,Web服务器会毫不犹豫地将其提供下来。当浏览器接收到 home.html 中的XHTML模板时,我们的SOHA无缓存机制将在页面加载时触发安全检查脚本(如上所列)的执行,该脚本会从查询字符串中抓取 sohaSecureToken
。如果在查询字符串中没有找到任何内容,它会将用户重定向并要求登录;否则,它会调用服务方法authenticate。(更多细节请参见上面的源代码)。
当浏览器发出 authenticate
服务调用时,它会附加上会话cookie(由 login
响应设置),并且请求体中也包含 sohaSecureToken
。当 authenticate
服务调用到达应用服务器上的服务安全过滤器时,将发生以下情况:
- 如果会话cookie不存在,返回未授权(已添加书签的受保护页面用例);
- 如果
sohaSecureToken
不存在,返回未授权(直接请求没有令牌的受保护资源用例); - 如果会话cookie无效(被嗅探或篡改的cookie值用例)或未与任何会话关联(会话已超时用例),返回未授权;
- 当会话cookie有效时,从关联的会话状态中检索用户的盐值;
- 取会话cookie值和用户盐值,运行安全哈希算法。如果结果不等于请求体中的
sohaSecureToken
值,返回未授权(浏览器第二个标签页登录不同用户用例,本质上是cookie值或sohaSecurToken
值被篡改的用例); - 否则,会话cookie和
sohaSecureToken
都有效且匹配,返回状态码200,告知客户端用户已登录。
现在 authenticate
服务调用返回到客户端,可以安全地加载页眉、页脚以及其他受保护页面的特定处理流程,用户数据可以被安全地检索来构建DOM。
安全的导航超链接
到目前为止,我们对以编程方式进行导航的安全性有了更强的信心,也可靠地处理了页面刷新、书签页面以及cookie或URL被篡改的情况。但SOHA的安全考虑并未止步于此,因为用户还可以通过点击XHTML模板中的链接进行导航。如果这些链接是用户特定内容的一部分,它将由页面构建逻辑处理,通过在其查询字符串中添加 sohaSecureToken=[用户的soha安全令牌值]
来实现。但是,共享HTML部分中的链接该如何处理呢?特别是共享页面页眉中的导航链接?那些 a 标签的 href 只指向基础URL,比如 userPageOne.html,因为它是一个静态
的共享HTML文件,它根本不知道SOHA安全令牌的值。
有两种方法可以确保页面链接能够安全导航。第一种方法是完全移除所有 a
标签的 href
属性,然后绑定一个 click
事件处理程序。在 click
事件处理程序中,可以调用 $.cbexp.navigateWithToken(newPageBaseURL)
来确保令牌在查询字符串中。然而,这种方法的两个缺点促使我寻找更好的解决方案。
第一个缺点是 click
事件处理程序不是通用的,需要编写大量脚本来连接超链接,容易出错。即使事件处理程序可以以通用的方式管理,你仍然需要从触发事件的 a
标签中找出目标URL;第二个缺点是用户会失去一些浏览器的默认导航功能,比如悬停在超链接上查看目标URL,按住Ctrl+点击在新标签页中打开而不是替换当前浏览器窗口内容等。
一种更好的确保页面超链接导航安全的方法是修补每个安全 a
标签的 href
值,这可以很容易地通过HTML和JavaScript实现,并且保留了浏览器的默认导航行为。唯一的技巧是如何识别指向受保护页面的链接,因为我们不想修补任何链接到公共区域页面的 href
。我们通过简单地给共享HTML内容中的每个 a
标签添加一个类(名为“sohaSecureLink
”)来解决这个问题,然后使用 jQuery
的类选择器轻松地将它们全部修补。例如,页眉导航链接的HTML可能看起来像这样:
<div id="headerNavigation">
<ul class="topNavWrapper">
<li><a href="home.html" class="sohaSecureLink">Home</a></li>
<li><a href="useraccounts.html" class="sohaSecureLink">Accounts</a></li>
<li><a href="useractions.html" class="sohaSecureLink">Actions</a></li>
<li><a href="userassets.html" class="sohaSecureLink">Assets</a></li>
<li><a href="useralerts.html" class="sohaSecureLink">Alerts</a></li>
<li><a href="offerts.html">Offers</a></li>
<li><a href="contactus.html">Contacs</a></li>
</ul>
</div>
以下是修补它们的脚本:
patchSecureHrefs: function ()
{
$("a.sohaSecureLink").each(function ()
{
var href = $(this).attr("href");
if (href.indexOf($.cbexp.getSOHASecureTokenQueryString()) < 0)
$(this).attr("href", href + $.cbexp.getSOHASecureTokenQueryString());
});
}
注意导航列表中的最后两项没有 class="sohaSecureLink"
属性,它们是公共区域页面,其 href
不会被上述脚本修改。修补后DOM中的 headerNavigation
HTML的最终结果会是这样:
<div id="headerNavigation">
<ul class="topNavWrapper">
<li><a href="home.html?sohaSecureToken=1m0z3n4g6d7hji90"
class="sohaSecureLink">Home</a></li>
<li><a href="useraccounts.html?sohaSecureToken=1m0z3n4g6d7hji90"
class="sohaSecureLink">Accounts</a></li>
<li><a href="useractions.html?sohaSecureToken=1m0z3n4g6d7hji90"
class="sohaSecureLink">Actions</a></li>
<li><a href="userassets.html?sohaSecureToken=1m0z3n4g6d7hji90"
class="sohaSecureLink">Assets</a></li>
<li><a href="useralerts.html?sohaSecureToken=1m0z3n4g6d7hji90"
class="sohaSecureLink">Alerts</a></li>
<li><a href="offerts.html">Offers</a></li>
<li><a href="contactus.html">Contacs</a></li>
</ul>
</div>
由于 sohaSecureToken
的值是由 login
服务响应设置的,每个登录的用户将共享相同结构的页眉HTML,但每个受保护超链接的 href
中的查询字符串不同。无需大量修改HTML和脚本,用户就可以安全地导航到公共或受保护区域的页面。
除了会话计时器协调,我们还解决了SOHA在安全性方面的独特导航挑战。上述技术将使已认证的用户保持在其关联的会话中,他们可以在受保护的页面之间以更高的安全级别(与仅由cookie识别会话的模式相比)进行导航。
当用户浏览每个静态HTML文件(XHTML模板)时,新页面加载时DOM会被重建;之前保存的用户数据(脚本中的变量)将会丢失。如果用户在一个页面输入了一些数据,而这些数据需要在发送回服务进行持久化之前,在新的页面中进行进一步处理或显示,我们该如何实现页面间的数据传输呢?这是SOHA会话管理的客户端数据缓存方面;下一节将详细讨论。
SOHA 数据共享
除了导航安全,在不同页面间共享数据是SOHA中的另一个独特挑战。常规的由Flex/Silverlight/JavaFX构建的RIA完全没有这个问题,因为在大多数情况下,整个应用程序是在运行前下载到客户端的一个SWF或XAP文件,客户端会话数据存储在应用程序数据模型中,对每个不同的应用程序视图始终可用。即使在模块化应用场景中,模块在运行时按需下载,它仍然可以访问存储在外壳应用程序中的数据模型。而在SOHA中,页面间的导航会导致DOM重建,没有可供新页面访问的应用程序数据模型。
首先想到的解决方案可能是要求服务为新加载的页面检索数据,因为它有会话cookie和SOHA安全令牌,可以检索与用户相关的任何数据。但这样做会使服务不再与表示层无关,因为“页面”的概念只属于表示层;仅根据加载的页面来获取数据会将服务API与导航流程绑定在一起。这违反了SOHA的原则,即服务应与表示层无关,并且可以同时为不同的客户端应用程序提供服务,而无需了解任何关于导航流程的设计。此外,对于一些公共数据,如多个页面都需要的用户名、电话号码等,这种“导航时请求服务检索页面数据”的方法会浪费带宽,因为所有这些共享数据在所有页面中都是相同的。
因此,解决方案需要在客户端实现。对于小块数据,可以通过查询字符串共享,但这是公开的,任何人都能看到。通常,我们不希望将任何用户特定数据以明文形式放在查询字符串中。
另一种方法是使用脚本在客户端读/写cookie来存储共享数据。然而,cookie通常有4k的大小限制,当共享数据增长时并不可靠。更重要的是,这也违反了SOHA的另一个原则,即cookie只由服务设置并由服务使用。避免客户端代码访问和使用cookie有助于消除未来客户端和服务器之间可能出现的交互问题。
解决方案是利用浏览器内置的本地存储。这种方式可以防止将不完整的信息发送回服务,并通过避免为常见的共享用户数据打扰服务来节省带宽。jQuery的jStoreage插件利用了HTML5的本地存储,并在Internet Explorer(自IE6起)中使用userData来实现客户端数据缓存。即使在一些旧版浏览器(IE6+, Firefox 2+等)中,它也能很好地工作,iPhone Safari和Google Chrome也支持得很好。但它也有一些注意事项。
首先是 jStoreage插件 在Opera 10.1和Safari 3中不起作用,它不支持WebKit SQLite API。另一个需要注意的是,即使浏览器支持 localStorage
,用户也可以禁用它。由于SOHA依赖本地存储进行数据缓存和共享,我们需要在应用程序的着陆页加载时检测存储是否已启用,就像我们检测JavaScript(因为它是我们的控制器,并且我们是基于jQuery构建的)和cookie(因为服务依赖它进行认证)一样。以下是一些检测本地存储的代码:
isStorageEnabled: function ()
{
var testKey = "cbexp_storage_detector_key";
var testVal = "cbexp_storage_test";
$.jStorage.set(testKey, testVal);
var retVal = (testVal == $.jStorage.get(testKey));
$.jStorage.deleteKey(testKey);
return retVal;
}
当本地存储启用时,第一个加载的受保护页面可以从服务中检索常见的共享用户数据,然后将其缓存在本地存储中。当其他受保护页面加载时,经过身份验证后,它可以尝试从存储中检索数据。如果数据存在且有效,那么在会话期间就无需再次调用服务获取相同的数据。只需记住,当用户注销或关闭浏览器时,我们需要清除缓存的数据。
SOHA Ajax 扩展
面向服务的HTML应用程序本质上是Ajax的扩展,它使得构建一个无需服务器端页面且不需要浏览器插件的丰富交互式HTML应用程序成为可能。它不仅通过 $.cbexp.postJason
封装 $.ajax
API(更多细节请参见Web App Common Tasks by jQuery)来与服务进行交互,还通过异步加载HTML($.cbexp.loadDivHTML
)、CSS($.cbexp.loadPageCSS
)和JavaScript文件($.cbexp.loadPageScript
)来完全通过客户端逻辑管理动态的用户偏好和体验。这个主题在上一篇文章中已有详尽的介绍。
上文讨论的安全检查脚本加上SOHA安全令牌机制,确保了所有的Ajax调用和用户通过静态
XHTML文件的导航都能以一种安全且用户友好的方式进行。在SOHA中,Ajax不仅在页面级别上操作,用于页面区域的数据交互,它还在应用程序级别上发挥作用,用于创建和管理用户体验和应用程序安全。大多数常见任务,如客户端母版页、客户端用户控件、一致的页面布局和外观、条件内容、导航逻辑和重定向过程,都变得简单且易于管理。
总结
重申:Web标准、服务和Ajax是SOHA(面向服务的HTML应用程序)的推动者。它利用HTML作为模型模板,CSS作为视图,JavaScript作为控制器,并利用Ajax与Web服务进行交互。它推崇Web标准,通过**消除传统的Web服务器页面(JSP, ASP, PHP, Ruby On Rail, ColdFusion等)**实现高可扩展性,并且**无需浏览器插件(Flash/Flex, Silverlight, JavaFX等)**即可实现丰富的交互性。
SOHA的关键概念和原则是:
- 高可扩展性的Web层:服务器不生成HTML,Web层只提供静态文件;
- 最大化应用程序的丰富性:不依赖浏览器插件,对下一代Web标准(HTML5, CSS3等)开放,所有浏览器和支持互联网的设备均可访问;
- 服务逻辑和数据访问层保持与表示层无关;
- 利用客户端处理能力:应用程序视图将在客户端生成,表示层无需服务器端应用程序逻辑,丰富的视觉效果和交互性通过Web标准实现;
- SOA:所有动态数据都通过Ajax从数据服务中检索,客户端/服务器交互通过Ajax调用执行,而不是回传到页面本身;
- 清晰的关注点分离:服务逻辑只关注业务逻辑而非渲染需求;Web服务器只提供静态的HTML/CSS/JS文件;客户端控制器(JS)处理用户交互和Ajax调用,并操作DOM;所有应用程序视图由CSS控制,静态HTML作为模型模板;
- 简化的应用程序常见任务:动态CSS/脚本/HTML加载、安全的基于静态HTML的导航、静态页面书签和重定向、页面间数据传输、扩展Ajax API等。
- 实用性:这种新模型和架构已应用于真实的Web应用程序项目。