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
