第 55 章 acl自动提醒

自定义AfterInvocationProvider,在创建实体类时自动创建对应的acl权限,在删除实体类的时候,自动删除acl权限。

55.1. 自动创建acl

首先一个目标是监听实体类的创建,在创建后生成对应的acl信息。

根据实体类创建acl信息的代码如下所示:

ObjectIdentity oid = new ObjectIdentityImpl(object);
MutableAcl acl = mutableAclService.createAcl(oid);
acl.insertAce(0, BasePermission.ADMINISTRATION,
    new PrincipalSid(getUsername()), true);
        

需要创建一个CreateAclEntryAfterInvocationProvider,将它注册到Spring Security中就可以在方法调用后进行对应的操作了。

public Object decide(Authentication authentication, Object object,
    Collection<ConfigAttribute> config, Object returnedObject)
    throws AccessDeniedException {
    Iterator iter = config.iterator();

    while (iter.hasNext()) {
        ConfigAttribute attr = (ConfigAttribute) iter.next();

        if (this.supports(attr)) {
            Object domainObject = getDomainObjectInstance(object,
                    processDomainObjectClass);

            Iterator cit = this.handlers.iterator();

            while (cit.hasNext()) {
                AclHandler handler = (AclHandler) cit.next();

                if (handler.supports(domainObject, returnedObject)) {
                    handler.create(authentication, domainObject,
                        config, returnedObject);

                    break;
                }
            }
        }
    }

    return returnedObject;
}
        

xml文件中的配置如下:

<bean id="afterAclCreate" class="com.family168.springsecuritybook.ch303.afterinvocation.CreateAclEntryAfterInvocationProvider">
    <sec:custom-after-invocation-provider/>
    <constructor-arg ref="aclService"/>
    <constructor-arg>
        <list>
            <util:constant static-field="org.springframework.security.acls.domain.BasePermission.ADMINISTRATION"/>
        </list>
    </constructor-arg>
    <property name="processDomainObjectClass" value="com.family168.springsecuritybook.ch303.AclDomainClass"/>
    <property name="handlers">
        <list>
            <ref local="aclHandler"/>
        </list>
    </property>
</bean>
        

为了指定在哪些方法调用后执行CreateAclEntryAfterInvocationProvider,我们在方法上使用AFTER_ACL_CREATE注解。

@Transactional
@Secured({"ROLE_USER", "AFTER_ACL_CREATE"})
public void save(Account account) {
    list.add(account);
}
        

这样在save(account)方法执行后,就会自动根据account这个对象生成对应的acl信息。

55.2. 自动删除acl

自动删除acl的步骤与创建acl的大致相同。

首先来看一下删除acl信息的代码:

public Object decide(Authentication authentication, Object object,
    Collection<ConfigAttribute> config, Object returnedObject)
    throws AccessDeniedException {
    Iterator iter = config.iterator();

    while (iter.hasNext()) {
        ConfigAttribute attr = (ConfigAttribute) iter.next();

        if (this.supports(attr)) {
            Object domainObject = getDomainObjectInstance(object,
                    processDomainObjectClass);

            Iterator cit = this.handlers.iterator();

            while (cit.hasNext()) {
                AclHandler handler = (AclHandler) cit.next();

                if (handler.supports(domainObject, returnedObject)) {
                    handler.delete(authentication, domainObject,
                        config, returnedObject);

                    break;
                }
            }
        }
    }

    return returnedObject;
}
        

xml中的配置如下所示:

<bean id="afterAclDelete" class="com.family168.springsecuritybook.ch303.afterinvocation.DeleteAclEntryAfterInvocationProvider">
    <sec:custom-after-invocation-provider/>
    <constructor-arg ref="aclService"/>
    <constructor-arg>
        <list>
            <util:constant static-field="org.springframework.security.acls.domain.BasePermission.ADMINISTRATION"/>
        </list>
    </constructor-arg>
    <property name="processDomainObjectClass" value="com.family168.springsecuritybook.ch303.AclDomainClass"/>
    <property name="handlers">
        <list>
            <ref local="aclHandler"/>
        </list>
    </property>
</bean>
        

最后记得使用AFTER_ACL_DELETE对方法进行注解。

@Transactional
@Secured({"ROLE_USER", "AFTER_ACL_DELETE"})
public void remove(Account account) {
    list.remove(account);
}
        

55.3. 根据id删除acl

上面演示的remove()方法中需要传入account对象的实例才能起作用,有时我们希望使用removeById(id)的方式,只通过对象的id就可以删除这个对象的实例。这时AfterInvocation就无法判断当前的id实际对应的是哪个类,因此也就无法删除对应的acl信息了。

为了解决这个问题,我们引入了AclDomainAware注解。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AclDomainAware {
    Class value();
}
        

可以为它设置一个Class值,使用它标示当前方法中id所对应的类型。

@Transactional
@Secured({"ACL_DELETE", "AFTER_ACL_DELETE"})
@AclDomainAware(Account.class)
public void removeById(Long id) {
    Account account = this.get(id);
    list.remove(account);
}
        

这样DeleteAclEntryAfterInvocationProvider就可以通过AclDomainAware中设置的类型和id的值找到对应实例的信息并删除之。

对AclDomainAware进行扩展后,我们也可以让它负责处理那些参数中包含id的情况,扩展AclEntryVoter,让它可以使用AclDomainAware中的类型和id值生成的acl信息,以此进行acl的权限控制。

IdParameterAclEntryVoter代码如下所示:

public class IdParameterAclEntryVoter extends AclEntryVoter {
    public IdParameterAclEntryVoter(AclService aclService,
        String processConfigAttribute, Permission[] requirePermission) {
        super(aclService, processConfigAttribute, requirePermission);
    }

    @Override
    protected Object getDomainObjectInstance(MethodInvocation invocation) {
        Object[] args;
        Class[] params;

        params = invocation.getMethod().getParameterTypes();
        args = invocation.getArguments();

        for (int i = 0; i < params.length; i++) {
            if (getProcessDomainObjectClass().isAssignableFrom(params[i])) {
                return args[i];
            }
        }

        Method method = invocation.getMethod();

        Serializable id = null;

        for (int i = 0; i < params.length; i++) {
            if (Serializable.class.isAssignableFrom(params[i])) {
                id = (Serializable) invocation.getArguments()[i];

                break;
            }
        }

        if (id == null) {
            throw new AuthorizationServiceException("MethodInvocation: "
                + invocation + " did not provide any ID argument.");
        }

        if (method.isAnnotationPresent(AclDomainAware.class)) {
            try {
                Class domainClass = method.getAnnotation(AclDomainAware.class)
                                          .value();
                Object instance = domainClass.newInstance();
                Method setter = domainClass.getDeclaredMethod("setId",
                        new Class[] {id.getClass()});
                setter.invoke(instance, new Object[] {id});

                return instance;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }

        throw new AuthorizationServiceException("Secure object: "
            + invocation + " did not provide any argument of type: "
            + getProcessDomainObjectClass());
    }
}
        

实例在ch303。