seam2.1权限验证(15.3) JAAS
羊舌诚
2023-12-01
15.3. 验证Seam安全中的验证特性是基于JAAS (Java Authentication and Authorization Service)开发的,它提供了用来进行用户身份认证的高度可配置的接口。然而,针对复杂多变的验证需求,Seam提供了一套非常简单的验证方法来隐藏JAAS的复杂性。
15.3.1. 配置一个验证组件注意: 如果你使用Seam的身份管理功能(稍后介绍),那么就不用特地建立一个验证组件(意味着你可以跳过这一章)。
这种简单的验证方法由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}绑定到验证组件的验证方法上,该方法被用来对登录的用户进行验证。
15.3.2. 编写验证方法components.xml文档中identity的authenticate-method属性指出SeamLoginModule将使用哪个方法来进行用户验证。该方法没有参数,并且返回值为boolean类型,用于判断登录是否成功。用户名和密码可以分别从Credentials.getUsername()和Credentials.getPassword()得到。用户所属的角色通过Identity.addRole()来添加。下面就是一个写在POJO组件中的完整验证方法:
@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都是实体。 roles属性包含了用户所拥有的角色,这个属性必须是一个字符串组成的Set,例如“admin”、“user”等。上面的程序中,如果符合条件的用户记录没有找到,那么抛出NoResultException 异常,验证失败,登录失败。
提示:当编写验证方法的时候,必须保证这个方法为最简的,因为Seam安全无法保证这个方法会被调用多少次,同一个请求中它可能被调用多次。因此,任一判断成功或失败之外的代码,应该写成实现an event observer。想要知道Seam安全所提交事件的更多信息,请参考后面的安全事件一章。
15.3.2.1. Identity.addRole()Identity.addRole()方法会根据当前session是否有效来执行不同的任务。如果当前session未经过验证,那么addRole()只会在验证过程中被调用。该方法被调用的时候,角色名称会被加入pre-authenticated的临时角色列表。一旦验证成功,pre-authenticated中的临时角色会变为真正的角色,并且在调用Identity.hasRole()的时候,会返回true。The following sequence diagram represents the list of pre-authenticated roles as a first class object to show more clearly how it fits in to the authentication process.
如果当前session通过验证,然后调用Identity.addRole(),就会立即将指定的角色赋予当前用户。
15.3.2.2. 编写一个与安全事件相关的observer让我们来讨论一个实际例子。当登录成功以后,一些用户数据需要被更新。这个功能可以通过编写一个事件观察器的方式来实现,通过监听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()就会使用credentials对用户进行权限验证。下面就是一个简单的登录表单例子:
<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}。调用这个方法会清除当前登录用户的登录状态,并且让当前用户的session失效。
15.3.4. 配置概述总的来说,配置登录验证,只需要三个步骤:
1、在components.xml中设置登录验证方法
2、编写一个登录验证方法
3、编写一个登录表单让用户可以有地方登录
15.3.5. 记住我Seam安全框架提供了一个类似“记住我”的功能,这个功能我们经常能在其他网站上看见。这个功能实际上是提供了两种不同的习惯或者模式。
第一种模式,浏览器将用户名通过cookie的方式保存下来,只保留密码文本框让用户填写(市面上常见的浏览器都提供了保存密码的功能)。
第二种模式,在cookie中保留用户的唯一性认证信息,允许用户下次登录网站的时候不用输入密码就能够自动通过登录权限验证。
警告
使用客户端的cookie记录用户信息,从而实现自动登录的方式是非常危险的。虽然你方便了客户,但是同时,你站点上的任何一个跨站点脚本安全漏洞都可能会造成严重的后果。如果没有登录验证cookie,那么黑客唯一能够通过XSS偷取的信息就只剩下当前用户的session了。这就意味着,只有当用户登录网站创建了一个session的一小段时间才可能被攻击。如果黑客能够通过“记住我”这个功能从保存在客户端的cookie中得到用户的帐号信息,那么他以后随时都能够以客户的身份通过登录验证。当然,你需要知道,前面说的这些攻击能否实现,也取决于你对XSS攻击做了哪些防范。如果你能够确保你的网站100%防御了XSS攻击,你完全可以让用户输入的数据全部都显示在页面上。只要你能够实现,这绝对是意见了不起的成就。
现在,几乎所有浏览器厂商都认识到了这个问题,并且提供了一个“保存密码”的功能。浏览器会为网站的一些页面,或者是一个domain保存用户的用户名和密码。这样用户在登录网站的时候,即使session未经过登录验证,浏览器也会自动将用户名密码填入指定的登录表单。网站的设计者可以放一个登录快捷键在页面上,然后让浏览器去记录用户名和密码,这样实现类似“记住我”的功能会更加安全。一些浏览器(例如Safari和OS X)甚至能够将用户的登录数据加密并保存在操作系统的密码表中。如果是在网络环境下,这个密码表还可以跟随用户到任何地方(例如从用户的台式机到笔记本)。而cookie在通常情况下是无法实现同步的。
总结:你最好在任何情况下都不要使用cookie来实现自动登录验证。不过,仅仅使用cookie来记住用户名,并且自动将保存的用户名填写进入表单中,就不会有太大的问题,同时也能在一定程度上方便用户。
如果想要默认打开“记住我”的功能(仅仅记住用户名),你不需要做任何额外的配置。只需要在登录表单中将记住我的复选框绑定到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实现将验证需要用到的所有特征数据存储在数据库的一个表中。
首先你需要创建一个新的包含了tokens的Entity。下面这个例子展现了一种你可能用到的结构:
@Entitypublic 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,它们用来指出实体的username和特征属性。这些注解标识了实体中需要用来验证的所有特征数据。
下一步就是配置JpaTokenStore,使用entity bean来存储或读取验证特征。具体的操作为,在components.xml中加入一个token-class属性:
<security:jpa-token-store token-class="org.jboss.seam.example.seamspace.AuthenticationToken"/>
然后,在components.xml加入RememberMe设置,并且将模式设置为autoLogin:
<security:remember-me mode="autoLogin"/>
通过上面这些设置,用户每次访问你的网站的时候就可以自动登录了(只要用户点击了“记住我”复选框)。
15.3.6. 处理安全异常在登录的时候难免出现异常,为了防止用户看见默认的错误页面,我们建议你在pages.xml里面配置一下页面自动跳转设置,这样就能够在出错的时候自动跳转到一些比较“漂亮”的页面上。最常见的两种安全异常为:
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 actionmessage> 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>
大部分web应用程序会需要更多的跳转规则,用以处理各种实际业务逻辑。所以Seam包含了一些特殊的功能用来处理这些问题。
15.3.7. 登录重定向当用户在没有登录的情况下试图访问某个需要登录才可以访问的页面(或者是一组用通配符指定的页面)的时候,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 Basic或HTTP Digest (RFC 2617)方式的验证。不过除非必要,我们不建议使用这些方式。在components.xml文件中,我们需要进行以下配置:
<web:authentication-filter url-pattern="*.seam" auth-type="basic"/>
如果需要使用digest验证方式,key和realm也需要设置一下:
<web:authentication-filter url-pattern="*.seam" auth-type="digest" key="AA3JK34aSDlkj" realm="My App"/>
key属性可以是任意值。realm为用户在登录的时候需要告诉用户的realm名称。
15.3.8.1. 编写Digest验证器当使用digest验证方式的时候,你的验证类需要集成自抽象类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 Security API提供的简化了的JAAS配置,那么你可以将其委托给系统默认的JAAS配置。你可以通过在components.xml配置一个jaas-config-name来实现。例如,如果你使用JBoss AS,并且希望使用其他的验证策略(例如使用JBoss AS提供的 UsersRolesLoginModule 登录模块)。你需要这么设置:
<security:identity jaas-config-name="other"/>
需要记住的是,通过上面这些配置,并不是让用户验证交给了部署Seam应用程序的容器。它仅仅是告诉Seam安全框架使用在容器中配置的JAAS安全规则来验证用户。