spring security 方法级别访问控制源码分析

394 阅读3分钟

方法级别访问控制源码分析

开启

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

@EnableGlobalMethodSecurity 向 IOC 容器导入的 spring bean:

  1. AutoProxyRegistrar (为某些spring bean 创建代理)
  2. MethodSecurityMetadataSourceAdvisorRegistrar。这个 bean 的作用是导入:
    • MethodSecurityMetadataSourceAdvisor bean。内部有一个 MethodInterceptor
  3. AuthenticationConfiguration。这个又会导入下面几个 spring bean:
    • AuthenticationManagerBuilder。实现类:DefaultPasswordEncoderAuthenticationManagerBuilder
  4. MethodInterceptor

MethodSecurityMetadataSourceAdvisor

public class MethodSecurityMetadataSourceAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {

	private transient MethodSecurityMetadataSource attributeSource;

    // 方法拦截器。实际类型是:MethodSecurityInterceptor
    // 标注权限注解的方法,调用的时候,会被这个组件拦截
	private transient MethodInterceptor interceptor;

    // 配置拦截什么方法。这里会拦截 @PreAuthorize, @PostAuthorize 等 security 注解标注的方法
	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;
	}

	
    // 
	class MethodSecurityMetadataSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {

		@Override
		public boolean matches(Method m, Class<?> targetClass) {
			MethodSecurityMetadataSource source = MethodSecurityMetadataSourceAdvisor.this.attributeSource;
			return !CollectionUtils.isEmpty(source.getAttributes(m, targetClass));
		}

	}

}

MethodSecurityInterceptor

public class MethodSecurityInterceptor extends AbstractSecurityInterceptor implements MethodInterceptor {

	private MethodSecurityMetadataSource securityMetadataSource;

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

}

看看其父类 AbstractSecurityInterceptor 有哪些重要的对象

public abstract class AbstractSecurityInterceptor
       implements InitializingBean, ApplicationEventPublisherAware, MessageSourceAware {
       
    // ---
    
   	// 验证能不能访问该方法
	private AccessDecisionManager accessDecisionManager;
    
    // 方法被调用之后,怎么做。比如验证 @PostAuthorize 注解
    private AfterInvocationManager afterInvocationManager;
    
    
    // ---
}
  • 会组合哪些 AccessDecisionManager 呢?需要根据 @EnableGlobalMethodSecurity 这个注解配置

    注解配置投票器
    @EnableGlobalMethodSecurity(prePostEnabled = true)PreInvocationAuthorizationAdviceVoter
    @EnableGlobalMethodSecurity(jsr250Enabled = true)Jsr250Voter

    另加两个 voter

    1. RoleVoter
    2. AuthenticatedVoter

AccessDecisionVoter

spring security 中,一个用户有没有权限访问某些权限注解标注的方法,由 AccessDecisionVoter 决定。

主要有四个:

  1. PreInvocationAuthorizationAdviceVoter. prePostEnabled = true 的时候,会添加。基于 SpEL
  2. Jsr250Voterjsr250Enabled = true 的时候,会添加。 如何使用 jsr250可以看下面
  3. RoleVoter
  4. AuthenticatedVoter

jsr250

提供的注解,主要有三个:

  1. @RolesAllowed
  2. @PermitAll
  3. @DenyAll

可以看到,控制的粒度会很大,要么根据角色,要么全部都可以访问、要么全都不可以访问

@Service
public class MyService {

    @RolesAllowed("ROLE_USER")
    public void userMethod() {
        // 只有具有 "ROLE_USER" 角色的用户可以访问这个方法
    }

    @RolesAllowed({"ROLE_ADMIN", "ROLE_USER"})
    public void adminOrUserMethod() {
        // 只有具有 "ROLE_ADMIN" 或 "ROLE_USER" 角色的用户可以访问这个方法
    }

    @PermitAll
    public void permitAllMethod() {
        // 任何用户都可以访问这个方法
    }

    @DenyAll
    public void denyAllMethod() {
        // 任何用户都不能访问这个方法
    }
}

SpEL

看下 spring security 如何基于 spel 验证 @PreAuthorize 注解的。

创建 root 对象

先说下 spel 中 root object。spel 的表达式中需要访问属性、方法的时候,都会从 root object 对象中获取。

下面的 root object 是 MethodSecurityExpressionRootMethodSecurityExpressionRootSecurityExpressionOperations的实现类

class MethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations{}

SecurityExpressionOperations

public interface SecurityExpressionOperations {

	boolean hasAuthority(String authority);

	boolean hasAnyAuthority(String... authorities);

	boolean hasRole(String role);

	boolean hasAnyRole(String... roles);

	boolean hasPermission(Object target, Object permission);

	boolean hasPermission(Object targetId, String targetType, Object permission);
    
    // ... 省略

}

看到这些方法之后,就会明白为啥 @PreAuthorize 会这样使用:

  • @PreAuthorize("hasRole('user')")
  • @PostAuthorize("hasPermission('foo', 'bar')")
  • @PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER')")
  • @PreAuthorize("hasAuthority('READ_PRIVILEGE')")
  • @PreAuthorize("hasAnyAuthority('READ_PRIVILEGE', 'WRITE_PRIVILEGE')")
	@Override
	protected MethodSecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication,
			MethodInvocation invocation) {
		MethodSecurityExpressionRoot root = new MethodSecurityExpressionRoot(authentication);
		root.setThis(invocation.getThis());
		root.setPermissionEvaluator(getPermissionEvaluator());
		root.setTrustResolver(getTrustResolver());
		root.setRoleHierarchy(getRoleHierarchy());
		root.setDefaultRolePrefix(getDefaultRolePrefix());
		return root;
	}

创建 EvaluationContext

SecurityExpressionOperations root = createSecurityExpressionRoot(authentication, invocation);

// 使用的是 StandardEvaluationContext
StandardEvaluationContext ctx = createEvaluationContextInternal(authentication, invocation);
// 让 spel 中能用 @ 符号引用 spring bean
ctx.setBeanResolver(this.beanResolver);
// 配置 root object
ctx.setRootObject(root);

接下来验证 @PreAutorize 里面的表达式

// 假如是这样的
@PreAuthorize("hasRole('user')")

// 
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parse("hasRole('user')");

// ctx 就是上一步的 StandardEvaluationContext
boolean access = expression.getValue(ctx, Boolean.class);
// access为true,能访问;为 false,不能访问