SpringSecurity 动态权限

Spring security默认是在代码里约定好权限,真实的业务场景通常需要可以支持动态配置角色访问权限,即在运行时去配置url对应的访问角色。

最简单的方法就是自定义一个Filter去完成权限判断,但这脱离了spring security框架,如何基于spring security优雅的实现呢? 其实只要仔细看看 FilterSecurityInterceptor`源码就知道从哪里找切入点了

要想实现动态权限 需要以下3步

  1. 实现FilterInvocationSecurityMetadataSource 获取请求Url需要的角色列表
  2. 自定义投票器 实现 AccessDecisionVoter 或者 直接重写 AccessDecisionManager 不使用SpringSecurity提供的默认访问策略逻辑
  3. 定义处理 访问无权限的handler 实现 AccessDeniedHandler 在这里可以返回给前端 对应的信息

1.FilterSecurityInterceptor

该过滤器实现了主要的鉴权逻辑,最核心的代码如下

	Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
				.getAttributes(object);

		if (attributes == null || attributes.isEmpty()) {
			if (rejectPublicInvocations) {
				throw new IllegalArgumentException(
						"Secure object invocation "
								+ object
								+ " was denied as public invocations are not allowed via this interceptor. "
								+ "This indicates a configuration error because the "
								+ "rejectPublicInvocations property is set to 'true'");
			}

			if (debug) {
				logger.debug("Public object - authentication not attempted");
			}

			publishEvent(new PublicInvocationEvent(object));

			return null; // no further work post-invocation
		}

		if (debug) {
			logger.debug("Secure object: " + object + "; Attributes: " + attributes);
		}

		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			credentialsNotFound(messages.getMessage(
					"AbstractSecurityInterceptor.authenticationNotFound",
					"An Authentication object was not found in the SecurityContext"),
					object, attributes);
		}

		Authentication authenticated = authenticateIfRequired();

		// Attempt authorization
		try {
			this.accessDecisionManager.decide(authenticated, object, attributes);
		}
		catch (AccessDeniedException accessDeniedException) {
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
					accessDeniedException));

			throw accessDeniedException;
		}

抽取重要的2部分

1.1 获取请求的Url的所需要的 角色Collection

	Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
				.getAttributes(object);


public SecurityMetadataSource obtainSecurityMetadataSource() {
		return this.securityMetadataSource;
	}

private FilterInvocationSecurityMetadataSource securityMetadataSource;

1.2 使用访问决策管理器 去 鉴权

	this.accessDecisionManager.decide(authenticated, object, attributes);

那么我们就从上面两部分进行切入

2.自定义实现 FilterInvocationSecurityMetadataSource接口

FilterInvocationSecurityMetadataSource 提供一个 getAttributes方法 用来获取请求的URL对应需要的角色 下面的代码中,我模拟从数据库取出 路径对应的角色urlRoleMap

可以通过 FilterInvocation获取到 请求的url 动态从数据库中获取 改url需要的角色列表,然后放入Collection ,而它是 SecurityConfig

@Slf4j
@Component
public class RoleSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {


    private final AntPathMatcher antPathMatcher = new AntPathMatcher();


    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        //根据 请求获取 需要的权限
        FilterInvocation filterInvocation = (FilterInvocation) object;
        String url = filterInvocation.getRequestUrl();
				log.info("【请求 url : {}】", url);
        Map<String, List<String>> urlRoleMap = new HashMap();
        urlRoleMap.put("/menu/**", Arrays.asList("admin","editor1"));
        urlRoleMap.put("/user/listByCondition**", Arrays.asList("admin1","editor1"));
        for (Map.Entry<String, List<String>> entry : urlRoleMap.entrySet()) {
            if (antPathMatcher.match(entry.getKey(), url)) {
                String[] array = entry.getValue().toArray(new String[0]);
                return SecurityConfig.createList(array);
            }
        }    
        return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }
     
    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

最后需要把该 自定义的元数据获取类 配置到SpringSecurity 中 通过 withObjectPostProcessor

  .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(
                            O fsi) {
                        fsi.setSecurityMetadataSource(roleSecurityMetadataSource);
                        return fsi;
                    }
                })

3.提供自定义的 投票器Voter

默认 SpringSecurity 提供了3中访问决策 逻辑

AffirmativeBased – 任何一个AccessDecisionVoter返回同意则允许访问
ConsensusBased – 同意投票多于拒绝投票(忽略弃权回答)则允许访问
UnanimousBased – 每个投票者选择弃权或同意则允许访问

我也可以选择 自定义实现AccessDecisionManager ,但是SpringSecurity提供的已经足够用了

所以我们选择 第一种,也是SpringSecurity 默认使用的 AffirmativeBased

3.1 使用 AffirmativeBased 作为 决策访问策略

主要就是遍历实现了AccessDecisionVoter 接口的 投票器,让投票器决定返回结果 支持3种返回结果

int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;
	public void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
		int deny = 0;

		for (AccessDecisionVoter voter : getDecisionVoters()) {
			int result = voter.vote(authentication, object, configAttributes);

			if (logger.isDebugEnabled()) {
				logger.debug("Voter: " + voter + ", returned: " + result);
			}

			switch (result) {
			case AccessDecisionVoter.ACCESS_GRANTED:
				return;

			case AccessDecisionVoter.ACCESS_DENIED:
				deny++;

				break;

			default:
				break;
			}
		}

		if (deny > 0) {
			throw new AccessDeniedException(messages.getMessage(
					"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
		}

		// To get this far, every AccessDecisionVoter abstained
		checkAllowIfAllAbstainDecisions();
	}

3.2 自定义 投票器 实现AccessDecisionVoter

其中入参 authentication 表示当前的认证用户信息, Object object 是指FilterInvocation Collection attributes 是指 FilterInvocationSecurityMetadataSource返回的当前Url请求需要的角色集合

/**
 * 自定义的 投票器
 *
 * @author johnny
 * @create 2020-07-19 上午12:41
 **/
public class RoleBasedVoter implements AccessDecisionVoter<Object> {


    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

    @Override
    public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
        if (authentication == null) {
            return ACCESS_DENIED;
        }
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        for (ConfigAttribute attribute : attributes) {
            if (attribute.getAttribute() == null) {
                continue;
            }
            //默认的SpringSecurity的投票器,比如RoleVoter 的 support方法会去判断角色是否 包含ROLE_前缀
            //我们这里不做这种限制
            if (this.supports(attribute)) {
                for (GrantedAuthority authority : authorities) {
                    if(attribute.getAttribute().equals(authority.getAuthority())){
                        return ACCESS_GRANTED;
                    }
                }
            }
        }
        return ACCESS_DENIED;
    }
}

4.SpringSecurity 配置装载

本配置是在 前后端分离情况下 包含登录、退出、以及无Session状态 和 自定义 JwtTokenFilter ,动态权限等等

   
    @Autowired
    private FilterInvocationSecurityMetadataSource roleSecurityMetadataSource;

	@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .formLogin()
                .loginPage("/auth/login")
                .successHandler(loginSuccessHandler)
                .failureHandler(loginFailHandler)
                .and()
                .logout()
                .logoutUrl("/user/logout")
                .clearAuthentication(true)
                .logoutSuccessHandler(logOutSuccessHandler)
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()
          			 //1.放入修改后的accessDecisionManager
                .accessDecisionManager(customizeAccessDecisionManager())
			           //2.扩展 FilterSecurityInterceptor,放入自定义的FilterInvocationSecurityMetadataSource
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(
                            O fsi) {
                        fsi.setSecurityMetadataSource(roleSecurityMetadataSource);
                        return fsi;
                    }
                })
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .exceptionHandling().authenticationEntryPoint(customizeAuthenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);


        http.addFilterAfter(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

	  //使用自定义角色器,放入 AccessDecisionManager的一个实现 AffirmativeBased 中
    private AccessDecisionManager customizeAccessDecisionManager() {

        List<AccessDecisionVoter<? extends Object>> decisionVoterList
                = Arrays.asList(
                new RoleBasedVoter()
        );
        return new AffirmativeBased(decisionVoterList);
    }

5.总结

本篇主要讲解 SpringSecurity中如何动态权限校验,粒度为请求级别校验,主要过滤器为 FilterSecurityInterceptor 去进行权限校验

涉及到FilterInvocationSecurityMetadataSource获取Url对应的角色信息 , AccessDecisionManager 真正进行访问决策的 以及 AccessDecisionVoter 进行投票的投票器 等 找准切入点 即可很轻松实现。


原文:Johnny小屋
作者;Johnny