基于方法的权限管理

基于方法的权限管理主要是通过 AOP 来实现的,Spring Security 中通过 MethodSecurityInterceptor 来提供相关的实现。不同在于 FilterSecurityInterceptor 只是在请求之前进行前置处理,MethodSecurityInterceptor 除了前置处理之外还可以进行后置处理。前置处理就是在请求之前判断是否具备相应的权限,后置处理则是对方法的执行结果进行二次过滤。前置处理和后置处理分别对应了不后的实现类,我们分别来看。

注解介绍

目前在 Spring Boot 中基于方法的权限管理主要是通过注解来实现,我们需要通过 @EnableGlobalMethodSecurity 注解开启权限注解的使用,用法如下:

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends GlobalMethodSecurityConfiguration {

}

这个注解中我们设置了三个属性:

  • prePostEnabled:开启 Spring Security 提供的四个权限注解,@PostAuthorize、@PostFilter、@PreAuthorize 以及 @PreFilter,这四个注解支持权限表达式,功能比较丰富。

  • SecuredEnabled:开启 Spring Security 提供的 @Secured 注解,该注解不支持权限表达式。

  • jsr250Enabled:开启 JSR-250 提供的注解,主要包括 @DenyAll、@PermitAll 以及 @RolesAllowed 三个注解,这些注解也不支持权限表达式。

这些注解的含义分别如下:

  • @PostAuthorize:在目标方法执行之后进行权限校验。

  • @PostFilter:在目标方法执行之后对方法的返回结果进行过滤。

  • @PreAuthorize:在目标方法执行之前进行权限校验。

  • @PreFilter:在目标方法执行之前对方法参数进行过滤。

  • @Secured:访问目标方法必须具备相应的角色。

  • @DenyAll:拒绝所有访问。

  • @PermitAll:允许所有访问。

  • @RolesAllowed:访问目标方法必须具备相应的角色。

这些基于方法的权限管理相关的注解,一般来说只要设置 prePostEnabled=true 就够用了。

另外还有一种比较 “古老” 的方法配置基于方法的权限管理,那就是通过 XML 文件配置方法拦截规则,目前已经很少有用 XML 文件来配置 Spring Security 了,所以对于这种方式我们不做过多介绍。感兴趣的读者可以查看官网的相关介绍: https://docs.spring.io/spring-security/site/docs/5.4.0/reference/html5/#secure-object-impls。

基本用法

接下来我们通过几个简单的案例来学习上面几种不同注解的用法。

首先创建一个 Spring Boot 项目,引入 Web 和 Spring Security 依赖,项目创建完成后,添加如下配置文件:

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends GlobalMethodSecurityConfiguration {

}

为了方使起见,我们将使用单元测试进行验证,所以这单就不进行额外的配置了,通过 @EnableGlobalMethodSecurity 注解开启其他权限注解的使用即可。

接下来创建一个 User 类以备后续使用:

public class User {
    private Integer id;
    private String username;

    // 省略 getter/setter
}

准备工作完成后,我们来逐个讲解一下前面注解的用法。

@PreAuthorize

@PreAuthorize 可以在目标方法执行之前对其进行安全校验,在安全校验时,可以直接使用我们在 13.3.7 小节中介绍的权限表达式。例如可以定义如下方法:

@Service
public class HelloService {
    @PreAuthorize("hasRole('ADMIN')")
    public String hello() {
        return "hello";
    }
}

这里使用了权限表达式 hasRole,表示执行该方法必须具备 ADMIN 角色才可以访问,否则不可以访问。我们在单元测试中来测试该方法:

@SpringBootTest
class BasedOnMethodApplicationTests {

    @Autowired
    HelloService helloService;

    @Test
    @WithMockUser(roles = "ADMIN")
    void preauthorizeTest01() {
        String hello = helloService.hello();
        assertNotNull(hello);
        assertEquals("hello", hello);
    }
}

通过 @WithMockUser(roles ="ADMIN") 注解设定当前执行的用户角色是 ADMIN,然后调用 helloService 中的方法进行测试即可。如果将用户角色设置为其他字符,那单元测试就不会通过。

当然,这里除了 hasRole 表达式之外,也可以使用其他权限表达式,包括在 13.4.3 小节中自定义的表达式也可以使用。也可以同时使用多个权限表达式,如下所示:

@Service
public class HelloService {
    @PreAuthorize("hasRole('ADMIN') and authentication.name=='javaboy'")
    public String hello() {
        return "hello";
    }
}

表示访问者名称必须是 javaboy,而且还需要同时具备 ADMIN 角色,才可以访问该方法。此时通过如下代码对其进行测试:

@Test
@WithMockUser(roles = "ADMIN",username = "javaboy")
void preauthorizeTest01() {
    String hello = helloService.hello();
    assertNotNull(hello);
    assertEquals("hello", hello);
}

在 @PreAuthorize 注解中,还可以通过 # 引用方法的参数,并对其进行校验,例如如下方法表示请求者的用户名必须等于方法参数 name 的值,方法才可以被执行:

@PreAuthorize("authentication.name==#name")
public String hello(String name) {
    return "hello:" + name;
}

测试方法如下:

@Test
@WithMockUser(username = "javaboy")
void preauthorizeTest02() {
    String hello = helloService.hello("javaboy");
    assertNotNull(hello);
    assertEquals("hello:javaboy", hello);
}

当模拟的用户名和方法参数相等时,单元测试就可以通过。

@PreFilter

@PreFilter 主要是对方法的请求参数进行过滤,它里边包含了一个内置对象 filterObject 表示要过滤的参数,如果方法只有一个参数,则内置的 filterObject 对象就代表该参数:如果方法有多个参数,则需要通过 filterTarget 来指定 filterObject 到底代表哪个对象:

@PreFilter(value = "filterObject.id%2!=0",filterTarget = "users")
public void addUsers(List<User> users, Integer other) {
    System.out.println("users = " + users);
}

上面代码表示对方法参数 users 进行过滤,将 id 为奇数的 user 保留。

然后通过单元测试对该方法进行测试:

@Test
@WithMockUser(username = "javaboy")
void preFilterTest01() {
    List<User> users = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        users.add(new User(i, "javaboy:" + i));
    }
    helloService.addUsers(users, 99);
}

执行单元测试方法,addUsers 方法中只会打印出 id 为奇数的 user 对象。

@PostAuthorize

@PostAuthorize 是在目标方法执行之后进行权限校验。可能有读者会觉得奇怪,目标方法都执行完了才去做权限校验意义何在?其实这个主要是在 ACL 权限模型中会用到,目标方法执行完毕后,通过 @PostAuthorize 注解去校验目标方法的返回值是否满足相应的权限要求。

从技术角度来讲,@PostAuthorize 注解中也可以使用权限表达式,但是在实际开发中权限表达式一般都是结合 @PreAuthorize 注解一起使用的。@PostAuthorize 包含一个内置对象 returnObject, 表示方法的返回值,开发者可以对返回值进行校验:

@PostAuthorize("returnObject.id==1")
public User getUserById(Integer id) {
    return new User(id, "javaboy");
}

这个表示方法返回的 user 对象的 id 必须为 1,调用才会顺利通过,否则就会抛出异常。

然后通过单元测试对该方法进行测试:

@Test
@WithMockUser(username = "javaboy")
void postAuthorizeTest01() {
    User user = helloService.getUserById(1);
    assertNotNull(user);
    assertEquals(1,user.getId());
    assertEquals("javaboy",user.getUsername());
}

如果调用时传入的参数为 1,单元测试就会顺利通过。

这里先通过这样一个简单的例子来了解一下 @PostAuthorize 注解的基本用法,在第 14 章的 ACL 权限模型讲解中,我们将会再次介绍 @PostAuthorize 注解的其他用法。

@PostFilter

@PostFilter 注解在目标方法执行之后,对目标方法的返回结果进行过滤,该注解中包含了一个内置对象 filterObject,表示目标方法返回的集合/数组中的具体元素:

@PostFilter("filterObject.id%2==0")
public List<User> getAll() {
    List<User> users = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        users.add(new User(i, "javaboy:" + i));
    }
    return users;
}

这段代码表示 getAll 方法的返回值 users 集合中 user 对象的 id 必须为偶数。

然后我们通过单元测试对其进行测试,代码如下:

@Test
@WithMockUser(roles = "ADMIN")
void postFilterTest01() {
    List<User> all = helloService.getAll();
    assertNotNull(all);
    assertEquals(5, all.size());
    assertEquals(2,all.get(1).getId());
}

@secured

@Secured 注解也是 Spring Security 提供的权限注解,不同于前面四个注解,该注解不支持权限表达式,只能做一些简单的权限描述。

@Secured({"ROLE_ADMIN","ROLE_USER"})
public User getUserByUsername(String username) {
    return new User(99, username);
}

这段代码表示用户需要具备 ROLE_ADMIN 或者 ROLE_USER 角色,才能访问 getUserByUsername 方法。

然后我们通过单元测试对其进行测试,代码如下:

@Test
@WithMockUser(roles = "ADMIN")
void securedTest01() {
    User user = helloService.getUserByUsername("javaboy");
    assertNotNull(user);
    assertEquals(99,user.getId());
    assertEquals("javaboy", user.getUsername());
}

注意,这里不需要给角色添加 ROLE_ 前缀,系统会自动添加。

@DenyAll

@DenyAll 是 JSR-250 提供的方法注解,看名字就知道这是拒绝所有访问:

@DenyAll
public String denyAll() {
    return "DenyAll";
}

然后我们通过单元测试对其进行测试,代码如下:

@Test
@WithMockUser(username = "javaboy")
void denyAllTest01() {
    helloService.denyAll();
}

在单元测试过程中,就会抛出异常。

@PermitAll

@PermitAll 也是 JSR-250 提供的方法注解,看名字就知道这是允许所有访问:

@PermitAll
public String permitAll() {
    return "PermitAll";
}

然后我们通过单元测试对其进行测试,代码如下:

@Test
@WithMockUser(username = "javaboy")
void permitAllTest01() {
    String s = helloService.permitAll();
    assertNotNull(s);
    assertEquals("PermitAll", s);
}

@RolesAllowed

@RolesAllowed 也是 JSR-250 提供的注解,可以添加在方法上或者类上,当添加在类上时,表示该注解对类中的所有方法生效:如果类上和方法上都有该注解,并且起冲突,则以方法上的注解为准。我们来看一太简单的案例:

@RolesAllowed({"ADMIN","USER"})
public String rolesAllowed() {
    return "RolesAllowed";
}

这个表示访问 rolesAllowed 方法需要具备 ADMIN 或者 USER 角色,然后我们通过单元测试对其进行测试,代码如下:

@Test
@WithMockUser(roles = "ADMIN")
void rolesAllowedTest01() {
    String s = helloService.rolesAllowed();
    assertNotNull(s);
    assertEquals("RolesAllowed", s);
}

这就是常见的方法权限注解。

原理剖析

MethodSecuritylnterceptor

在 13.4.4 小节中,我们介绍了 AbstractSecurityInterceptor 中的三大方法,当我们基于 URL 请求地址进行权限控制时,使用的 AbstractSecurityInterceptor 实现类是 FilterSecurityInterceptor,而当我们基于方法进行权限控制时,使用的 AbstractSecurityInterceptor 实现类是 MethodSecurityInterceptor。

MethodSecurityInterceptor 提供了基于 AOP Alliance 的方法拦截,该拦截器中所使用的 SecurityMetadataSource 类型为 MethodSecurityMetadataSource。 MethodSecurityInterceptor 中最重要的就是 invoke 方法,我们一起来看一下:

public Object invoke(MethodInvocation mi) throws Throwable {
    InterceptorStatusToken token = super.beforeInvocation(mi);

    Object result;
    try {
        result = mi.proceed();
    }
    finally {
        super.finallyInvocation(token);
    }
    return super.afterInvocation(token, result);
}

invoke 方法的逻辑非常清晰明了。首先调用父类的 beforeInvocation 方法进行权限校验校验通过后,调用 mi.proceed() 方法继续执行目标方法,然后在 finally 代码块中调用 finallyInvocation 方法完成一些清理工作,最后调用父类的 afterInvocation 方法进行请求结果过滤。

在 13.4.4 小节中,我们介绍了 FilterSecurityInterceptor 是通过 ExpressionUrlAuthorizationConfigurer 或者 UrlAuthorizationConfigurer 进行配置的,那么 MethodSecurityInterceptor 又是通过谁配置的呢?在前面的配置中,我们使用到了 @EnableGlobalMethodSecurity 注解,所以就以该注解为线索展开分析。

@EnableGlobalMethodSecurity

@EnableGlobalMethodSecurity 用来开启方法的权限注解,我们来看一下该注解的定义:

@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ GlobalMethodSecuritySelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableGlobalMethodSecurity {
    // 省略其它
}

从该类的定义上可以看到,它引入了一个配置 GlobalMethodSecuritySelector,该类的作用主要是用来导入外部配置类,我们来看一下该类的定义:

final class GlobalMethodSecuritySelector implements ImportSelector {

	public String[] selectImports(AnnotationMetadata importingClassMetadata) {
		Class<EnableGlobalMethodSecurity> annoType = EnableGlobalMethodSecurity.class;
		Map<String, Object> annotationAttributes = importingClassMetadata
				.getAnnotationAttributes(annoType.getName(), false);
		AnnotationAttributes attributes = AnnotationAttributes
				.fromMap(annotationAttributes);
		Assert.notNull(attributes, () -> String.format(
				"@%s is not present on importing class '%s' as expected",
				annoType.getSimpleName(), importingClassMetadata.getClassName()));

		// TODO would be nice if could use BeanClassLoaderAware (does not work)
		Class<?> importingClass = ClassUtils
				.resolveClassName(importingClassMetadata.getClassName(),
						ClassUtils.getDefaultClassLoader());
		boolean skipMethodSecurityConfiguration = GlobalMethodSecurityConfiguration.class
				.isAssignableFrom(importingClass);

		AdviceMode mode = attributes.getEnum("mode");
		boolean isProxy = AdviceMode.PROXY == mode;
		String autoProxyClassName = isProxy ? AutoProxyRegistrar.class
				.getName() : GlobalMethodSecurityAspectJAutoProxyRegistrar.class
				.getName();

		boolean jsr250Enabled = attributes.getBoolean("jsr250Enabled");

		List<String> classNames = new ArrayList<>(4);
		if (isProxy) {
			classNames.add(MethodSecurityMetadataSourceAdvisorRegistrar.class.getName());
		}

		classNames.add(autoProxyClassName);

		if (!skipMethodSecurityConfiguration) {
			classNames.add(GlobalMethodSecurityConfiguration.class.getName());
		}

		if (jsr250Enabled) {
			classNames.add(Jsr250MetadataSourceConfiguration.class.getName());
		}

		return classNames.toArray(new String[0]);
	}
}

这里只有一个 selectImports 方法,该方法的参数 importingClassMetadata中保存了 @EnableGlobalMethodSecurity 注解的元数据,包括各个属性的值、注解是加在哪个配置类上等。

selectImports 方法的逻辑比较简单,要导入的外部配置类有如下几种:

  • MethodSecurityMetadataSourceAdvisorRegistrar:如果使用的是 Spring 自带的 AOP,则该配置类会被导入。该类主要用来向 Spring 容器中注册一个 MethodSecurityMetadataSourceAdvisor 对象,这个对象中定义了 AOP 中的 pointcut 和 advice。

  • autoProxyClassName:注册自动代理创建者,根据不同的代理模式而定。

  • GlobalMethodSecurityConfiguration:这个配置类用来提供 MethodSecurityMetadataSource 和 MethodInterceptor 两个关键对象。如果开发者自定义配置类继承自 GlobalMethodSecurityConfiguration,则这里不会导入这个外部配置类。

  • Jsr250MetadataSourceConfiguration:如果开启了JSR-250 注解,则会导入该配置类。该配置类主要用来提供 JSR-250 注解所需的 Jsr250MethodSecurityMetadataSource 对象。

这四个导入的外部配置类中,MethodSecurityMetadataSourceAdvisorRegistrar 是用来配置 MethodSecurityMetadataSourceAdvisor 的,而 MethodSecurityMetadataSourceAdvisor 则提供 AOP 所需的 pointcut 和 advice。先来看 MethodSecurityMetadataSourceAdvisorRegistrar:

class MethodSecurityMetadataSourceAdvisorRegistrar implements
		ImportBeanDefinitionRegistrar {

	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
			BeanDefinitionRegistry registry) {

		BeanDefinitionBuilder advisor = BeanDefinitionBuilder
				.rootBeanDefinition(MethodSecurityMetadataSourceAdvisor.class);
		advisor.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
		advisor.addConstructorArgValue("methodSecurityInterceptor");
		advisor.addConstructorArgReference("methodSecurityMetadataSource");
		advisor.addConstructorArgValue("methodSecurityMetadataSource");

		MultiValueMap<String, Object> attributes = importingClassMetadata.getAllAnnotationAttributes(EnableGlobalMethodSecurity.class.getName());
		Integer order = (Integer) attributes.getFirst("order");
		if (order != null) {
			advisor.addPropertyValue("order", order);
		}

		registry.registerBeanDefinition("metaDataSourceAdvisor",
				advisor.getBeanDefinition());
	}
}

这个类很好理解,在 registerBeanDefinitions 方法中,首先定义 BeanDefinitionBuilder,然后给目标对象 MethodSecurityMetadataSourceAdvisor 的构造方法设置参数,参数一共有三个:第一个是要引用的 MethodInterceptor 对象名;第二是要引用的 MethodSecurityMetadataSource 对象名:第三个参数和第二人一样,只不过一人是引用,一个是字符串。所有属性都配置好之后,将其注册到 Spring 容器中。

我们再来看 MethodSecurityMetadataSourceAdvisor:

public class MethodSecurityMetadataSourceAdvisor extends AbstractPointcutAdvisor
		implements BeanFactoryAware {

	private transient MethodSecurityMetadataSource attributeSource;
	private transient MethodInterceptor interceptor;
	private final Pointcut pointcut = new MethodSecurityMetadataSourcePointcut();
	private BeanFactory beanFactory;
	private final String adviceBeanName;
	private final String metadataSourceBeanName;
	private transient volatile Object adviceMonitor = new Object();

	public MethodSecurityMetadataSourceAdvisor(String adviceBeanName,
			MethodSecurityMetadataSource attributeSource, String attributeSourceBeanName) {
		Assert.notNull(adviceBeanName, "The adviceBeanName cannot be null");
		Assert.notNull(attributeSource, "The attributeSource cannot be null");
		Assert.notNull(attributeSourceBeanName,
				"The attributeSourceBeanName cannot be null");

		this.adviceBeanName = adviceBeanName;
		this.attributeSource = attributeSource;
		this.metadataSourceBeanName = attributeSourceBeanName;
	}

	public Pointcut getPointcut() {
		return pointcut;
	}

	public Advice getAdvice() {
		synchronized (this.adviceMonitor) {
			if (interceptor == null) {
				Assert.notNull(adviceBeanName,
						"'adviceBeanName' must be set for use with bean factory lookup.");
				Assert.state(beanFactory != null,
						"BeanFactory must be set to resolve 'adviceBeanName'");
				interceptor = beanFactory.getBean(this.adviceBeanName,
					MethodInterceptor.class);
			}
			return interceptor;
		}
	}

	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
		this.beanFactory = beanFactory;
	}

	class MethodSecurityMetadataSourcePointcut extends StaticMethodMatcherPointcut
			implements Serializable {
		@SuppressWarnings("unchecked")
		public boolean matches(Method m, Class targetClass) {
			Collection attributes = attributeSource.getAttributes(m, targetClass);
			return attributes != null && !attributes.isEmpty();
		}
	}
}

MethodSecurityMetadataSourceAdvisor 继承自 AbstractPointcutAdvisor,主要定义了 AOP 的 pointcut 和 advice。MethodSecurityMetadataSourceAdvisor 构造方法所需的三个参数就是前 面 MethodSecurityMetadataSourceAdvisorRegistrar 类中提供的三个参数。

pointcut 也就是切点,可以简单理解为方法的拦截规则,即哪些方法需要拦截,哪些方法不需要拦截。不用看代码我们也知道,加了权限注解的方法需要拦截下来,没加权限注解的方法则不需要拦截。

这里的 pointcut 对象就是内部类 MethodSecurityMetadataSourcePointcut,在它的 matches 方法中,定义了具体的拦截规则。通过 attributeSource.getAttributes 方法去查看目标方法上有没有相应的权限注解,如果有,则返回 true,目标方法就被拦截下来;如果没有,则返回 false,目标方法就不会被拦截。这里的 attributeSource 实际上就是 MethodSecurityMetadataSource 对象,也就是我们在 13.3.6 小节中介绍的提供权限元数据的类。

advice 也就是增强/通知,就是将方法拦截下来之后要增强的功能。advice 由 getAdvice() 方法返回,在该方法内部,就是去 Spring 容器中查找一个名为 methodSecurityInterceptor 的 MethodInterceptor 对象,这就是 advice。

此时,读者已经明白了 AOP 的切点和增强/通知是如何定义的了,这里涉及两个关键对象:一个名为 methodSecurityInterceptor 的 MethodInterceptor 对象和一个名为 methodSecurityMetadataSource 的 MethodSecurityMetadataSource 对象。

这两个关键对象在 GlobalMethodSecurityConfiguration 类中定义,相关的方法比较长,我们先来看 MethodSecurityMetadataSource 对象的定义:

@Bean
public MethodSecurityMetadataSource methodSecurityMetadataSource() {
    List<MethodSecurityMetadataSource> sources = new ArrayList<>();
    ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
            getExpressionHandler());
    MethodSecurityMetadataSource customMethodSecurityMetadataSource = customMethodSecurityMetadataSource();
    if (customMethodSecurityMetadataSource != null) {
        sources.add(customMethodSecurityMetadataSource);
    }

    boolean hasCustom = customMethodSecurityMetadataSource != null;
    boolean isPrePostEnabled = prePostEnabled();
    boolean isSecuredEnabled = securedEnabled();
    boolean isJsr250Enabled = jsr250Enabled();

    if (!isPrePostEnabled && !isSecuredEnabled && !isJsr250Enabled && !hasCustom) {
        throw new IllegalStateException("In the composition of all global method configuration, " +
                "no annotation support was actually activated");
    }

    if (isPrePostEnabled) {
        sources.add(new PrePostAnnotationSecurityMetadataSource(attributeFactory));
    }
    if (isSecuredEnabled) {
        sources.add(new SecuredAnnotationSecurityMetadataSource());
    }
    if (isJsr250Enabled) {
        GrantedAuthorityDefaults grantedAuthorityDefaults =
                getSingleBeanOrNull(GrantedAuthorityDefaults.class);
        Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource = this.context.getBean(Jsr250MethodSecurityMetadataSource.class);
        if (grantedAuthorityDefaults != null) {
            jsr250MethodSecurityMetadataSource.setDefaultRolePrefix(
                    grantedAuthorityDefaults.getRolePrefix());
        }
        sources.add(jsr250MethodSecurityMetadataSource);
    }
    return new DelegatingMethodSecurityMetadataSource(sources);
}

可以看到,这里首先创建了一个 List 集合,用来保存所有的 MethodSecurityMetadataSource 对象,然后调用 customMethodSecurityMetadataSource 方法去获取自定义的 MethodSecurityMetadataSource,默认情况下该方法返回 null,如果项目有需要,开发者可以重写 customMethodSecurityMetadataSource 方法来提供自定义的 MethodSecurityMetadataSource 对象。接下来就是根据注解中配置的属性值,来向 sources 集合中添加相应的 MethodSecurityMetadataSource 对象:

  • 如果 @EnableGlobalMethodSecurity 注解配置了 prePostEnabled=true,则加入 PrePostAnnotationSecurityMetadataSource 对象来解析相应的注解。

  • 如果 @EnableGlobalMethodSecurity 注解配置了 securedEnabled=true,则加入 SecuredAnnotationSecurityMetadataSource 对象来解析相应的注解。

  • 如果 @EnableGlobalMethodSecurity 注解配置了 jsr250Enabled=true,则加入 Jsr250MethodSecurityMetadataSource 对象来解析相应的注解。

  • 最后构建一个代理对象 DelegatingMethodSecurityMetadataSource 返回即可。

可以看到,默认提供的 MethodSecurityMetadataSource 对象实际上是一个代理对象,它包含多个不同的 MethodSecurityMetadataSource 实例。

回顾前面所讲的切点定义,在判断一个方法是否需要被拦截下来时,由这些被代理的对象逐个去解析目标方法是否含有相应的注解(例如,PrePostAnnotationSecurityMetadataSource 可以检查出目标方法是否含有 @PostAuthorize、@PostFilter、@PreAuthorize 以及 @PreFilter 四种注解),如果有,则请求就会被拦截下来。

举个反例,如果开发者在项目中使用了 @Secured 注解,但是却没有在 @EnableGlobalMethodSecurity 注解中配置 securedEnabled=true,那么这里就不会加入 SecuredAnnotationSecurityMetadataSource 对象到代理对象中去,进而导致在切点定义的方法中,SecuredAnnotationSecurityMetadataSource 对象不会参与到目标方法注解的解析中,而其他的 SecurityMetadataSource 又无法解析目标方法上的 @Secured,所以最终目标方法就不会被拦截。因此,使用哪个权限注解,一定要先在 @EnableGlobalMethodSecurity 中开启对应的配置。

再来看 MethodInterceptor 的定义:

@Bean
public MethodInterceptor methodSecurityInterceptor(MethodSecurityMetadataSource methodSecurityMetadataSource) {
    this.methodSecurityInterceptor = isAspectJ()
            ? new AspectJMethodSecurityInterceptor()
            : new MethodSecurityInterceptor();
    methodSecurityInterceptor.setAccessDecisionManager(accessDecisionManager());
    methodSecurityInterceptor.setAfterInvocationManager(afterInvocationManager());
    methodSecurityInterceptor
            .setSecurityMetadataSource(methodSecurityMetadataSource);
    RunAsManager runAsManager = runAsManager();
    if (runAsManager != null) {
        methodSecurityInterceptor.setRunAsManager(runAsManager);
    }

    return this.methodSecurityInterceptor;
}

protected AccessDecisionManager accessDecisionManager() {
    List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
    if (prePostEnabled()) {
        ExpressionBasedPreInvocationAdvice expressionAdvice =
                new ExpressionBasedPreInvocationAdvice();
        expressionAdvice.setExpressionHandler(getExpressionHandler());
        decisionVoters
                .add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice));
    }
    if (jsr250Enabled()) {
        decisionVoters.add(new Jsr250Voter());
    }
    RoleVoter roleVoter = new RoleVoter();
    GrantedAuthorityDefaults grantedAuthorityDefaults =
            getSingleBeanOrNull(GrantedAuthorityDefaults.class);
    if (grantedAuthorityDefaults != null) {
        roleVoter.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());
    }
    decisionVoters.add(roleVoter);
    decisionVoters.add(new AuthenticatedVoter());
    return new AffirmativeBased(decisionVoters);
}

protected AfterInvocationManager afterInvocationManager() {
    if (prePostEnabled()) {
        AfterInvocationProviderManager invocationProviderManager = new AfterInvocationProviderManager();
        ExpressionBasedPostInvocationAdvice postAdvice = new ExpressionBasedPostInvocationAdvice(
                getExpressionHandler());
        PostInvocationAdviceProvider postInvocationAdviceProvider = new PostInvocationAdviceProvider(
                postAdvice);
        List<AfterInvocationProvider> afterInvocationProviders = new ArrayList<>();
        afterInvocationProviders.add(postInvocationAdviceProvider);
        invocationProviderManager.setProviders(afterInvocationProviders);
        return invocationProviderManager;
    }
    return null;
}

MethodInterceptor 的创建,首先看代理的方式,默认使用 Spring 自带的 AOP,所以使用 MethodSecurityInterceptor 来创建对应的 MethodInterceptor 实例。然后给 methodSecurityInterceptor 对象设置AccessDecisionManager 决策管理器,默认的决策管理器是 AffirmativeBased,根据 @EnableGlobalMethodSecurity 注解的配置,在角色管理器中配置不同的投票器;接下来给 methodSecurityInterceptor 配置后置处理器,如果 @EnableGlobalMethodSecurity 注解配置了 prePostEnabled=true,则添加一个后置处理器 PostInvocationAdviceProvider,该类用来处理 @PostAuthorize 和 @PostFilter 两个注解;最后再把前面创建好的 MethodSecurityMetadataSource 对象配置给 methodSecurityInterceptor。

至于 methodSecurityInterceptor 对象的工作逻辑,我们在本小节一开始就已经介绍了。