15.1. 概述
Seam安全API为你基于Seam的应用程序提供了众多的安全相关的功能,涵盖的区域有:
*认证——一个可扩展的基于JAAS的认证层,允许用户根据任何安全提供者进行认证。
*身份管理——一个在运行时间管理Seam应用程序的用户和角色的API。
*授权——一个极其全面的授权框架,支持用户角色、持久化、基于角色的许可和容易实现自定义安全逻辑的可插式许可解析器。
*许可管理——一套内建Seam组件,方便管理应用程序的安全政策。
* CAPTCHA(completely automated public Turing test to tell computers and humans apart )支持——协助预防自动化软件/脚本滥用你的基于Seam的站点。
*还有更多
本章将涵盖所有这些功能的细节。
15.2. 禁用安全
在一些情况下,禁用Seam安全可能是必要的,例如,在单元测试期间。通过调用静态方法Identity.setSecurityEnabled(false)禁用安全检查。这样做可以防止下列任何安全检查被执行:
* Entity Security——实体安全
* Hibernate Security Interceptor——Hibernate安全拦载器
* Seam Security Interceptor——Seam安全拦载器
* Page restrictions——页面约束
15.3. 认证
认证功能通过建立在JAAS上的Seam安全提供,因此,提供了强大的和高度可配置的API来处理用户认证。然而,对不太复杂的认证要求,Seam提供了一个更简单的认证方法,隐蔽了JAAS的复杂性。
15.3.1. 配置authenticator组件
注意:如果你使用了Seam的身份管理功能(在后面的章节讨论),那么创建authenticator组件是没有必要的(你可以跳过这节)。
简化的认证方法通过Seam 使用一个内建的JAAS注册模块提供,SeamLoginModule, 它委托认证给你自己的一个Seam组件。这个注册模块已经被配置在Seam内部,作为一个缺省的应用程序策略,并且因此不需要任何额外的配置文件。它允许你用自己的应用程序提供的实体类编写一个认证方法,或者作为选择,使用其它一些第三方提供商的认证。配置这个简单的认证形式要求在components.xml配置identity组件:
<components xmlns="http://jboss.com/products/seam/components"
xmlns:core="http://jboss.com/products/seam/core"
xmlns:security="http://jboss.com/products/seam/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://jboss.com/products/seam/components http://jboss.com/products/seam/
components-2.1.xsd
http://jboss.com/products/seam/security http://jboss.com/products/seam/security-2.1.xsd">
<security:identity authenticate-method="#{authenticator.authenticate}"/>
</components>
EL表达式 #{authenticator.authenticate}是一个方法捆绑,表示authenticator 组件的authenticate方法将被用来认证用户。
15.3.2. 编写一个认证方法
在components.xml为identity指定的authenticate-method属性指定那个方法,会被SeamLoginModule用来认证用户。这个方法不使用参数,并且预计返回一个布尔值,用来指明认证是否成功。用户名和密码能分别能从Credentials.getUsername() 和 Credentials.getPassword()获得。用户所属的任何角色都应该利用 Identity.instance().addRole() 分配。 这里是在一个POJO(Plain Old Java Object)组件内部的一个认证方法的完整实例:
@Name("authenticator")
public class Authenticator {
@In EntityManager entityManager;
@In Credentials credentials;
@In Identity identity;
public boolean authenticate() {
try {
User user = (User) entityManager.createQuery(
"from User where username = :username and password = :password")
.setParameter("username", credentials.getUsername())
.setParameter("password", credentials.getPassword())
.getSingleResult();
if (user.getRoles() != null) {
for (UserRole mr : user.getRoles())
identity.addRole(mr.getName());
}
return true;
}
catch (NoResultException ex) {
return false;
}
}
}
在上面这个例子里,User 和 UserRole两个都是特定应用程序的实体bean。roles 参数以用户所属的角色填充,它应该添加到 Set作为文字型字符串值,如“admin”、“user”。在这个案例,如果用户记录没有被找到,则NoResultException抛出,认证方法返回false指明认证失败。
技巧:当编写一个认证方法时,保持最小和没有副作用是重要的。这是因为,谁也不能保证认证方法会被安全API调用多少次,并且同样也不能保证它在单一请求期间可能被调用多少次。正因为此,将要执行一个成功或失败的任何特殊代码,应该通过实现一个事件观察器进行编写。有关被Seam安全触发事件的更多信息,请看Security Events再后一些的章节。
15.3.2.1. Identity.addRole()
Identity.addRole() 方法不同地表现,取决于当前会话是被认证了还是没有被认证。如果会话没有被认证,那么addRole()在认证处理期间只应被调用。当在这里调用时,角色名称被放进一个预先认证角色的临时列表中。一旦认证成功,预先认证角色就会变成“真实的”角色,并且对这些角色调用Identity.hasRole(),然后将返回true。下列次序图表演示了作为首类对象的预先认证角色列表,很清楚地显示出它如何适应认证过程。
如果当前会话已被认证,那么调用Identity.addRole(),会有预期结果,立即准许指定的角色成为当前用户。
15.3.2.2. 为安全相关的事件编写一个事件观察器
例如说,一个成功注册, 一些用户统计表必须被更新。这可能做到,通过为org.jboss.seam.security.loginSuccessful事件编写一个事件观察器,象这样:
@In UserStats userStats;
@Observer("org.jboss.seam.security.loginSuccessful")
public void updateUserStats()
{
userStats.setLastLoginDate(new Date());
userStats.incrementLoginCount();
}
这个观察器能被放在任何地方,甚至在认证器组件本身。在后现章节,你能找到更多有关安全相关事件的信息。
15.3.3. 编写一个注册表单
credentials组件提供了username和password两个属性, 满足最普遍的认证情节。这些属性能被直接捆绑在一个注册表单上的用户名和密码字段。一旦这些属性被设置,调用identity.login()会使用提供的证书认证用户。这里是一个简单注册表单的例子:
<div>
<h:outputLabel for="name" value="Username"/>
<h:inputText id="name" value="#{credentials.username}"/>
</div>
<div>
<h:outputLabel for="password" value="Password"/>
<h:inputSecret id="password" value="#{credentials.password}"/>
</div>
<div>
<h:commandButton value="Login" action="#{identity.login}"/>
</div>
相似的,注销用户通过调用#{identity.logout}实现。调用这个动作会清除当前认证用户的安全状态,并且废止用户会话。
15.3.4. 配置摘要
所以总结起来,配置认证有三个容易的步骤:
* 在components.xml中配置一个认证方法.
* 编写一个认证方法。
* 编写一个注册表单,使用户可能验证。
15.3.5. 记住我
Seam安全支持在许多在线的基于网页的应用程序中普遍遭遇的一样的“记住我” 功能。它实际上支持两种不同的“口味”或者模式——第一种模式允许用户名存储在用户的浏览器作为一个cookie,并且丢弃输入到浏览器的密码(大多数现代浏览器能够记着密码)。
第二种模式支持存储一个唯一的令牌在一个cookie中,并且允许一个用户自动认证近回到站点,不必提供一个密码。
警告:用一个存储在客户机器上的持久化cookie 自动地客户认证是危险的。在方便用户的同时,在你的网站上的任何跨站点脚本安全漏洞将会比平常有更严重的影响。不用认证cookie,对一个用XSS(跨站脚本攻击)的攻击者而言,窃取的唯一cookie是一个用户当前会话的cookie。这意味着仅当用户有一个打开的会话时——应该是一个很短的时期,攻击才会起作用。然而,如果一个攻击者有可能窃取一个持久化“记住我”cookie,允许他在任何时候不用认证注册,这是更具有吸引力和危险的。注意,这一切取决于如何更好地保护你的网站反对XSS攻击——确保您的网站是100 %的XSS安全是看你的了——对允许用户输入被渲染在一个页面的任何网站的一个不平凡的成绩。
游览器供应商认识到这个问题,并且引入了一个“记住密码”功能——今天几乎所有的游览器都支持这个。这里,游览器记着一个特殊网站和域的注册用户名和密码,并且当你同这个网站没有一个活动的会话时,自动填写登录表单。如果你作为一个网站设计者,那么提供一个方便的注册快捷键,这个方法如“记住我”一样方便并且更安全。一些浏览器(如Safari on OS X)甚至用加密全球操作系统keychain(键链)来存储注册表单数据。或者一个网络环境下,当浏览器cookies 通常不同步时,keychain能与用户一起被传送(例如膝上型和桌面电脑之间)。
总结:当每个人做它,用自动化认证持久化“记住我”不是一个好的实践,并且应该不要使用。只“记住着”用户注册名,并且使用用户名填写注册表单作为一个方便,不是一个问题。
对缺省模式,“记住我”功能是可用的,不需要专门的配置。在你的注册表单,简单地捆绑“记住我”复选框用rememberMe.enabled,象下面的例子:
<div>
<h:outputLabel for="name" value="User name"/>
<h:inputText id="name" value="#{credentials.username}"/>
</div>
<div>
<h:outputLabel for="password" value="Password"/>
<h:inputSecret id="password" value="#{credentials.password}" redisplay="true"/>
</div>
<div class="loginRow">
<h:outputLabel for="rememberMe" value="Remember me"/>
<h:selectBooleanCheckbox id="rememberMe" value="#{rememberMe.enabled}"/>
</div>
15.3.5.1. 基于令牌的“记住我”认证
为了使用自动的基于令牌模式的“记住我”功能,你必须首先配置一个令牌库。最普通的情节是存储这些认证令牌在一个数据库内(Seam支持的),然而,通过实现org.jboss.seam.security.TokenStore接口实现你自己的令牌库是可能的。这一节假设你会使用提供的JpaTokenStore实现来存储认证令牌在一个数据库表内部。
第一步是创建一个新的将包含令牌的实体。下面例子显示了你可能使用的一个可能的结构:
@Entity
public class AuthenticationToken implements Serializable {
private Integer tokenId;
private String username;
private String value;
@Id @GeneratedValue
public Integer getTokenId() {
return tokenId;
}
public void setTokenId(Integer tokenId) {
this.tokenId = tokenId;
}
@TokenUsername
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@TokenValue
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
与你可能从这个列表看见的一样,一对特殊的注释,@TokenUsername和@TokenValue被用来配置用户名和实体的令牌属性。对将包含认证的令牌的实体而言,这些注释是必要的。
下一步是配置JpaTokenStore,为使用这个实体bean来存储和检索认证令牌。在components.xml中通过指定token-class属性来实现:
<security:jpa-token-store token-class="org.jboss.seam.example.seamspace.AuthenticationToken"/>
一旦这样做了,最后要做的事,也是在components.xml配置RememberMe组件。其mode应设置为autoLogin:
<security:remember-me mode="autoLogin"/>
这就是所有必需的了——当用户重新访问你的站点时,自动认证现在会发生(他们不再选中“记住我”复选框)。
15.3.6. 处理安全异常
为了防止用户收到的默认的针对一个安全错误的错误页面,pages.xml配置重定向到一个更“完美的”页面是被推荐。被安全API抛出的异常有两种主要类型:
* NotLoggedInException ——当用户不注册,企图访问一个受限动作或页面,这个异常发生。
* AuthorizationException ——如果用户已经注册,并且他们企图访问他们没有必要的特权的一个受限动作或页面,这个异常发生。
在NotLoggedInException情况下,用户重定向到一个登录或注册页面以便于他们能注册是被推荐的。对AuthorizationException重定向用户到一个错误页面可能更有用。这里是一个pages.xml文件例子,重定向两种安全异常:
<pages>
...
<exception class="org.jboss.seam.security.NotLoggedInException">
<redirect view-id="/login.xhtml">
<message>You must be logged in to perform this action</message>
</redirect>
</exception>
<exception class="org.jboss.seam.security.AuthorizationException">
<end-conversation/>
<redirect view-id="/security_error.xhtml">
<message>You do not have the necessary security privileges to perform this action.</message>
</redirect>
</exception>
</pages>
大多数网页应用程序需要更先进的登录处理重定向,所以Seam包括一些特殊的功能来处理这一问题。
15.3.7. 注册重定向
当一个未认证的用户企图访问一个特殊视窗(或者通配符视窗id) ,你可经请求Seam重定向用户到一个登录屏幕,如下面:
<pages login-view-id="/login.xhtml">
<page view-id="/members/*" login-required="true"/>
...
</pages>
技巧:这是一个比展示在上面的异常处理稍利的工具,但是也许应该联合使用它。
在用户登录后,我们想自动送他们回到他们来的地方,这样他们能重试要求注册前的动作。如果你增加了下列侦听器到components.xml,企图访问一个受限而没有登录的视窗会被记住,因此,一旦用户登录成功,他们将被重定向到原先请求的视窗,带着一些存在于原来请求的页面参数。
<event type="org.jboss.seam.security.notLoggedIn">
<action execute="#{redirect.captureCurrentView}"/>
</event>
<event type="org.jboss.seam.security.postAuthenticate">
<action execute="#{redirect.returnToCapturedView}"/>
</event>
注意:登录重定向根据一个对话范围机制实现,所以,在你的authenticate()方法中的对话不会被结束。
15.3.8. HTTP 认证
虽然不建议使用,除非绝对必要,Seam提供了使用基于HTTP或HTTP分类 (RFC 2617)方法的认证方法。为了使用任一的认证形式,认证过滤器组件必须在components.xml被激活:
<web:authentication-filter url-pattern="*.seam" auth-type="basic"/>
为了激活基本认证过滤器,设置auth-type为basic,或者分类认证,设置它为digest。如果使用分类认证,key和 realm也必须设置:
<web:authentication-filter url-pattern="*.seam" auth-type="digest" key="AA3JK34aSDlkj" realm="My App"/>
key可能任何字符串值,realm是当用户认证时呈现给他们的认证领域名。
15.3.8.1. 编写一个分类认证
如果使用分类认证,你的认证器类应该扩充抽象类org.jboss.seam.security.digest.DigestAuthenticator,并且根据分类请求使用validatePassword()方法验证用户的纯文字密码,这里是一个例子。
public boolean authenticate()
{
try
{
User user = (User) entityManager.createQuery(
"from User where username = :username")
.setParameter("username", identity.getUsername())
.getSingleResult();
return validatePassword(user.getPassword());
}
catch (NoResultException ex)
{
return false;
}
}
15.3.9. 高级认证功能
这节探讨一些安全API提供的处理更复杂的安全要求的高级功能
15.3.9.1. 使用你容器的JAAS配置
如何你不愿意使用Seam安全 API提供的简单的JAAS配置,你可以在components.xml中提供jaas-config-name属性替换缺省的系统JAAS配置代理。例如,如果你正在使用JBoss AS,并且希望使用其它策略(使用JBoss AS提供的UsersRolesLoginModule登录模块),那么,在components.xml中的入口看起象这样:
<security:identity jaas-config-name="other"/>
请记住,这样做并不意味着你的用户会被认证在任何一个你的Seam应用程序部署的容器。它只是指示Seam安全使用配置的JAAS安全策略认证它自己。
mar