這兩天因爲公司項目的需求,對 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-1html
想要找個框架的切入點必須對框架如何工做,源碼要熟悉,否則很難找到一個合適的切入點。有點見縫插針的意思,首先就須要找到一個適合「插針」的位置。java
FilterSecurityInterceptor 過濾器是 Spring Security 過濾器鏈條中的最後一個過濾器,它的任務是來最終決定一個請求是否能夠被容許訪問。web
org.springframework.security.web.access.intercept.FilterSecurityInterceptor#invoke 函數源碼:這個函數中作了調用下一個過濾器的操做,也就是這行代碼 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()) 。由於 FilterSecurityInterceptor 是Security 過濾器鏈條中的最後一個過濾器,再去調用下一個過濾器就是調用原始過濾器鏈條中的下一個過濾器了,這也就意味着請求是被容許訪問的。可是在調用下一個過濾器以前還有一行代碼 ,InterceptorStatusToken token = super.beforeInvocation(fi); 這一行代碼就會決定本次請求是否會被放行。spring
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 函數源碼:這個函數作的事情大體是對此次請求是禁止訪問仍是容許訪問進行投票,若是投票都經過的話就容許訪問,若是有一票反對就會禁止訪問拋出異常結束後續處理流程。投票的依據就是經過這行代碼 express
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object); 獲取到的。這行代碼也就是我實現功能的切入點。它先獲取了一個 SecurityMetadataSource 對象,而後經過這個對象獲取了投票的依據。 個人思路就是自定義 SecurityMetadataSource 類的子類,來替換掉 FilterSecurityInterceptor 中的 SecurityMetadataSource 實例。apache
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 對象被設置。app
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 的實現類。maven
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 對象。ide
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; } }
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 實例對象。