这两天由于公司项目的需求,对 spring security 的应用过程中需要实现动态的获取 antMatchers ,permitAll , hasAnyRole , hasIpAddress 等这些原本通过硬编码的方式配置的数据。为了让每一个业务服务不用再去处理权限验证等这些和业务无关的逻辑,而是只专注于它所负责的业务,就要将认证、授权统一的放在 API 网关层去处理。但是每个不同的业务服务有的接口需要认证后才能访问,有的接口是不需要认证就可以访问的,有的接口可能是需要某些权限、角色才可以访问。这样依赖 API 网关就必须知道并且能够区分出来每个业务服务的接口哪些是需要认证后才可以访问的,那些接口是不需要经过认证就可以访问的。 为了实现这个功能 spring security 提供的 antMatchers 函数硬编码的方式就不适用了。而是应该提供一个管理端,每个业务服务把他们这些个性化的接口通过管理端去进行配置,统一的存储起来,spring security 在获取这些数据的时候从统一的存储中来获取这些数据。基于这个需求前提我来考虑如何实现这个功能。配套视频讲解地址 : http://www.iqiyi.com/w_19s456x5b5.html?pltfm=11&pos=title&flashvars=videoIsFromQidan%3Ditemviewclk_a#vfrm=5-6-0-1
想要找个框架的切入点必须对框架如何工作,源码要熟悉,不然很难找到一个合适的切入点。有点见缝插针的意思,首先就需要找到一个适合“插针”的位置。
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(FILTER_APPLIED) != null) && observeOncePerRequest) { // filter already applied to this request and user wants us to observe // once-per-request handling, so don't re-do security checking fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { // first time this request being called, so perform security checking if (fi.getRequest() != null) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } }
org.springframework.security.access.intercept.AbstractSecurityInterceptor#beforeInvocation 函数源码:这个函数做的事情大致是对这次请求是禁止访问还是允许访问进行投票,如果投票都通过的话就允许访问,如果有一票反对就会禁止访问抛出异常结束后续处理流程。投票的依据就是通过这行代码
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object); 获取到的。这行代码也就是我实现功能的切入点。它先获取了一个 SecurityMetadataSource 对象,然后通过这个对象获取了投票的依据。 我的思路就是自定义 SecurityMetadataSource 类的子类,来替换掉 FilterSecurityInterceptor 中的 SecurityMetadataSource 实例。
protected InterceptorStatusToken beforeInvocation(Object object) { Assert.notNull(object, "Object was null"); final boolean debug = logger.isDebugEnabled(); if (!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: " + getSecureObjectClass()); } 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; } if (debug) { logger.debug("Authorization successful"); } if (publishAuthorizationSuccess) { publishEvent(new AuthorizedEvent(object, attributes, authenticated)); } // Attempt to run as a different user Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes); if (runAs == null) { if (debug) { logger.debug("RunAsManager did not change Authentication object"); } // no further work post-invocation return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object); } else { if (debug) { logger.debug("Switching to RunAs Authentication: " + runAs); } SecurityContext origCtx = SecurityContextHolder.getContext(); SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext()); SecurityContextHolder.getContext().setAuthentication(runAs); // need to revert to token.Authenticated post-invocation return new InterceptorStatusToken(origCtx, true, attributes, object); } }
我的目的是替换掉 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; } }
2.3 重写自定义 SecurityMetadataSource 中的 org.springframework.security.access.SecurityMetadataSource#getAttributes 函数
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<ConfigAttribute>> 。并且这个成员变量的值是在 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; } }
为了实现解耦的目的我定义了一个 SecurityConfigAttributeLoader 接口 , 这个接口负责从任何指定的地方去读取配置数据。关于该功能的代码我都发布到了 maven 中央仓库中 , 坐标是 :
<dependency>
<groupId>org.hepeng</groupId>
<artifactId>hp-java-commons</artifactId>
<version>1.1.3</version>
</dependency>
使用的时候只需要一行简单的配置代码 , 还有提供一个 SecurityConfigAttributeLoader 接口的实现,配置代码 :org.hepeng.commons.spring.security.web.CustomizeSecurityConfigAttributeSourceConfigurer#public static <T extends ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry> T enable(T configurer) , 这个配置方式会从 Spring 的容器中去寻找一个 SecurityConfigAttributeLoader 实例对象。