Spring实战(十四)-保护方法应用

本文基于《Spring实战(第4版)》所写。

通过使用Spring Security保护bean方法,能声明安全规则,保证如果用户没有执行方法的权限,就不会执行相应的方法。

使用注解保护方法

在Spring Security中实现方法级安全性的最常见办法是使用特定的注解,将这些注解应用到需要保护的方法上。

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

  • Spring Security自动的@Security注解;
  • JSR-250的@RolesAllowed注解;
  • 表达式驱动的注解,包括@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter。

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

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

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

package spittr.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;

@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");
}

让我们回到@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标准注解。

如果选择使用@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有些优势,它是实现方法安全的标准注解,但是这两个注解有一个共同的不足。它们只能根据用户有没有授予特定的权限来限制方法的调用。在判断方式是否执行方面,无法使用其他的因素。在之前的篇章中曾经看到过,在保护URL方面,能够使用SpEL表达式克服这一限制。接下来,我们看一下如何组合使用SpEL与Spring Security所提供的方法调用前后注解,实现基于表达式的方法安全性。

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

Spring Security 3.0 引入了几个新注解,它们使用SpEL能够在方法调用上实现更有意思的安全性约束。这些注解的值参数中都可以接受一个SpEL表达式。表达式可以是任意合法的SpEL表达式,可能会包含之前篇章所列的Spring Security 对 SpEL的扩展。如果表达式的计算结果为true,那么安全规则通过,否则就会失败。安全规则通过或失败的结果会因为所使用注解的差异而有所不同。下表描述了这些新的注解。

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

首先,我们需要将@EnableGlobalMethodSecurity注解的prePostEnabled属性设置为true,从而启用它们:

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

表述方法访问规则

Spring Security提供了两个注解,@PreAuthorize和@PostAuthorize,他们能够基于表达式的计算结果来限制方法的访问。在定义安全限制方面,表达式带来了极大的灵活性。

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

在方法调用前验证权限

@PreAuthorize不只是添加了SpEL支持的@Secured和@RolesAllowed,还可以基于用户所授予的角色,使用@PreAuthorize来限制访问:

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

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

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

表达式中的#spittle部分直接引用了方法中的同名参数。这使得Spring Security能够检查传入方法的参数,并将这些参数用于认证决策的制定。

在方法调用之后验证权限

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

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

@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对象。

过滤方法的输入和输出

有时候限制方法调用太严格了。有时,需要保护的并不是对方法的调用,需要保护的是传入方法的数据和方法返回的数据。

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

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

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

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

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

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

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

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

在这里,@PreAuthorize限制只有具备ROLE_SPITTER或ROLE_ADMIN权限的用户才能访问该方法。然后,@PostFilter注解将会过滤这个列表,确保用户只能看到允许的Spittle。具体来讲,管理员能够看到所有攻击性的Spittle,非管理员只能看到属于自己的Spittle。

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

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

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

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

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

如果我们想在它上面应用一些安全规则的话,比如Spittle只能由其所有者或管理员删除。如果将逻辑放在方法中,意味着我们需要将安全逻辑直接嵌入到方法之中。相对于删除Spittle来讲,安全逻辑是独立的关注点。如果列表中只包含实际要删除Spittle,这样会帮助方法中的逻辑更加简单。

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

@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提供的另外一个值,它代表了要进行计算的当前列表元素。

如果安全表达式很复杂且难以控制了,那么就应该看以下如何编写自定义的许可计算器(premission evaluator),以简化你的SpEL表达式。

定义许可计算器

我们能够将表达式替换为更加简单的版本,如下所示:

@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.web;

import com.sun.org.apache.bcel.internal.generic.IF_ACMPEQ;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import spittr.model.Spittle;

import java.io.Serializable;

public class SpittlePermissonEvaluator implements PermissionEvaluator {

    private static final GrantedAuthority ADMIN_AUTHORITY = new SimpleGrantedAuthority("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 serializable, String s, Object o) {
        throw new UnsupportedOperationException();
    }

    public boolean isAdmin(Authentication authentication){
        return authentication.getAuthorities().contains(ADMIN_AUTHORITY);
    }
}

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

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

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

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

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,585评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,923评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,314评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,346评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,718评论 3 291
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,828评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,020评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,758评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,486评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,722评论 2 251
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,196评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,546评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,211评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,132评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,916评论 0 200
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,904评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,758评论 2 274

推荐阅读更多精彩内容