SpringSecurity动态控制权限

  • 2020 年 2 月 17 日
  • 笔记

提前预祝各位新春快乐,愿新的一年好运通通“鼠”于你

定义

所谓最短路径问题是指:如果从图中某一顶点(源点)到达另一顶点(终点)的路径可能不止一条,如何找到一条

前言

SpringSecurity做权限校验各位估计已经都会了,它可以实现角色授权、权限授权。它用起来可以说是简单到爆炸,但是有下面的需求,我就陷入了沉思(直接搜索引擎)。

需求:如何通过修改数据库中某个角色的对某一访问路径的权限,进而动态的限制该角色的权限。

即是:在权限管理界面禁用某角色的对某URL访问权限,对应的该角色用户就立马不能这URL访问了。(shrio可能很简单,但我没研究过,本文主要是SpringSecurity)

以下都是来自这位兄台的解答,我就是闲着,整理一下:http://www.chinacion.cn/article/5023.html,

问题原因

SpringSecurity对权限的进行配置的关键代码如下:

httpSecurity.authorizeRequests().antMatchers(rp.getUrls()).hasAnyRole(rp.getRoles());

这也就意味着我们在启动的时候就已经硬编码的写进到SpringSecurity里,没法在对其进行动态修改,每次修改都要重启应用。

那么我们有没有办法让其通过读取数据库的配置,进行动态地修改权限呢?还是那位兄台阅读了SpringSecurity源码,帮我们找到一个合适的切入点。那么我接着往下帮大家理一理。

1、寻找切入点

从上面的httpSecurity入手,调试跟踪到方法performBuild(),我们可以看到这里有12个拦截器Filter,其中最后一个FilterSecurityInterceptor就是本次的入口点。

1.1、FilterSecurityInterceptor

FilterSecurityInterceptor 过滤器是 Spring Security 过滤器链条中的最后一个过滤器,它的任务是来最终决定一个请求是否可以被允许访问。

org.springframework.security.web.access.intercept.FilterSecurityInterceptor#invoke 函数源码:这个函数中做了调用下一个过滤器的操作,也就是这行代码 fi.getChain().doFilter(fi.getRequest(),fi.getResponse()).

因为 FilterSecurityInterceptor 是Security 过滤器链条中的最后一个过滤器,再去调用下一个过滤器就是调用原始过滤器链条中的下一个过滤器了,这也就意味着请求是被允许访问的。但是在调用下一个过滤器之前还有一行代码

InterceptorStatusToken token = super.beforeInvocation(fi);

这一行代码就会决定本次请求是否会被放行。

public void invoke(FilterInvocation fi) throws IOException, ServletException {          if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest) {              fi.getChain().doFilter(fi.getRequest(), fi.getResponse());          } else {              if (fi.getRequest() != null && this.observeOncePerRequest) {                  fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);              }                InterceptorStatusToken token = super.beforeInvocation(fi);              try {                  fi.getChain().doFilter(fi.getRequest(), fi.getResponse());              } finally {                  super.finallyInvocation(token);              }              super.afterInvocation(token, (Object)null);          }      }
1.2、AbstractSecurityInterceptor.beforeInvocation

org.springframework.security.access.intercept.AbstractSecurityInterceptor#beforeInvocation 函数源码:这个函数做的事情大致是对这次请求是禁止访问还是允许访问进行投票,如果投票都通过的话就允许访问,如果有一票反对就会禁止访问抛出异常结束后续处理流程。投票的依据就是通过这行代码 Collectionattributes = this.obtainSecurityMetadataSource().getAttributes(object); 获取到的。

这行代码也就是我实现功能的切入点。

它先获取了一个 SecurityMetadataSource 对象,然后通过这个对象获取了投票的依据。

我的思路就是自定义 SecurityMetadataSource 类的子类,来替换掉 FilterSecurityInterceptor 中的 SecurityMetadataSource 实例。

protected InterceptorStatusToken beforeInvocation(Object object) {          Assert.notNull(object, "Object was null");          boolean debug = this.logger.isDebugEnabled();          if (!this.getSecureObjectClass().isAssignableFrom(object.getClass())) {              throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName() + " but AbstractSecurityInterceptor only configured to support secure objects of type: " + this.getSecureObjectClass());          } else {              //切入点              Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);              if (attributes != null && !attributes.isEmpty()) {                  if (debug) {                      this.logger.debug("Secure object: " + object + "; Attributes: " + attributes);                  }                    if (SecurityContextHolder.getContext().getAuthentication() == null) {                      this.credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes);                  }                    Authentication authenticated = this.authenticateIfRequired();                    try {                      this.accessDecisionManager.decide(authenticated, object, attributes);                  } catch (AccessDeniedException var7) {                      this.publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, var7));                      throw var7;                  }                    if (debug) {                      this.logger.debug("Authorization successful");                  }                    if (this.publishAuthorizationSuccess) {                      this.publishEvent(new AuthorizedEvent(object, attributes, authenticated));                  }                    Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);                  if (runAs == null) {                      if (debug) {                          this.logger.debug("RunAsManager did not change Authentication object");                      }                        return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);                  } else {                      if (debug) {                          this.logger.debug("Switching to RunAs Authentication: " + runAs);                      }                        SecurityContext origCtx = SecurityContextHolder.getContext();                      SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());                      SecurityContextHolder.getContext().setAuthentication(runAs);                      return new InterceptorStatusToken(origCtx, true, attributes, object);                  }              } else if (this.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'");              } else {                  if (debug) {                      this.logger.debug("Public object - authentication not attempted");                  }                    this.publishEvent(new PublicInvocationEvent(object));                  return null;              }          }      }

2、替换 FilterSecurityInterceptor 中的 SecurityMetadataSource

我的目的是替换掉 FilterSecurityInterceptor 中的 SecurityMetadataSource 实例 , 而不是去替换掉原有的 FilterSecurityInterceptor , 如果要替换掉原有的 FilterSecurityInterceptor 那么工作量就变大了,所以替换掉原有的 FilterSecurityInterceptor 并不是一个好的选择。

首先我需要找到 FilterSecurityInterceptor 对象是在什么时候被实例化的。

通过使用代码搜索找到 FilterSecurityInterceptor 的实例化位置:org.springframework.security.config.annotation.web.configurers.AbstractInterceptUrlConfigurer#createFilterSecurityInterceptor , 也是在这个函数中 SecurityMetadataSource 对象被设置。

private FilterSecurityInterceptor createFilterSecurityInterceptor(H http,              FilterInvocationSecurityMetadataSource metadataSource,              AuthenticationManager authenticationManager) throws Exception {          FilterSecurityInterceptor securityInterceptor = new FilterSecurityInterceptor();          securityInterceptor.setSecurityMetadataSource(metadataSource);          securityInterceptor.setAccessDecisionManager(getAccessDecisionManager(http));          securityInterceptor.setAuthenticationManager(authenticationManager);          securityInterceptor.afterPropertiesSet();          return securityInterceptor;      }

createFilterSecurityInterceptor 函数被调用的位置在 :

org.springframework.security.config.annotation.web.configurers.AbstractInterceptUrlConfigurer#configure

这里关键的一行代码是 :

securityInterceptor = postProcess(securityInterceptor);

@Override      public void configure(H http) throws Exception {          FilterInvocationSecurityMetadataSource metadataSource = createMetadataSource(http);          if (metadataSource == null) {              return;          }          FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor(                  http, metadataSource, http.getSharedObject(AuthenticationManager.class));          if (filterSecurityInterceptorOncePerRequest != null) {              securityInterceptor                      .setObserveOncePerRequest(filterSecurityInterceptorOncePerRequest);          }          securityInterceptor = postProcess(securityInterceptor);          http.addFilter(securityInterceptor);          http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor);      }

org.springframework.security.config.annotation.SecurityConfigurerAdapter#postProcess 函数作用 :这个函数中使用了一个 objectPostProcessor 成员变量去调用了 postProcess 函objectPostProcessor 成员变量默认是 org.springframework.security.config.annotation.SecurityConfigurerAdapter.CompositeObjectPostProcessor 的实现类。

protected <T> T postProcess(T object) {          return (T) this.objectPostProcessor.postProcess(object);      }

org.springframework.security.config.annotation.SecurityConfigurerAdapter.CompositeObjectPostProcessor#postProcess 函数源码:这个类的 postProcess 函数中获取到了多个 ObjectPostProcessor 对象,循环的进行调用。看到这里我就找到解决我的问题的方法了,我提供一个 ObjectPostProcessor 实例对象添加到这个 ObjectPostProcessor 对象的列表中,然后在我自定义的 ObjectPostProcessor 对象中就可以获取到原始的 FilterSecurityInterceptor 对象,然后对它进行操作,替换掉原有的 SecurityMetadataSource 对象。

public Object postProcess(Object object) {              for (ObjectPostProcessor opp : postProcessors) {                  Class<?> oppClass = opp.getClass();                  Class<?> oppType = GenericTypeResolver.resolveTypeArgument(oppClass,                          ObjectPostProcessor.class);                  if (oppType == null || oppType.isAssignableFrom(object.getClass())) {                      object = opp.postProcess(object);                  }              }              return object;          }

我进行替换 SecurityMetadataSource 操作的代码 :

package org.hepeng.commons.spring.security.web;    import org.springframework.security.config.annotation.ObjectPostProcessor;  import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;    /**   * @author he peng   */  public class CustomizeSecurityMetadataSourceObjectPostProcessor implements ObjectPostProcessor<FilterSecurityInterceptor> {        private SecurityConfigAttributeLoader securityConfigAttributeLoader;        public CustomizeSecurityMetadataSourceObjectPostProcessor(SecurityConfigAttributeLoader securityConfigAttributeLoader) {          this.securityConfigAttributeLoader = securityConfigAttributeLoader;      }        @Override      public <O extends FilterSecurityInterceptor> O postProcess(O object) {          FilterSecurityInterceptor interceptor = object;            CustomizeConfigSourceFilterInvocationSecurityMetadataSource metadataSource =                  new CustomizeConfigSourceFilterInvocationSecurityMetadataSource(                          interceptor.obtainSecurityMetadataSource() , securityConfigAttributeLoader);          interceptor.setSecurityMetadataSource(metadataSource);          return (O) interceptor;      }  }

3、重写自定义 SecurityMetadataSource

org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer#createMetadataSource 函数

在实例化 FilterSecurityInterceptor 对象之前被调用。Spring Security 默认提供了 ExpressionBasedFilterInvocationSecurityMetadataSource 的实例。我的思路是模仿这个类中 getAttributes 函数的实现。

看了这个类的源码后发现这个类中没有重写 getAttributes 函数,而是使用父类 DefaultFilterInvocationSecurityMetadataSource 的 getAttributes 函数。

@Override      final ExpressionBasedFilterInvocationSecurityMetadataSource createMetadataSource(              H http) {          LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = REGISTRY                  .createRequestMap();          if (requestMap.isEmpty()) {              throw new IllegalStateException(                      "At least one mapping is required (i.e. authorizeRequests().anyRequest().authenticated())");          }          return new ExpressionBasedFilterInvocationSecurityMetadataSource(requestMap,                  getExpressionHandler(http));      }

org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource#getAttributes 源码:

这就去操作了 requestMap 这个成员变量 , 这个成员变量的类型是 :Map<requestmatcher, collection> 并且这个成员变量的值是在 ExpressionBasedFilterInvocationSecurityMetadataSource 对象的构造函数中进行传递给父类的。

public Collection<ConfigAttribute> getAttributes(Object object) {          final HttpServletRequest request = ((FilterInvocation) object).getRequest();          for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap                  .entrySet()) {              if (entry.getKey().matches(request)) {                  return entry.getValue();              }          }          return null;      }

ExpressionBasedFilterInvocationSecurityMetadataSource 源码:在构造函数中就通过 processMap 函数完成了父类构造函数所需参数的创建。关键就是这个 org.springframework.security.web.access.expression.ExpressionBasedFilterInvocationSecurityMetadataSource#processMap 函数。我也需要调用这个 processMap 函数,但是这个函数是 private 的没法直接调用, 所以只能是通过反射的方式调用。

public ExpressionBasedFilterInvocationSecurityMetadataSource(              LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap,              SecurityExpressionHandler<FilterInvocation> expressionHandler) {          super(processMap(requestMap, expressionHandler.getExpressionParser()));          Assert.notNull(expressionHandler,                  "A non-null SecurityExpressionHandler is required");      }        private static LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> processMap(              LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap,              ExpressionParser parser) {          Assert.notNull(parser, "SecurityExpressionHandler returned a null parser object");            LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestToExpressionAttributesMap = new LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>>(                  requestMap);            for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap                  .entrySet()) {              RequestMatcher request = entry.getKey();              Assert.isTrue(entry.getValue().size() == 1,                      "Expected a single expression attribute for " + request);              ArrayList<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>(1);              String expression = entry.getValue().toArray(new ConfigAttribute[1])[0]                      .getAttribute();              logger.debug("Adding web access control expression '" + expression + "', for "                      + request);                AbstractVariableEvaluationContextPostProcessor postProcessor = createPostProcessor(                      request);              try {                  attributes.add(new WebExpressionConfigAttribute(                          parser.parseExpression(expression), postProcessor));              }              catch (ParseException e) {                  throw new IllegalArgumentException(                          "Failed to parse expression '" + expression + "'");              }                requestToExpressionAttributesMap.put(request, attributes);          }            return requestToExpressionAttributesMap;      }

我自定义的 SecurityMetadataSource 源码 :

package org.hepeng.commons.spring.security.web;    import org.apache.commons.collections.CollectionUtils;  import org.apache.commons.collections.MapUtils;  import org.joor.Reflect;  import org.springframework.expression.ExpressionParser;  import org.springframework.security.access.ConfigAttribute;  import org.springframework.security.access.SecurityMetadataSource;  import org.springframework.security.access.expression.SecurityExpressionHandler;  import org.springframework.security.web.FilterInvocation;  import org.springframework.security.web.access.expression.ExpressionBasedFilterInvocationSecurityMetadataSource;  import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;  import org.springframework.security.web.util.matcher.RequestMatcher;    import javax.servlet.http.HttpServletRequest;  import java.util.ArrayList;  import java.util.Collection;  import java.util.LinkedHashMap;  import java.util.Map;  import java.util.Objects;    /**   * @author he peng   */  public class CustomizeConfigSourceFilterInvocationSecurityMetadataSource extends DefaultFilterInvocationSecurityMetadataSource {        private static final Reflect REFLECT = Reflect.on(ExpressionBasedFilterInvocationSecurityMetadataSource.class);        private SecurityMetadataSource delegate;      private SecurityConfigAttributeLoader metadataSourceLoader;      private ExpressionParser expressionParser;        public CustomizeConfigSourceFilterInvocationSecurityMetadataSource(              SecurityMetadataSource delegate ,              SecurityConfigAttributeLoader metadataSourceLoader) {          super(new LinkedHashMap<>());          this.delegate = delegate;          this.metadataSourceLoader = metadataSourceLoader;            copyDelegateRequestMap();      }        private void copyDelegateRequestMap() {          Reflect reflect = Reflect.on(this);          reflect.set("requestMap" , getDelegateRequestMap());      }        private LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> getDelegateRequestMap() {          Reflect reflect = Reflect.on(this.delegate);          return reflect.field("requestMap").get();      }        @Override      public Collection<ConfigAttribute> getAttributes(Object object) {          final HttpServletRequest request = ((FilterInvocation) object).getRequest();          Collection<ConfigAttribute> configAttributes = new ArrayList<>();          LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap =                  this.metadataSourceLoader.loadConfigAttribute(request);            if (MapUtils.isEmpty(requestMap)) {              configAttributes.addAll(this.delegate.getAttributes(object));              return configAttributes;          }            if (Objects.isNull(this.expressionParser)) {              SecurityExpressionHandler securityExpressionHandler = GlobalSecurityExpressionHandlerCacheObjectPostProcessor.getSecurityExpressionHandler();              if (Objects.isNull(securityExpressionHandler)) {                  throw new NullPointerException(SecurityExpressionHandler.class.getName() + " is null");              }              this.expressionParser = securityExpressionHandler.getExpressionParser();          }              LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> webExpressionRequestMap =                  REFLECT.call("processMap" , requestMap , this.expressionParser).get();          for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : webExpressionRequestMap.entrySet()) {              if (entry.getKey().matches(request)) {                  configAttributes.addAll(entry.getValue());                  break;              }          }            if (CollectionUtils.isEmpty(configAttributes)) {              configAttributes.addAll(this.delegate.getAttributes(object));          }            return configAttributes;      }  }

最终实现

第一步:引入maven依赖如下:

<dependency>      <groupId>org.hepeng</groupId>      <artifactId>hp-java-commons</artifactId>      <version>1.1.3</version>  </dependency>

第二步:然后写类JdbcSecurityMetaDataSourceLoader 实现接口SecurityConfigAttributeLoader 。该接口已经封装好了的,你只需与你的数据库表关联起来即可。

public class JdbcSecurityMetaDataSourceLoader implements SecurityConfigAttributeLoader {       //用户或权限的服务类。      private UserService userService;        public JdbcSecurityMetaDataSourceLoader(UserService userService) {          this.userService = userService;      }        @Override      public LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> loadConfigAttribute(HttpServletRequest var1) {          LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMatchers = null;          //查询数据库将路径以及权限          requestMatchers = new LinkedHashMap<>();          SecurityAccessConfigHelper helper = new SecurityAccessConfigHelper();          AntPathRequestMatcher pathRequestMatcher = new AntPathRequestMatcher("/test2");          List<ConfigAttribute> configAttributes = SecurityConfig.createList(helper.hasAnyRole("ADMIN","SUPER").access());            SecurityAccessConfigHelper helper1 = new SecurityAccessConfigHelper();          AntPathRequestMatcher pathRequestMatcher2 = new AntPathRequestMatcher("/test1/**");          AntPathRequestMatcher pathRequestMatcher22 = new AntPathRequestMatcher("/user/**");          List<ConfigAttribute> configAttributes2 = SecurityConfig.createList(helper1.permitAll().access());            SecurityAccessConfigHelper helper2 = new SecurityAccessConfigHelper();          AntPathRequestMatcher pathRequestMatcher3 = new AntPathRequestMatcher("/test2/gggg");          List<ConfigAttribute> configAttributes3 = SecurityConfig.createList(helper2.hasAnyAuthority("write").hasAnyRole("ADMIN").access());            requestMatchers.put(pathRequestMatcher,configAttributes);          requestMatchers.put(pathRequestMatcher2,configAttributes2);          requestMatchers.put(pathRequestMatcher22,configAttributes2);          requestMatchers.put(pathRequestMatcher3,configAttributes3);            return requestMatchers;      }  }

最后一步:在SpringSecurity的配置类中将JdbcSecurityMetaDataSourceLoader加入即可。

httpSecurity.authorizeRequests().anyRequest().authenticated()                  .withObjectPostProcessor(                          new CustomSecurityMetadataSourceObjectPostProcessor(                          new JdbcSecurityMetaDataSourceLoader(userService)))                  .withObjectPostProcessor(new GlobalSecurityExpressionHandlerCacheObjectPostProcessor());

原作者:周娱 原文链接:http://www.chinacion.cn/article/5023.html