by Shane Bryzak,翻译: JSF中文技术文摘
Article permalink: http://shane.bryzak.com/articles/seam_security_gets_an_upgrade
即将到来的 JBoss Seam 2.1.0.GA 发布版本将包含一些新的功能和增强的安全特性,这些安全特性包括 Identity Management, 基于ACL的权限, 和 Permission Management, 还有强类型(strongly-typed)的安全注解。在这篇文章中,我将结合在SeamSpace示例(在Seam发布包的/examples/seamspace目录下)来讲解下这些新的安全特性。
如果你想自己来运行下SeamSpace示例程序,下面有些建议来指导你如何做:
1) 首先,确保你安装了JDK 1.5,还有 Apache Ant(最近发布版本)
2) 下载并安装 JBoss AS 4.2.2, 下载地址: http://www.jboss.org/jbossas/downloads/
3) 下载并且解压缩Seam的2.1每日建构版本,下载地址: http://www.seamframework.cn/Download (如果你具有探索精神,可以从SVN中检出最新版本,地址:
http://anonsvn.jboss.org/repos/seam/trunk/)
4) 编辑Seam目录中的 build.properties 文件,修改你的 JBoss AS 安装目录
5) 导航到 examples/seamspace 目录,运行 'ant'
6) 在 JBoss bin目录下运行run脚步来启动JBoss AS
7) 在 JBoss AS 启动后,打开 http://localhost:8080/seam-space
现在我们开始看看新的 Identity Management 特性。
Identity Management
那么什么是 Identity Management 呢? 直到现在,Seam只提供了使用内建的组件来做用户验证(Identity 组件)。Seam没有提供的是一个用于创建和管理实际用户帐户的API,
你可以使用这些API来验证用户信息,这留给了开发者。Identity Management通过提供这样一个API来填补了这个空白,这个API努力使用一致的方式来管理用户和角色,而不用考
虑他们存储在那里。无论他们是作为记录保存在关系数据库中还是作为实体保存在LDAP目录中,Identity Management都在Seam程序中提供了统一标准的API来创建,更新,和删除
用户和角色。
刚才所说的 Identity Management API 就是 IdentityManager 组件。这个组件暴露了一些identity management的操作。为了让你更多的了解下这个特性,下面列出了一些它具有
的函数:
* createUser(String username, String password)
* deleteUser(String name)
* enableUser(String name)
* disableUser(String name)
* changePassword(String name, String password)
* isUserEnabled()
* grantRole(String name, String role)
* revokeRole(String name, String role)
* createRole(String role)
* deleteRole(String role)
* userExists(String name)
* roleExists(String name)
* listUsers()
* authenticate(String username, String password)
从上表中你可以看到,大多数的函数都和操作系统安全相关的命令同义(假设你属性unix/linux),就像:adduser,deluser 等待。
Configuring IdentityManager
IdentityManager的配置是相当简单的 -- 它需要和一个或则两个认证中心(identity stores)一起配置。一个identity store 用于所有用于相关的操作,另外一个用于角色相关
的操作。如果你的用户和角色相关信息都保存在同一个存储中心中(例如一个数据库),这样只需要一个identity store就可以了,这一个identity store用作用户和角色相关的
操作。这听起来好像有点奇怪,当处理复杂的安全需求时,这有相当大的伸缩性。 例如这样一个场景:从LDAP中验证用户,但是从一个关系数据库中载入用户的角色信息。
每个 IdentityStore 实现都知道如何与一个特别的存储中心打交道。Seam提供了两个现成的 IdentityStore 实现,JpaIdentityStore 和 LdapIdentityStore, 分别用于数据库和
基于LDAP的安全验证。如果IdentityManager没有配置identity stores,那么它就使用默认值JpaIdentityStore。我们以后在来讲解如何使用LdapIdentityStore,现在这篇文章中
我们就只关注JpaIdentityStore。
JpaIdentityStore
这个 IdentityStore 实现可以让你把用户帐户信息保存在数据库中。通过应用特定的注解到代表用户和角色的实体bean上,JpaIdentityStore可以使用这些实体来管理用户数据库
中的用户信息。
我们来看看用来保存用户帐户信息的 MemberAccount 实体 bean。 下面的代码为了排版被截断了 :
@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = "username"))
public class MemberAccount implements Serializable
{
// snip field and key declarations
@NotNull @UserPrincipal
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
@UserPassword(hash = "MD5")
public String getPasswordHash() { return passwordHash; }
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
@UserEnabled
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
@UserRoles
@ManyToMany(targetEntity = MemberRole.class)
@JoinTable(name = "AccountMembership",
joinColumns = @JoinColumn(name = "AccountId"),
inverseJoinColumns = @JoinColumn(name = "MemberOf"))
public Set<MemberRole> getRoles() { return roles; }
public void setRoles(Set<MemberRole> roles) { this.roles = roles; }
}
就如我们看到的,在bean的属性访问方法中有一些附加的注解,这些注解告诉 JpaIdentityStore 如何与特定的实体bean交互。我们下面来详细的看看这些注解:
* @UserPrincipal - 该注解表明该属性包含用户的主要属性(i.e. 用户名称)
* @UserPassword - 该注解表明该属性包含用户的密码。把用户密码保存为普通文本通常不是一个好的注意,因此这个注解支持使用hash算法。该算法用来产生一个用户密码
的hash值,然后会把hash值保存到数据库中。
* @UserEnabled - 这个是可选的,用户指示该用户帐户是否是启用的,默认值为启用所有的帐户。
* @UserRoles - 这个注解指示该用户所属的角色组。
这里还有一组类似的注解来配置一个用来保存角色的实体bean。还可以把角色信息和用户信息保存在同一个表中,通过鉴别器来区分,请参考Seam的文档来了解相关信息。
Alternatively, it is possible to store role records in the same table as users (which then becomes self-referencing via a one-to-many relationship to itself) by specifying one of the columns as a discriminator (determining whether the record represents a user or a role).
Using IdentityManager
我们来看看SeamSpace示例, 看看Identity management是如何工作的
从上图,SeamSpace的主页,我们可以看到一个让用户注册的'SIGN UP'链接。在注册页面用户可以输入一些帐户信息来注册,然后可以用来登陆。注册的程序使用IdentityManager
来创建新的用户帐户,然后登陆新注册的用户。看一下 RegisterAction.java 类,我们可以看到 Identity Manager 通过使用 @In 注解注入进来了:
@In
private IdentityManager identityManager;
更进一步,我们可以在uploadPicture()方法中看到新用户是如何被创建的 (在注册流程的最后调用了该方法,并且结束了当前的会话):
@End
public void uploadPicture()
{
(snip)
new RunAsOperation() {
public void execute() {
identityManager.createUser(username, password);
identityManager.grantRole(username, "user");
}
}.addRole("admin")
.run();
(snip)
// Login the user
identity.setUsername(username);
identity.setPassword(password);
identity.login();
}
RunAsOperation 使用较高的优先级来执行特别的操作。在这种情况下,创建一个新用户要求当前的用户具有管理员的权限,上面的代码通过addRole()函数暂时为RunAsOperation
操作授权。在该函数的最后我们看到该用户使用用户名和密码登陆到系统中了。
Authentication
以前,Seam安全管理在认证阶段需要调用一个'authenticator' 组件,该组件用来计算用户的角色。这个认证模型当前依然支持,同时使用IdentityManager 来认证用户更合常理
,他提供了认证功能而不用写额外的待命。请记住,要使用IdentityManager 来验证用户,只需要从components.xml文件的identity 组件配置中删除'authenticate-method'属性
就可以了:
<!-- The old way of authenticating, using an authenticator component -->
<!--security:identity authenticate-method="#{authenticator.authenticate}"/-->
<!-- The new way to authenticate, using IdentityManager (you don't actually need to
include this element, since it has no attributes now) -->
<security:identity/>
User Management Views
如果你使用管理员帐号登陆 SeamSpace (username/password: demo/demo),在顶部你会看到一个'Security' 连接. 点击这个连接可以用来管理其他的用户和角色:
点击第一个连接, 'Manage Users' 将会打开用户管理页面:
这里可以添加,修改和删除用户。点击 'new user' 按钮来创建用户, 打开一个输入用户信息的页面:
点击 save 会保存新用户,返回到用户管理界面,可以看到新创建的用户:
现在来看看角色管理, 点击'Manage Roles' 会打开角色管理页面:
我们会看到系统定义的角色。点击'new role'来创建新的角色:
可以输入角色名和指定用户组。点击save保存,然后返回的角色管理界面:
这里我们就结束了 identity management 特效的预览,下面来看看 permission management.
Permission Management
尽管 Identity Management 提供了一致的API来管理用户帐户,我们依然需要一个方式来管理用户许可(Permission)。从前一个版本以来,在Seam 2.1.0中的验证特性经过了整个的修订。在以前开发者需要继承一个内建的 Identity 组件来实现自定义的许可(Permission)检查,在Seam 2.1.0中提供了一个可插拔的系统,可以让你注册你自己的许可(Permission)分析器而不用继承其他的组件了。下图展示了这些东西是如何协作 的:
在上图中, Identity 现在使用 PermissionMapper 映射一个特别的ResolverChain来执行许可(Permission)检查,该ResolverChain可以配置一个或多个 PermissionResolvers。Seam提供了 RuleBasedPermissionResolver (for resolving rule-based permission checks) 和 PersistentPermissionResolver (for performing checks based on permissions stored in persistent storage, such as a database)。如果你的程序需要自定义安全管理,实现你自己的PermissionResolver 也是相当简单的。
我想在进一步前进之前,我们需要定义下许可(Permission)是什么。在Seam中,许可(Permission)有3个方面:
* A target, 一个在某些方面起作用的对象
* An action, 在 target 上被执行的动作
* A recipient, 具有在target上执行特定action的许可(Permission)的用户或则角色实体
许可(Permission)检查的target就是PermissionMapper用来判断使用那个ResolverChain来执行检查。这样可以 实现为不同的对象配置不同的PermissionResolvers。例如:你可能希望仅仅使用RuleBasedPermissionResolver 来执行Customer对象的许可(Permission)检查,同时使用PersistentPermissionResolver来执行 Invoice对象上的许可(Permission)检查。PermissionMapper支持这种灵活性。
让我们实际的看看这些应用。
Persistent Permissions
SeamSpace示例允许用户上传一个图片。其他用户在浏览用户信息的时候可以看到该图片:
现在我们假设一些图片你想设置为私有的,而其他一些图片你想仅仅显示给你的朋友。点击你图片下的 padlock 图标,会打开图片的许可(Permission)管理页面:
在这个页面,我们可以看到那些用户和角色有查看这个图片的许可。在这个示例中,仅仅我的朋友具有查看该图片的权限。现在我想让该站点的任何用户都可以看到该图片。可以点击'new permission'来授权,会打开许可详细信息页面:
在这里我们通过选择角色或则具体的用户来提供特殊的许可权限。我希望所有的用户都可以查看,所以我从角色列表中选择‘user’,然后点击‘view’ checkbox。点击save按钮保存设置,然后返回到许可管理界面,在这里可以看到我设置的新许可:
当我们授予一个新的许可的时候,实际发生了什么呢?我们看看在这个场景后面使用的组件,ImagePermission. 下面是相关的代码:
@Name("imagePermission")
@Scope(CONVERSATION)
public class ImagePermission implements Serializable
{
// (snip)
@In PermissionManager permissionManager;
@In PermissionSearch permissionSearch;
private MemberImage target;
private Principal recipient;
@SuppressWarnings("unchecked")
@Begin(nested = true)
public void createPermission() {
target = (MemberImage) permissionSearch.getTarget();
// (snip)
}
public void applyPermissions() {
// (snip)
List<Permission> permissions = new ArrayList<Permission>();
for (String role : selectedRoles)
{
Principal r = new Role(role);
for (String action : selectedActions)
{
permissions.add(new Permission(target, action, r));
}
}
for (Member friend : selectedFriends)
{
MemberAccount acct = (MemberAccount) entityManager.createQuery(
"select a from MemberAccount a where a.member = :member")
.setParameter("member", friend)
.getSingleResult();
Principal p = new SimplePrincipal(acct.getUsername());
for (String action : selectedActions)
{
permissions.add(new Permission(target, action, p));
}
}
permissionManager.grantPermissions(permissions);
Conversation.instance().endBeforeRedirect();
}
// (snip)
}
从上面的代码中我们可以看到 ImagePermission 是一个会话作用域(conversation-scoped)的组件。其实,它的功能被实现为一个嵌套的作用域,在同一个target对象上可以同时打开 多个 'new permission' 窗口来操作。我们还可以看到PermissionManager通过@In 注解注入进来了。
现在 createPermission() 方法开始一个嵌套的会话 (thanks to the @Begin(nested = true) annotation),然后在我们的target对象中保存一个引用。当用户点击save按钮,在分配了指定的许可后,用来构建一些 Permission对象来执行授权的applyPermissions()方法被调用了。在该函数内,使用一些授权的许可来调用 PermissionManager.grant()。我们发些时间来详细解释下这个 PermissionManager (the heart of the Permission Management API)。
The PermissionManager Component
就像 IdentityManager 用来处理用户和角色操作一样,PermissionManager 被设计为来操作许可。它提供了API来允许授权和激活许可,或则一个target对象的列表。我们来看看一些方法:
* listPermissions(String target, String action)
* listPermissions(Object target)
* grantPermission(Permission permission)
* grantPermissions(List<Permission> permissions)
* revokePermission(Permission permission)
* revokePermissions(List<Permission> permissions)
就像 IdentityManager 需要一个 IdentityStore 来保存数据, PermissionManager 也需要一个 PermissionStore 来和持久化存储打交到。Seam 只提供了一个 PermissionStore 实现 - JpaPermissionStore, 可以通过JPA来和数据库存在互操作。在理论上也可以把许可信息保存在LDAP目录中,或则一个普通文件中,但是大多数情况下还是保存在一个数据库中的。
现在来看看SeamSapce示例中 AccountPermission entity bean的代码,下面的代码为了排版被截断了:
@Entity
public class AccountPermission implements Serializable
{
// snip field declarations, etc
@PermissionUser @PermissionRole
public String getRecipient() { return recipient; }
public void setRecipient(String recipient) { this.recipient = recipient; }
@PermissionTarget public String getTarget() { return target; }
public void setTarget(String target) { this.target = target; }
@PermissionAction
public String getAction() { return action; }
public void setAction(String action) { this.action = action; }
@PermissionDiscriminator
public String getDiscriminator() { return discriminator; }
public void setDiscriminator(String discriminator) { this.discriminator = discriminator; }
}
再一次的,我们注意到使用了一些特殊的注解。这里我告诉你,许可既可以指定到用户上也可以指定到角色上。这就意味着可以把许可信息保存的分开的表格中,这 从性能角度来说,把他们保存在单个表中,使用一个辨别器列(discriminator column)来区分他们,可能看起来更合适。因此在上面的代码中,我们看到getDiscriminator() 使用了@PermissionDiscriminator注解,表明该列用来判断许可是应用到用户上还是角色上。
继续向下看,用来配置一个保存许可信息的实体注解如下:
* @PermissionUser - designates the field that contains the name of the recipient of the permission (for user-assigned permissions)
* @PermissionRole - same as above, but for role-assigned permissions
* @PermissionTarget - contains a unique identifier string, identifying a single instance of an object. Alternatively, can contain a class name or any arbitrary string for the designation of more generalised permissions.
* @PermissionAction - contains a list of the actions that the recipient may perform on the target object.
* @PermissionDiscriminator - see paragraph above
这里说明下 - permission management 特性只用来管理持久的许可信息( persistent permissions)。当然在大多数情况下,你可能希望通过业务逻辑来应用许可权限,例如:用户应该具有查看和管理他们自己图片的权限。这类许可有 Seam的rule-based security(基于角色的安全)来处理,在下面我们来详细讨论下这个问题。
Rule-based Permissions
Seam 基于Drools 提供了一个rule-based security模型,因此这并不是一个真正的新功能。我们这里来重新温习下这是如何应用到我们的示例项目SeamSpace中的。
继续图片安全这个话题,当处理用户图片的时候,除了我们上面讨论的问题外,显然我们需要一些基础的安全规则。在默认的情况下,查看用户的图片被安全规则限 制了,这意味着在SeamSpace中,如果你想看一个用户的图片,你必须在被该用户授权后才可以查看(either via a persistent permission grant, or a security rule)。另外请记住,我们特别的需要允许下面的一些事情:
* Users should be allowed to grant and revoke permissions for their own images
* Users should be allowed to delete their own images
* User profile images (a user's main image, i.e. their 'avatar') should always be viewable by anyone
* Users should always be allowed to view their own images (of course)
* User images with 'friend' permissions should be viewable by the user's friends (more on this in a bit)
现在我们详细的看看这些规则。首先,用户应该允许给他们的图片授予和撤回许可权限。这是相当直接的,并且作为两个独立的规则实现了。当和对象许可一起使用 的时候,Seam会插入一个PermissionCheck对象到Drools的工作内存(working memory)中,该内存包含许可目标和根据用户要做什么分别由'seam.grant-permission' 或则 'seam.revoke-permission'(based on whether PermissionManager.grantPermission() or PermissionManager.revokePermission() is called)定义的一个动作(containing both the target of the permission, and an action either being 'seam.grant-permission' or 'seam.revoke-permission' depending on what the user is trying to do (based on whether PermissionManager.grantPermission() or PermissionManager.revokePermission() is called))。用于验证用户的MemberAccount示例一直在working memory中,在实际上下面的规则是: '如果我们正在处理的MemberImage是属于当前登陆的用户的,那么就授予许可权限(if the MemberImage for which we're performing the permission check is owned by the current user, then grant the permission)':
rule GrantImagePermissions
no-loop
activation-group "permissions"
when
acct: MemberAccount()
image: MemberImage(mbr : member -> (mbr.memberId.equals(acct.member.memberId)))
check: PermissionCheck(target == image, action == "seam.grant-permission", granted == false)
then
check.grant();
end
rule RevokeImagePermissions
no-loop
activation-group "permissions"
when
acct: MemberAccount()
image: MemberImage(mbr : member -> (mbr.memberId.equals(acct.member.memberId)))
check: PermissionCheck(target == image, action == "seam.revoke-permission", granted == false)
then
check.grant();
end
在下一步,我们也需要一个允许用户删除他们自己图片的规则。和第一个规则相似,我们验证当前执行删除的图片是否属性该用户的,如果是那么就授予权限:
rule DeleteImage
no-loop
activation-group "permissions"
when
acct: MemberAccount()
image: MemberImage(mbr : member -> (mbr.memberId.equals(acct.member.memberId)))
check: PermissionCheck(target == image, action == "delete", granted == false)
then
check.grant();
end
用于查看用户图片的规则有点不同。这里我们简单的测试 -- 正在查看的图片是用户自己的图片(Here we simply test that the image being viewed is the profile image for the owning member) (i.e. image.getMember().getPicture() == image):
rule ViewProfileImage
no-loop
activation-group "permissions"
when
image: MemberImage()
check: PermissionCheck(target == image, action == "view", granted == false)
eval( image.getMember().getPicture() == image )
then
check.grant();
end
还有,用户应该总是可以查看他们自己的图片。这个权限验证类似于第一个规则,在那里我们简单的验证图片的所有者是否为当前登陆的用户:
rule ViewMyImages
no-loop
activation-group "permissions"
when
acct: MemberAccount()
image: MemberImage(mbr : member -> (mbr.memberId.equals(acct.member.memberId)))
check: PermissionCheck(target == image, action == "view")
then
check.grant();
end
Conditional Roles
最后,为了当我们的朋友查看我们的图片,我们需要定义一个特殊的规则。在前面我们看到了如何给‘friends’角色授权,然而根据上下文的不同 ‘riend’可能具有不同的意思。可以认为,在系统中的任何一个用户都是某人的‘friend’,你们当其他人来看图片的时候我们如何定义他们是否是一 个‘friend’呢?这就是条件角色(conditional roles )的领域了,这些角色是特殊的,并且不能直接的授予给用户。
当一个对象的权限检查通过安全(security)API检查过后, permission manager就通知 security API一个conditional role被授予权限了,需要执行一个特殊的基础规则(rule-based)来验证该用户是否具有该角色,但在仅仅在permission check的上下文中执行。要实现这个功能,和往常一样把一个PermissionCheck对象插入到包含target和action的working memory中,然而也会附加的插入一个RoleCheck对象,该对象包含用来对比的conditional role的名字。这样我们可以写一个安全规则来验证是否授予conditional role权限:
rule FriendViewImage
no-loop
activation-group "permissions"
when
acct: MemberAccount()
image: MemberImage(mbr : member -> (mbr.isFriend(acct.member)))
PermissionCheck(target == image, action == "view")
role: RoleCheck(name == "friends")
then
role.grant();
end
这个规则检查当前验证的用户是否在图片所有者的朋友列表中。如果在的话,角色就暂时的授予这个许可检查。在某些复杂安全规则的情况下这具有非常大的灵活性 ( This allows great flexibility in assigning complex security rules to dynamic groups of users (in this case, a user's friends list) that don't necessarily warrant having their own role/group, due to either impracticality or design restrictions).
Strongly-typed Security Annotations
最后,作为结束我们来看看一些新的安全注解。为了使Seam Security看起来更'Web Beansy',我们引入了一些用于现在组件方法的类型安全的注解。通过使用meta-annotations,我们可以提供一些安全注解来应用安全限制 到函数上或则参数上。Seam提供了一些开箱即用的注解,他们是标准的CRUD注解 (@Insert, @Read, @Update, @Delete) ,并且添加你自己的注解也是非常简单的。看看下面的例子:
@Begin @Insert(Customer.class)
public void createCustomer() {
这个注解的功能是,除非你具有插入新customer对象的权利,放置你不能调用createCustomer方法。类似的,我们可以注解一个方法的参数:
public void updateCustomer(@Update Customer customer) {
创建自己的注解只需要使用@PermissionCheck就可以了。来看个示例,你希望创建一个新的‘Promote’许可。该注解可以非常简单的实现,如下:
@Target({METHOD, PARAMETER})
@Documented
@Retention(RUNTIME)
@Inherited
@PermissionCheck
public @interface Promote {
Class value() default void.class;
}
定义后,就可以直接使用了:
public void promoteStaff(@Promote Staff person) {
如果写一个基于规则的许可,该规则看起来就像这样:
rule PromoteStaffMember
no-loop
activation-group "permissions"
when
acct: MemberAccount()
Role(name == 'admin')
staff: Staff()
check: PermissionCheck(target == staff, action == "promote")
then
check.grant();
end
许可动作(permission action)变为注解的小些名称了。这真的是很简单的!
同时我们仍然支持遗留的用于基于表达式的安全检查的@Restrict注解,我建议各位都使用新的类型安全的注解,至少在编译期安全检查就提供了简单的验证。
Conclusion
到这里就结束了JBoss Seam中的新安全特性。这里是一些参考的链接:
JBoss Seam Community Site (downloads, documentation, forums) - http://www.seamframework.org/
JBoss Seam 中文社区站点(下载,文档,论坛) - http://www.seamframework.cn/
JBoss Home Page - http://www.jboss.org
转自:http://hi.baidu.com/jsfcn/