spring实战 保护方法应用

444 阅读12分钟

保护方法应用

使用注解保护方法

在Spring Security中实现方法级安全性的最常见办法是使用特定的注解,将这些注解应用到需要保护的方法上。这样有几个好处,最重要的是在编辑器中查看给定的方法时,能够很清楚地看到它的安全规则。

Spring Security提供了三种不同的安全注解:

Spring Security自带的@Secured注解

JSR-250的@RolesAllowed注解

表达式驱动的注解,包括@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter

@Secured和@RolesAllowed方案非常类似,能够基于用户所授予的权限限制对方法的访问。当需要在方法上定义更灵活的安全规则时,Spring Security提供了@PreAuthorize和@PostAuthorize,而@PreFilter/@PostFilter能够过滤方法返回的以及传入方法的集合。

使用@Secured注解限制方法调用

在Spring中,如果要启用基于注解的方法安全性,关键之处在于要在配置类上使用@EnableGlobalMethodSecurity,如下所示:

@Configuration
@EnableGlobalMethodSecurity(securedEnabled=true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    
}

除了使用@EnableGlobalMethodSecurity注解,我们可能也注意到配置类扩展了GlobalMethodSecurityConfiguration。在前面,Web安全的配置类扩展了WebSecurityConfigurerAdapter,与之类似,这个类能够为方法级别的安全性提供更精细的配置。

例如,如果在Web层的安全配置中设置认证,那么可以通过重载GlobalMethodSecurityConfiguration的configure()方法实现该功能:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}

将会看到如何重载GlobalMethodSecurity-Configuration的createExpressionHandler()方法,提供一些自定义的安全表达式处理行为。

回到@EnableGlobalMethodSecurity注解,注意它的securedEnabled属性设置成了true。如果securedEnabled属性的值为true的话,将会创建一个切点,这样的话Spring Security切面就会包装带有@Secured注解的方法。例如,考虑如下这个带有@Secured注解的addSpittle()方法:

@Secured("ROLE_SPITTER")
public void addSpittle(Spittle spittle) {
    //...
}

@Secured注解会使用一个String数组作为参数。每个String值是一个权限,调用这个方法至少需要具备其中的一个权限。通过传递进来ROLE_SPITTER,告诉Spring Security只允许具有ROLE_SPITTER权限的认证用户才能调用addSpittle ()方法。

如果传递给@Secured多个权限值,认证用户必须至少具备其中的一个才能进行方法的调用。例如,下面使用@Secured的方式表明用户必须具备ROLE_SPITTER或ROLE_ADMIN权限才能触发这个方法:

@Secured("ROLE_SPITTER", "ROLE_ADMIN")
public void addSpittle(Spittle spittle) {
    //...
}

如果方法被没有认证的用户或没有所需权限的用户调用,保护这个方法的切面将抛出一个Spring Security异常(可能是AuthenticationException或AccessDeniedException的子类)。它们是非检查型异常,但这个异常最终必须要被捕获和处理。如果被保护的方法是在Web请求中调用的,这个异常会被Spring Security的过滤器自动处理。否则的话,需要编写代码来处理这个异常。

在Spring Security中使用JSR-250的@RolesAllowed注解

@RolesAllowed注解和@Secured注解在各个方面基本上都是一致的。唯一显著的区别在于@RolesAllowed是JSR-250定义的Java标准注解。

差异更多在于政治考量而非技术因素。但是,当使用其他框架或API来处理注解的话,使用标准的@RolesAllowed注解会更有意义。

如果选择使用@RolesAllowed的话,需要将@EnableGlobalMethodSecurity的jsr250Enabled属性设置为true,以开启此功能:

@Configuration
@EnableGlobalMethodSecurity(jsr250Enabled=true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    
}

尽管这里只是启用了jsr250Enabled,但需要说明的一点是这与securedEnabled并不冲突。这两种注解风格可以同时启用。

在将jsr250Enabled设置为true之后,将会启用一个切点,这样带有@RolesAllowed注解的方法都会被Spring Security的切面包装起来。因此,在方法上使用@RolesAllowed的方式与使用@Secured类似。例如,如下的addSpittle()方法使用了@RolesAllowed注解来代替@Secured:

@RolesAllowed("ROLE_SPITTER")
public void addSpittle(Spittle spittle) {
    //...
}

尽管@RolesAllowed比 @Secured在政治上稍微有点优势,它是实现方法安全的标准注解,但是这两个注解有一个共同的不足。它们只能根据用户有没有授予特定的权限来限制方法的调用。

使用表达式实现方法级别的安全性

尽管@Secured和@RolesAllowed注解在拒绝未认证用户方面表现不错,但这也是它们所能做到的所有事情了。有时候,安全性约束不仅仅涉及用户是否有权限。

Spring Security 3.0引入了几个新注解,它们使用SpEL能够在方法调用上实现更有意思的安全性约束。这些新的注解在下表中进行了描述。

这些注解的值参数中都可以接受一个SpEL表达式。表达式可以是任意合法的SpEL表达式,如果表达式的计算结果为true,那么安全规则通过,否则就会失败。安全规则通过或失败的结果会因为所使用注解的差异而有所不同。

注 解描 述
@PreAuthorize在方法调用之前,基于表达式的计算结果来限制对方法的访问
@PostAuthorize允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常
@PostFilter允许方法调用,但必须按照表达式来过滤方法的结果
@PreFilter允许方法调用,但必须在进入方法之前过滤输入值

需要将@EnableGlobalMethod-Security注解的prePostEnabled属性设置为true,从而启用它们:

@Configuration
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    
}

表述方法访问规则

到目前为止,已经看到@Secured和@RolesAllowed能够限制只有用户具备所需的权限才能触发方法的执行。但是,这两个注解的不足在于它们只能基于用户授予的权限来做出决策。

Spring Security还提供了两个注解,@PreAuthorize和@PostAuthorize,它们能够基于表达式的计算结果来限制方法的访问。在定义安全限制方面,表达式带了极大的灵活性。通过使用表达式,只要想象得到,就可以定义任意允许访问或不允许访问方法的条件。

@PreAuthorize和@PostAuthorize之间的关键区别在于表达式执行的时机。@PreAuthorize的表达式会在方法调用之前执行,如果表达式的计算结果不为true的话,将会阻止方法执行。与之相反,@PostAuthorize的表达式直到方法返回才会执行,然后决定是否抛出安全性的异常。

在方法调用前验证权限

@PreAuthorize乍看起来可能只是添加了SpEL支持的@Secured和@RolesAllowed。实际上,可以基于用户所授予的角色,使用@PreAuthorize来限制访问:

@PreAuthorize("hasRole('ROLE_SPITTER')")
public void addSpittle(Spittle spittle) {
    //...
}

如果按照这种方式的话,@PreAuthorize相对于@Secured和@RolesAllowed并没有什么优势。如果用户具有ROLE_SPITTER角色的话,允许方法调用。否则,将会抛出安全性异常,方法也不会执行。

但是,@PreAuthorize的功能并不限于这个简单例子所展现的。@PreAuthorize的String类型参数是一个SpEL表达式。借助于SpEL表达式来实现访问决策,我们能够编写出更高级的安全性约束。例如,Spittr应用程序的一般用户只能写140个字以内的Spittle,而付费用户不限制字数。

虽然@Secured和@RolesAllowed在这里无能为力,但是@PreAuthorize注解恰好能够适用于这种场景:

@PreAuthorize("hasRole('ROLE_SPITTER' and #spittle.text.length() <= 140) or hasRole('ROLE_PREMIUM')")
public void addSpittle(Spittle spittle) {
    //...
}

表达式中的#spittle部分直接引用了方法中的同名参数。这使得Spring Security能够检查传入方法的参数,并将这些参数用于认证决策的制定。在这里,深入到Spitter的文本内容中,保证不超过Spittr标准用户的长度限制。如果是付费用户,那么就没有长度限制了。

在方法调用之后验证权限

在方法调用之后验证权限并不是比较常见的方式。事后验证一般需要基于安全保护方法的返回值来进行安全性决策。这种情况意味着方法必须被调用执行并且得到了返回值。

例如,假设想对getSpittleById()方法进行保护,确保返回的Spittle对象属于当前的认证用户。我们只有得到Spittle对象之后,才能判断它是否属于当前用户。因此,getSpittleById()方法必须要先执行。在得到Spittle之后,如果它不属于当前用户的话,将会抛出安全性异常。

除了验证的时机之外,@PostAuthorize与@PreAuthorize的工作方式差不多,只不过它会在方法执行之后,才会应用安全规则。此时,它才有机会在做出安全决策时,考虑到返回值的因素。

例如,要保护上面描述的getSpittleById()方法,可以按照如下的方式使用@PostAuthorize注解:

@PostAuthorize("returnObject.spitter.username == principal.username")
public Spittle getSpittleById(long id) {
    //...
}

为了便利地访问受保护方法的返回对象,Spring Security在SpEL中提供了名为returnObject的变量。在这里,知道返回对象是一个Spittle对象,所以这个表达式可以直接访问其spittle属性中的username属性。

在对比表达式双等号的另一侧,表达式到内置的principal对象中取出其username属性。principal是另一个Spring Security内置的特殊名称,它代表了当前认证用户的主要信息(通常是用户名)。

在Spittle对象所包含Spitter中,如果username属性与principal的username属性相同,这个Spittle将返回给调用者。否则,会抛出一个AccessDeniedException异常,而调用者也不会得到Spittle对象

有一点需要注意,不像@PreAuthorize注解所标注的方法那样,@PostAuthorize注解的方法会首先执行然后被拦截。这意味着,需要小心以保证如果验证失败的话不会有一些负面的结果。

过滤方法的输入和输出

如果希望使用表达式来保护方法的话,那使用@PreAuthorize和@PostAuthorize是非常好的方案。但是,有时候限制方法调用太严格了。有时,需要保护的并不是对方法的调用,需要保护的是传入方法的数据和方法返回的数据。

例如,有一个名为getOffensiveSpittles()的方法,这个方法会返回标记为具有攻击性的Spittle列表。这个方法主要会给管理员使用,以保证Spittr应用中内容的和谐。但是,普通用户也可以使用这个方法,用来查看他们所发布的Spittle有没有被标记为具有攻击性。这个方法的签名大致如下所示;

public List<Spittle> getOffensiveSpittles() { ... }

按照这种方法的定义,getOffensiveSpittles()方法与具体的用户并没有关联。它只会返回攻击性Spittle的一个列表,并不关心它们属于哪个用户。对于管理员使用来说,这是一个很好的方法,但是它无法限制列表中的Spittle都属于当前用户。

当然,也可以重载getOffensiveSpittles(),实现另一个版本,让它接受一个用户ID作为参数,查询给定用户的Spittle。但是,正如开头所讲的那样,始终会有这样的可能性,那就是将较为宽松限制的版本用在具有一定安全限制的场景中。

需要有一种方式过滤getOffensiveSpittles()方法返回的Spittle集合,将结果限制为允许当前用户看到的内容,而这就是Spring Security的@PostFilter所能做的事情。

事后对方法的返回值进行过滤

与@PreAuthorize和@PostAuthorize类似,@PostFilter也使用一个SpEL作为值参数。但是,这个表达式不是用来限制方法访问的,@PostFilter会使用这个表达式计算该方法所返回集合的每个成员,将计算结果为false的成员移除掉。

为了阐述该功能,将@PostFilter应用在getOffensiveSpittles()方法上:

@PreAuthorize("hasAnyPole({'ROLE_SPITTER', 'ROLE_ADMIN'})")
@PostFilter("hasRole('ROLE_ADMIN') || filterObject.spitter.username == principal.name")
public List<Spittle> getOffensiveSpittles() {
    ...
}

在这里,@PreAuthorize限制只有具备ROLE_SPITTER或ROLE_ADMIN权限的用户才能访问该方法。如果用户能够通过这个检查点,那么方法将会执行,并且会返回Spittle所组成的一个List。但是,@PostFilter注解将会过滤这个列表,确保用户只能看到允许的Spittle。具体来讲,管理员能够看到所有攻击性的Spittle,非管理员只能看到属于自己的Spittle。

表达式中的filterObject对象引用的是这个方法所返回List中的某一个元素(我们知道它是一个Spittle)。在这个Spittle对象中,如果Spitter的用户名与认证用户(表达式中的principal.name)相同或者用户具有ROLE_ADMIN角色,那这个元素将会最终包含在过滤后的列表中。否则,它将被过滤掉。

事先对方法的参数进行过滤

除了事后过滤方法的返回值,还可以预先过滤传入到方法中的值。

例如,假设希望以批处理的方式删除Spittle组成的列表。为了完成该功能,可能会编写一个方法,其签名大致如下所示:

public void deleteSpittles(List<Spittle> spittles) { ... }

如果想在它上面应用一些安全规则的话,比如Spittle只能由其所有者或管理员删除,那该怎么做呢?如果是这样的话,可以将逻辑放在deleteSpittles()方法中,在这里循环列表中的Spittle,只删除属于当前用户的那一部分对象(如果当前用户是管理员的话,则会全部删除)。

这能够运行正常,但是这意味着需要将安全逻辑直接嵌入到方法之中。相对于删除Spittle来讲,安全逻辑是独立的关注点(当然,它们也有所关联)。如果列表中能够只包含实际要删除的Spittle,这样会更好一些,因为这能帮助deleteSpittles()方法中的逻辑更加简单,只关注于删除Spittle的任务。

Spring Security的@PreFilter注解能够很好地解决这个问题。与@PostFilter非常类似,@PreFilter也使用SpEL来过滤集合,只有满足SpEL表达式的元素才会留在集合中。但是它所过滤的不是方法的返回值,@PreFilter过滤的是要进入方法中的集合成员。

@PreFilter的使用非常简单。如下的deleteSpittles()方法使用了@PreFilter注解:

@PreAuthorize("hasAnyRole({'ROLE_SPITTER', 'ROLE_ADMIN'})")
@PreFilter("hasRole('ROLE_ADMIN') || targetObject.spitter.username == principal.name")
public void deleteSpittles(List<Spittle> spittles) { ... }

与前面一样,对于没有ROLE_SPITTER或ROLE_ADMIN权限的用户,@PreAuthorize注解会阻止对这个方法的调用。但同时,@PreFilter注解能够保证传递给deleteSpittles()方法的列表中,只包含当前用户有权限删除的Spittle。这个表达式会针对集 合中的每个元素进行计算,只有表达式计算结果为true的元素才会保留在列表中。targetObject是Spring Security提供的另外一个值,它代表了要进行计算的当前列表元素。

Spring Security提供了注解驱动的功能,这是通过一系列注解来实现的,到此为止,已经对这些注解进行了介绍。相对于判断用户所授予的权限,使用表达式来定义安全限制是一种更为强大的方式。

定义许可计算器

在@PreFilter和@PostFilter中所使用的表达式还算不上太复杂。但是,它也并不简单,可以很容易地想象如果还要实现其他的安全规则,这个表达式会不断膨胀。在变得很长之前,表达式就会笨重、复杂且难以测试。

其实能够将整个表达式替换为更加简单的版本,如下所示:

@PreAuthorize("hasAnyRole({'ROLE_SPITTER', 'ROLE_ADMIN'})")
@PreFilter("hasPermission(targetObject, 'delete')")
public void deleteSpittles(List<Spittle spittles) { ... }

现在,设置给@PreFilter的表达式更加紧凑。它实际上只是在问一个问题“用户有权限删除目标对象吗?”。如果有的话,表达式的计算结果为true,Spittle会保存在列表中,并传递给deleteSpittles()方法。如果没有权限的话,它将会被移除掉。

hasPermission()函数是Spring Security为SpEL提供的扩展,它为开发者提供了一个时机,能够在执行计算的时候插入任意的逻辑。需要做的就是编写并注册一个自定义的许可计算器。下列程序展现了SpittlePermissionEvaluator类,它就是一个自定义的许可计算器,包含了表达式逻辑。

package spittr.security;
import java.io.Serializable;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import spittr.Spittle;

public class SpittlePermissionEvaluator implements PermissionEvaluator {
    private static final GrantedAuthority ADMIN_AUTHORITY = new GrantedAuthorityImpl("ROLE_ADMIN");
    
    public boolean hasPermission(Authentication authentication, Object target, Object Permission) {
        if(target instanceof Spittle) {
            Spittle spittle = (Spittle)target;
            String username = spittle.getSpitter().getUsername();
            if("delete".equals(permission)) {
                return isAdmin(authentication) || username.equals(authentication.getName());
            }
        }
        throw new UnsupportedOperationException("hasPermission not supported for object <" + target + "> and permission <" + permission + ">");
    }
    
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        throw new UnsupportedOperationException();
    }
    
    private boolean isAdmin(Authentication authentication) {
        return authentication.getAuthorties().contains(ADMIN_AUTHORITY);
    }
}

SpittlePermissionEvaluator实现了Spring Security的PermissionEvaluator接口,它需要实现两个不同的hasPermission()方法。其中的一个hasPermission()方法把要评估的对象作为第二个参数。第二个hasPermission()方法在只有目标对象的ID可以得到的时候才有用,并将ID作为Serializable传入第二个参数。

为了满足我们的需求,假设使用Spittle对象来评估权限,所以第二个方法只是简单地抛出UnsupportedOperationException。

对于第一个hasPermission()方法,要检查所评估的对象是否为一个Spittle,并判断所检查的是否为删除权限。如果是这样,它将对比Spitter的用户名是否与认证用户的名称相等,或者当前用户是否具有ROLE_ADMIN权限。

许可计算器已经准备就绪,接下来需要将其注册到Spring Security中,以便在使用@PreFilter表达式的时候支持hasPermission()操作。为了实现该功能,我们需要替换原有的表达式处理器,换成使用自定义许可计算器的处理器。

默认情况下,Spring Security会配置为使用DefaultMethodSecurityExpression-Handler,它会使用一个DenyAllPermissionEvaluator实例。顾名思义,Deny-AllPermissionEvaluator将会在hasPermission()方法中始终返回false,拒绝所有的方法访问。但是,我们可以为Spring Security提供另外一个DefaultMethod-SecurityExpressionHandler,让它使用我们自定义的SpittlePermissionEvaluator,这需要重载GlobalMethodSecurityConfiguration的createExpressionHandler方法:

@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
    DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
    expressionHandler.setPermissionEvaluator(new SpittlePermissionEvaluator());
    return expressionHandler;
}

现在,不管在任何地方的表达式中使用hasPermission()来保护方法,都会调用SpittlePermissionEvaluator来决定用户是否有权限调用方法。