spring-security框架源碼改造:根據接口參數驗證權限

一、背景

spring-security作爲一個權限驗證框架,還是很好用的(雖然有點“重”),它能攔截請求,根據請求的路徑、配置的權限碼和定義的權限驗證器進行權限攔截,同時能很方便的和spring、sprign-session等集成。但是我現在有個需求:對於同一個接口的某些參數取不同的值時,可能需要不同的權限驗證。比如:/test/set?type=?,當type==1或type==2的時候需要攔截的權限是不一樣的(由於項目原因,我們目前使用的版本還是 4.x)。

當然,很直觀的想,我們有一些解決方案:

1.在代碼層面進行控制(一般不會這樣做,耦合性較高)

2.通過自定義攔截器實現(一般都這樣做,相當於在原權限攔截的基礎上又加了一層攔截,可以在框架的基礎上加,也可以在容器的基礎上加)

3.改造spring-security源碼,使其支持根據參數值進行權限驗證(本文要介紹的方式,之所以採取這種方式,主要還是爲了深入研究框架)

注:雖然本文要介紹如何改造該框架,但是通常情況下,我都不建議這樣做。要改造源碼,則需要在某一個指定的版本上操作,但是框架可能涉及到更新換代,等你更新框架版本的時候可能就有的事兒做了哦。另外,我們要保證改造不會對框架本身和依賴被改造框架的框架本身造成不良影響(別改出一堆bug出來o(╯□╰)o),就需要我們對框架本身要足夠的瞭解。

好了,廢話不多說,下面我們先介紹spring+spring-security的原理,然後再介紹如何改造。(注:不知道高版本的security有沒有相關的功能擴展,還沒有去研究,這次改造也是在一兩年以前弄的了,只是現在才總結到博客中。另外spring系列的原理我會整理到專欄,這裏先不做闡述哈)

二、spring、spring-security 原理簡介(4.1.6.RELEASE)

由於security的使用在網上一搜一大堆,這裏就不浪費篇幅講解如何配置使用了哈,我們直接進入正題:它包含哪些關鍵組件,這些組件是如何協同工作的。

首先,servlet容器在處理http請求的時候,會獲取對應請求路徑的FilterChain,如果獲取到的FilterChain不爲空,那麼則會執行對應的doFilter方法(這個是servlet的內容,這裏就先不深究,一樣放到專題中總結)。另外,這裏還是得提一下spring的一些簡單“原理”。

根據servlet3.0的規範,servlet容器在啓動應用的時候,會掃描應用下每一個jar包裏面,此文件:META-INF/services/javax.servlet.ServletContainerInitializer 裏配置的ServletContainerInitializer的實現類,啓動運行該示例的onStartup方法,另外可通過註解:@HandlesTypes 綁定它所“感興趣”的類,這些感興趣的類,會通過參數傳入onStartup方法。

而spring正是利用了這一點,其SpringServletContainerInitializer類實現了ServletContainerInitializer接口,並且將其配置在了spring-web/META-INFO/services/javax.servlet.ServletContainerInitializer中,如下圖所示:

而SpringServletContainerInitializer所感興趣的類爲:WebApplicationInitializer.class:

WebApplicationInitializer是一個接口,它模仿了ServletContainerInitializer,只有一個onStartup方法。SpringServletContainerInitializer在啓動(onStartup())的時候,會獲取到所有實現了WebApplicationInitializer接口的類,並且根據它們各自的@Order排序,然後順序執行每個WebApplicationInitializer的onStartup方法。

然後再回到我們的spring-security。在我的項目中是通過編碼方式集成的spring-security:繼承AbstractSecurityWebApplicationInitializer類,通過父類構造方法傳遞一些配置類,如下所示:

具體的配置不是我們的重點,現在我們來看一下這個AbstractSecurityWebApplicationInitializer是何方神聖。

我們可以看到,它實現了我們上面提到的WebApplicationInitializer接口,並且實現了onStartup方法。在其onStartup方法裏做的工作裏就包含有一項操作:註冊springSecurity的過濾器,並且該過濾器是通過DelegatingFilterProxy實現的(delegate爲FilterChainProxy,是通過上圖中的DEFAULT_FILTER_NAME從WebApplicationContext中獲取的,而context中的bean是通過WebSecurityConfiguration注入其中的,且指定了名稱爲該DEFAULT_FILTER_NAME,具體的操作會在spring源碼系列中詳細總結),感興趣的小夥伴可以看看相關源碼,邏輯很簡單。這裏貼一下主要代碼,具體就是生成一個過濾器,註冊到servletContext,之後的所有請求(/*)都會經過它:

同樣的,spring-session也是通過類似的操作開始運作的,經過filter註冊之後,我們一個請求可能會包含很多的filter,比如我的項目有spring-session、spring-security、spring-MVC、還有編碼的過濾器,整個過濾器鏈看起來是這個樣子的:

springSessionRepositoryFilter->springSecurityFilterChain->characterEncodingFilter->dispathcerServlet

其中還有很多細節,但是並不是我們現在的關注重點,我們要改造springSecurity,所以還是看springSecurityFilterChain這個東東。

我們上面說了,springSecurityFilterChain其實是由DelegatingFilterProxy實現的,而其delegate就是個FilterChainProxy,在doFilter的時候會根據當前的request(比如請求/test/set)獲取對應的過濾器列表。它緩存了一些路徑和其對應的過濾器列表,方便請求來的時候直接匹配,比如我們配置了一些不需要權限攔截的路徑,那麼它們排在前面,如果匹配成功就沒有權限相關的驗證了,否則會有另一個filter列表對請求進行過濾處理。而這個filter列表會被封裝成一個VirtualFilterChain,由它進行處理。這是典型的責任鏈模式應用,大家可以參考其實現運用到自己的項目中(如果需要的話)。

而這個filter列表有很多的filter,下圖是我本地項目打斷點截的圖,可以看到有15個filter(其中有我自己加的filter),每個filter都有自己的作用,密碼驗證相關的、記住密碼的等等,我們主要看和權限驗證相關的:FilterSecurityInterceptor(下圖中的最後一個)

FilterSecurityInterceptor的主要任務是從緩存中獲取當前請求路徑的權限配置,這個緩存的配置來源於我們的配置文件。按照我們改造之前的流程的話,是這樣的:

1、我們在配置文件中配置了 intercept-url,比如spring-security.xml中有如下配置:

<http auto-config="true" access-decision-manager-ref="myAccessDecisionManager">

      <intercept-url pattern="/test/set" access="30"/>

</http>

2、框架啓動的時候會解析該配置文件,將這些intercept-url的配置緩存起來

3、FilterSecurityInterceptor在過濾請求的時候,會根據請求路徑從緩存中獲取對應的attribute列表,傳遞到我們指定的AccessDecisionManager(myAccessDecisionManager)中處理

這裏還需要我們詳細看看,配置文件是如何加載解析的。首先還是要從spring說起,spring有個基礎配置文件,大家都很熟悉:applicationContext.xml(另外還有個ContextLoaderListener),不論是在web.xml配置,還是使用註解,我們會引入這個xml文件,spring在啓動的時候會解析該文件,我們集成spring-security的時候會在applicationContext.xml中通過<import >標籤引入spring-security.xml配置文件:

<import resource="classpath:spring/spring-security.xml"/>

spring會解析resource標籤,然後解析spring-security.xml文件,而spring-security.xml這個文件並不是由解析applicationContext.xml文件的解析器解析的,這裏涉及到spring處理子項目xml的一些方式,我們簡單介紹一下。

首先,在spring-security.xml中配置有namespaceUri:

就是箭頭指向的這個:xmlns="http://www.springframework.org/schema/security"。它是幹嘛的呢?spring在解析該xml的是時候,會先拿到這個namespaceUri,然後根據這個uri去獲取它的解析器類全限定名,怎麼獲取呢?這就要說到spring的:/META-INF/spring.handlers這個文件。這個文件裏面配置了一些uri和類全限定名的對應關係,spring會加載這些handlers文件,然後將其放到一個叫做handlerMappings的Map裏面,key爲uri,value則爲類的全限定名。

和spring.handlers相關的主要涉及的類爲DefaultNamespaceHandlerResolver,它是通過懶加載的方式加載的,感興趣的可以去看看,這裏貼出相關代碼 (還涉及到單例的BeanDefinitionDocumentReader和delegate等,具體的流程,這裏暫時不深究):

/**
 * Load the specified NamespaceHandler mappings lazily.
 */
private Map<String, Object> getHandlerMappings() {
	if (this.handlerMappings == null) {
		synchronized (this) {
			if (this.handlerMappings == null) {
				try {
					Properties mappings =
							PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
					if (logger.isDebugEnabled()) {
						logger.debug("Loaded NamespaceHandler mappings: " + mappings);
					}
					Map<String, Object> handlerMappings = new ConcurrentHashMap<String, Object>(mappings.size());
					CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
					this.handlerMappings = handlerMappings;
				}
				catch (IOException ex) {
					throw new IllegalStateException(
							"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
				}
			}
		}
	}
	return this.handlerMappings;

再說spring-security的jar包,它下面有個這個文件:

如上圖所示,spring-security的jar包裏配置有該文件,並且將http://www.springframework.org/schema/security指向了org.springframework.security.config.SecurityNamespaceHandler這個類,按照上面說的,最終將由SecurityNamespaceHandler來處理我們的spring-security.xml配置文件。接下來我們重心放到它上就行了。

SecurityNamespaceHandler裏有很多的parser,針對於一些的節點處理,而我們目前就只關心<http>節點的處理:HttpSecurityBeanDefinitionParser。它會綁定該節點指定的JaasApiFilter、WebAsyncManagerFilter等等,而我們只關心權限驗證的相關的內容。而FilterSecurityInterceptor攔截請求需要一個接口路徑和權限配置的對應關係,也就是MetadataSource這個東西,它是由FilterInvocationSecurityMetadataSourceParser來負責解析的,接下來我們研究研究它就行了。這裏貼出該類的全部代碼,我在關鍵的地方做了一些註釋,下面就不做討論了哈:

package org.springframework.security.config.http;

import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.config.Elements;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.access.expression.ExpressionBasedFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;

/**
 * Allows for convenient creation of a {@link FilterInvocationSecurityMetadataSource} bean
 * for use with a FilterSecurityInterceptor.
 *
 * @author Luke Taylor
 */
public class FilterInvocationSecurityMetadataSourceParser implements BeanDefinitionParser {
	private static final String ATT_USE_EXPRESSIONS = "use-expressions";
	private static final String ATT_HTTP_METHOD = "method";
	private static final String ATT_PATTERN = "pattern";
	private static final String ATT_ACCESS = "access";
	private static final Log logger = LogFactory
			.getLog(FilterInvocationSecurityMetadataSourceParser.class);

	public BeanDefinition parse(Element element, ParserContext parserContext) {
		// 首先獲取所有的intercept-url元素
		List<Element> interceptUrls = DomUtils.getChildElementsByTagName(element,
				"intercept-url");

		// 有些配置不允許在這裏出現,當然,我們的例子中沒有配置requires-channel和filters
		for (Element elt : interceptUrls) {
			if (StringUtils.hasLength(elt
					.getAttribute(HttpSecurityBeanDefinitionParser.ATT_REQUIRES_CHANNEL))) {
				parserContext.getReaderContext().error(
						"The attribute '"
								+ HttpSecurityBeanDefinitionParser.ATT_REQUIRES_CHANNEL
								+ "' isn't allowed here.", elt);
			}

			if (StringUtils.hasLength(elt
					.getAttribute(HttpSecurityBeanDefinitionParser.ATT_FILTERS))) {
				parserContext.getReaderContext().error(
						"The attribute '" + HttpSecurityBeanDefinitionParser.ATT_FILTERS
								+ "' isn't allowed here.", elt);
			}
		}
		
		//創建MetadataSource的具體邏輯,具體見下面該方法
		BeanDefinition mds = createSecurityMetadataSource(interceptUrls, false, element,
				parserContext);

		String id = element.getAttribute(AbstractBeanDefinitionParser.ID_ATTRIBUTE);

		if (StringUtils.hasText(id)) {
			parserContext.registerComponent(new BeanComponentDefinition(mds, id));
			parserContext.getRegistry().registerBeanDefinition(id, mds);
		}

		return mds;
	}

	static RootBeanDefinition createSecurityMetadataSource(List<Element> interceptUrls,
			boolean addAllAuth, Element httpElt, ParserContext pc) {
		//獲取指定的request-matcher,如果沒有指定,則使用默認的AntPathRequestMatcher.class,我們的例子中沒有指定
		MatcherType matcherType = MatcherType.fromElement(httpElt);
		//看是否配置了use-expressions,默認爲true,我們的例子中沒有配置
		boolean useExpressions = isUseExpressions(httpElt);
		
		//真正的解析邏輯在parseInterceptUrlsForFilterInvocationRequestMap方法裏,具體見下面該方法
		ManagedMap<BeanDefinition, BeanDefinition> requestToAttributesMap = parseInterceptUrlsForFilterInvocationRequestMap(
				matcherType, interceptUrls, useExpressions, addAllAuth, pc);
		BeanDefinitionBuilder fidsBuilder;

		if (useExpressions) {
			Element expressionHandlerElt = DomUtils.getChildElementByTagName(httpElt,
					Elements.EXPRESSION_HANDLER);
			String expressionHandlerRef = expressionHandlerElt == null ? null
					: expressionHandlerElt.getAttribute("ref");

			if (StringUtils.hasText(expressionHandlerRef)) {
				logger.info("Using bean '" + expressionHandlerRef
						+ "' as web SecurityExpressionHandler implementation");
			}
			else {
				//如果沒有指定expression-handler,那麼註冊默認的處理器
				expressionHandlerRef = registerDefaultExpressionHandler(pc);
			}
			
			//綁定到ExpressionBasedFilterInvocationSecurityMetadataSource.class
			fidsBuilder = BeanDefinitionBuilder
					.rootBeanDefinition(ExpressionBasedFilterInvocationSecurityMetadataSource.class);
			fidsBuilder.addConstructorArgValue(requestToAttributesMap);
			fidsBuilder.addConstructorArgReference(expressionHandlerRef);
		}
		else {
			fidsBuilder = BeanDefinitionBuilder
					.rootBeanDefinition(DefaultFilterInvocationSecurityMetadataSource.class);
			fidsBuilder.addConstructorArgValue(requestToAttributesMap);
		}

		fidsBuilder.getRawBeanDefinition().setSource(pc.extractSource(httpElt));

		return (RootBeanDefinition) fidsBuilder.getBeanDefinition();
	}

	static String registerDefaultExpressionHandler(ParserContext pc) {
		BeanDefinition expressionHandler = BeanDefinitionBuilder.rootBeanDefinition(
				DefaultWebSecurityExpressionHandler.class).getBeanDefinition();
		String expressionHandlerRef = pc.getReaderContext().generateBeanName(
				expressionHandler);
		pc.registerBeanComponent(new BeanComponentDefinition(expressionHandler,
				expressionHandlerRef));

		return expressionHandlerRef;
	}

	static boolean isUseExpressions(Element elt) {
		String useExpressions = elt.getAttribute(ATT_USE_EXPRESSIONS);
		return !StringUtils.hasText(useExpressions) || "true".equals(useExpressions);
	}

	/**
	 *循環所有的interceptor-url配置,會返回一個map:其key和value都爲BeanDefinition類型,其中,key的root爲對應的matcherType,
	 *本例中爲AntPathRequestMatcher.class;而value的root爲SecurityConfig.class,它以字符串的形式保存一個我們上面提到的ConfigAttribute
	 *後面會交由 ExpressionBasedFilterInvocationSecurityMetadataSource處理
	 */
	private static ManagedMap<BeanDefinition, BeanDefinition> parseInterceptUrlsForFilterInvocationRequestMap(
			MatcherType matcherType, List<Element> urlElts, boolean useExpressions,
			boolean addAuthenticatedAll, ParserContext parserContext) {

		ManagedMap<BeanDefinition, BeanDefinition> filterInvocationDefinitionMap = new ManagedMap<BeanDefinition, BeanDefinition>();

		for (Element urlElt : urlElts) {
			//每一個urlElt就是一個<intercept-url>
			
			//獲取access屬性,也就是我們需要的權限
			String access = urlElt.getAttribute(ATT_ACCESS);
			if (!StringUtils.hasText(access)) {
				//如果access不爲有效字符串,則忽略此項
				continue;
			}
	
			//獲取pattern屬性,也就是我們需要攔截的路徑比如:/test/set
			String path = urlElt.getAttribute(ATT_PATTERN);

			if (!StringUtils.hasText(path)) {
				//如果path不爲有效字符串,當做異常處理
				parserContext.getReaderContext().error(
						"path attribute cannot be empty or null", urlElt);
			}
			
			//獲取method字段,當然我們的例子中並沒有配置
			String method = urlElt.getAttribute(ATT_HTTP_METHOD);
			if (!StringUtils.hasText(method)) {
				method = null;
			}
			
			//根據path和method創建一個matcher(將path和method添加到構造參數),
			//該返回值是一個BeanDefinition(注:spring中,BeanDefinition用於描述一個類示例,屬性、構造器參數等等),rootBean爲傳入的matcherType
			BeanDefinition matcher = matcherType.createMatcher(path, method);
			//SecurityConfig主要就是以字符串的形式保存一個我們上面提到的ConfigAttribute
			BeanDefinitionBuilder attributeBuilder = BeanDefinitionBuilder
					.rootBeanDefinition(SecurityConfig.class);

			if (useExpressions) {
				logger.info("Creating access control expression attribute '" + access
						+ "' for " + path);
				// The single expression will be parsed later by the
				// ExpressionFilterInvocationSecurityMetadataSource
				//只有一個構造參數就是access,以數組形式傳入
				attributeBuilder.addConstructorArgValue(new String[] { access });
				//構造方法爲createList
				attributeBuilder.setFactoryMethod("createList");

			}
			else {
				//和上面相比就是factoryMethod不一樣,當然我們的情況是useExpressions is true
				attributeBuilder.addConstructorArgValue(access);
				attributeBuilder.setFactoryMethod("createListFromCommaDelimitedString");
			}

			if (filterInvocationDefinitionMap.containsKey(matcher)) {
				logger.warn("Duplicate URL defined: " + path
						+ ". The original attribute values will be overwritten");
			}

			filterInvocationDefinitionMap.put(matcher,
					attributeBuilder.getBeanDefinition());
		}

		if (addAuthenticatedAll && filterInvocationDefinitionMap.isEmpty()) {
			//沒有對應配置的處理情況
			BeanDefinition matcher = matcherType.createMatcher("/**", null);
			BeanDefinitionBuilder attributeBuilder = BeanDefinitionBuilder
					.rootBeanDefinition(SecurityConfig.class);
			attributeBuilder.addConstructorArgValue(new String[] { "authenticated" });
			attributeBuilder.setFactoryMethod("createList");
			filterInvocationDefinitionMap.put(matcher,
					attributeBuilder.getBeanDefinition());
		}
		
		//返回整個map
		return filterInvocationDefinitionMap;
	}

}

從源碼中我們都能看出一些熟悉的字段了,比如method、pattern、access這些,其實就是我們<intercept-url>標籤的內容。根據代碼中的註釋,最終生成的是BeanDefinition,其root爲ExpressionBasedFilterInvocationSecurityMetadataSource.class

該返回結果會綁定到FilterSecurityInterceptor.class的構造參數上,具體實施的類爲HttpConfigurationBuilder.java,由於代碼太多,這裏只貼我們關注的代碼塊,圖中紅框框起來的securityMds就是上步中返回的BeanDefinition。

上圖的這個Builder就是用於創建FilterSecurityInterceptor的,其有一個metadataSource屬性,它會在創建階段將securityMds綁定到該參數上。還記得我們之前提到的,過濾一個請求的時候會根據該請求獲取對應的ConfigAttribute嗎?這個東西就來源於securityMds,也就是FilterSecurityInterceptor的metadataSource屬性,核心操作爲:

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

三、改造

到這裏,我們已經明白了spring-security是如何運作起來的。那麼再來說我們的擴展。我們目前在配置文件中是這樣配置的:

<http auto-config="true" access-decision-manager-ref="myAccessDecisionManager">

      <intercept-url pattern="/test/set" access="30"/>

</http>

這個是不支持根據參數來攔截權限的,那麼通過源碼分析,我們目前有幾種方式可以達到我們的目的呢?

1、加一個servlet的Filter。放在我們的springSecurityFilterChain之後,實現在springSecurity攔截的基礎上進行二次攔截

2、在springSecurityFilterChain的內部增加一個filter。放在FilterSecurityInterceptor之後,實現在springSecurity攔截的基礎上二次攔截

3、直接修改access的表達式,結合自定義ExpressionParse進行表達式的解析,然後在myAccessDesisiionManager中根據attribute進行權限攔截。比如access="[{type=1,privilege=1},{type=2,privilege=2}]"(隨便寫的啊)

4、修改源碼,以支持我們的自定義操作。

我現在介紹第4種方式,修改源碼,因爲我想要達到一個目的:更改spring-security.xml的配置方式。比如修改後的結構變成下面這樣:

<intercept-url pattern="/test/set">
      <item paramName="type" paramValue="1" access="1"/>
      <item paramName="type" paramValue="2" access="2"/>
      <item paramName="type" paramValue="3" access="3"/>
      <item paramName="type" paramValue="4" access="4"/>
</intercept-url>

表示/test/set這個接口,要根據type參數的具體值進行權限攔截,如果type==1,那麼需要access爲1(這個access是和具體的權限結構相關的哈,只是我的系統中權限碼爲數字);如果type==2,那麼需要access爲2,依次類推。我們還可以對access進行擴展,比如access="1&2&3"表示同時需要1、2、3權限;access="1|2|3"表示需要1、2、3中的任意一個權限等等。說幹就開幹。

第一步、我們需要改造xml的解析器:FilterInvocationSecurityMetadataSourceParser。使它支持我們新增的item子項解析:在解析每個interceptor-url節點時候,如果其下有item節點列表,那麼解析列表,並且每個子項的matcher和之前不同,它需要4個構造器參數,分別是path、method、paramName、paramValue,子項的attribute則是一樣的,我們目前簡單設置爲字符串,然後將它們放到filterInvocationDefinitionMap中,最終綁定到FilterSecurityInterceptor的成員變量,其它邏輯則不變。

第二步、自定義RequestMatcher,使其支持:

        1、接收第一步提到的path、method、paramName、paramValue參數構造matcher的BeanDefinition;

        2、其matches方法需要支持paramName和paramValue的比對支持

第三步、修改xsd文件。由於我們在xml文件中新增了item節點,所以需要將新增的節點定義到XML的結構定義文件中。

第四步、讓jvm加載我們修改之後的文件,而不是jar包中的源碼文件

我們先來一步一步實施前三步操作:

1、修改FilterInvocationSecurityMetadataSourceParser.java,主要修改了parseInterceptUrlsForFilterInvocationRequestMap方法,使其支持item子節點解析:

package org.springframework.security.config.http;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.config.Elements;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.access.expression.ExpressionBasedFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;

import java.util.List;

/**
 * Allows for convenient creation of a {@link FilterInvocationSecurityMetadataSource} bean
 * for use with a FilterSecurityInterceptor.
 *
 * @author Luke Taylor
 */
public class FilterInvocationSecurityMetadataSourceParser implements BeanDefinitionParser {
    private static final String ATT_USE_EXPRESSIONS = "use-expressions";
    private static final String ATT_HTTP_METHOD = "method";
    private static final String ATT_PATTERN = "pattern";
    private static final String ATT_ACCESS = "access";
	//paramName,paramValue,item爲我們新增的常量定義,表示我們在XML中新增的節點
    private static final String ATT_PARAMNAME = "paramName";
    private static final String ATT_PARAMVALUE = "paramValue";
    private static final String SUB_ITEM = "item";
    private static final Log logger = LogFactory
            .getLog(FilterInvocationSecurityMetadataSourceParser.class);

    public BeanDefinition parse(Element element, ParserContext parserContext) {
        List<Element> interceptUrls = DomUtils.getChildElementsByTagName(element,
                "intercept-url");

        // Check for attributes that aren't allowed in this context
        for (Element elt : interceptUrls) {
            if (StringUtils.hasLength(elt
                    .getAttribute(HttpSecurityBeanDefinitionParser.ATT_REQUIRES_CHANNEL))) {
                parserContext.getReaderContext().error(
                        "The attribute '"
                                + HttpSecurityBeanDefinitionParser.ATT_REQUIRES_CHANNEL
                                + "' isn't allowed here.", elt);
            }

            if (StringUtils.hasLength(elt
                    .getAttribute(HttpSecurityBeanDefinitionParser.ATT_FILTERS))) {
                parserContext.getReaderContext().error(
                        "The attribute '" + HttpSecurityBeanDefinitionParser.ATT_FILTERS
                                + "' isn't allowed here.", elt);
            }
        }

        BeanDefinition mds = createSecurityMetadataSource(interceptUrls, false, element,
                parserContext);

        String id = element.getAttribute(AbstractBeanDefinitionParser.ID_ATTRIBUTE);

        if (StringUtils.hasText(id)) {
            parserContext.registerComponent(new BeanComponentDefinition(mds, id));
            parserContext.getRegistry().registerBeanDefinition(id, mds);
        }

        return mds;
    }

    static RootBeanDefinition createSecurityMetadataSource(List<Element> interceptUrls,
                                                           boolean addAllAuth, Element httpElt, ParserContext pc) {
        MatcherType matcherType = MatcherType.fromElement(httpElt);
        boolean useExpressions = isUseExpressions(httpElt);

        ManagedMap<BeanDefinition, BeanDefinition> requestToAttributesMap = parseInterceptUrlsForFilterInvocationRequestMap(
                matcherType, interceptUrls, useExpressions, addAllAuth, pc);
        BeanDefinitionBuilder fidsBuilder;

        if (useExpressions) {
            Element expressionHandlerElt = DomUtils.getChildElementByTagName(httpElt,
                    Elements.EXPRESSION_HANDLER);
            String expressionHandlerRef = expressionHandlerElt == null ? null
                    : expressionHandlerElt.getAttribute("ref");

            if (StringUtils.hasText(expressionHandlerRef)) {
                logger.info("Using bean '" + expressionHandlerRef
                        + "' as web SecurityExpressionHandler implementation");
            } else {
                expressionHandlerRef = registerDefaultExpressionHandler(pc);
            }

            fidsBuilder = BeanDefinitionBuilder
                    .rootBeanDefinition(ExpressionBasedFilterInvocationSecurityMetadataSource.class);
            fidsBuilder.addConstructorArgValue(requestToAttributesMap);
            fidsBuilder.addConstructorArgReference(expressionHandlerRef);
        } else {
            fidsBuilder = BeanDefinitionBuilder
                    .rootBeanDefinition(DefaultFilterInvocationSecurityMetadataSource.class);
            fidsBuilder.addConstructorArgValue(requestToAttributesMap);
        }

        fidsBuilder.getRawBeanDefinition().setSource(pc.extractSource(httpElt));

        return (RootBeanDefinition) fidsBuilder.getBeanDefinition();
    }

    static String registerDefaultExpressionHandler(ParserContext pc) {
        BeanDefinition expressionHandler = BeanDefinitionBuilder.rootBeanDefinition(
                DefaultWebSecurityExpressionHandler.class).getBeanDefinition();
        String expressionHandlerRef = pc.getReaderContext().generateBeanName(
                expressionHandler);
        pc.registerBeanComponent(new BeanComponentDefinition(expressionHandler,
                expressionHandlerRef));

        return expressionHandlerRef;
    }

    static boolean isUseExpressions(Element elt) {
        String useExpressions = elt.getAttribute(ATT_USE_EXPRESSIONS);
        return !StringUtils.hasText(useExpressions) || "true".equals(useExpressions);
    }

    private static ManagedMap<BeanDefinition, BeanDefinition> parseInterceptUrlsForFilterInvocationRequestMap(
            MatcherType matcherType, List<Element> urlElts, boolean useExpressions,
            boolean addAuthenticatedAll, ParserContext parserContext) {

        ManagedMap<BeanDefinition, BeanDefinition> filterInvocationDefinitionMap = new ManagedMap<BeanDefinition, BeanDefinition>();

        for (Element urlElt : urlElts) {
            String path = urlElt.getAttribute(ATT_PATTERN);
            if (!StringUtils.hasText(path)) {
                parserContext.getReaderContext().error(
                        "path attribute cannot be empty or null", urlElt);
            }
            String method = urlElt.getAttribute(ATT_HTTP_METHOD);
            if (!StringUtils.hasText(method)) {
                method = null;
            }

	    //在處理每一個interceptor-url節點的時候,如果下面有item子項,那麼解析它的子項列表
            List<Element> items = DomUtils.getChildElementsByTagName(urlElt, SUB_ITEM);
            if (items != null && items.size() > 0) {
				
                //item子節點處理
                for (Element item : items) {
                    String paramName = item.getAttribute(ATT_PARAMNAME);
                    String paramValue = item.getAttribute(ATT_PARAMVALUE);
                    String access = item.getAttribute(ATT_ACCESS);
                    if (!StringUtils.hasText(paramName) || !StringUtils.hasText(paramValue) || !StringUtils.hasText(access)) {
                        continue;
                    }
                    BeanDefinition matcher = matcherType.createMatcher(path, method, paramName, paramValue);
                    BeanDefinitionBuilder attributeBuilder = BeanDefinitionBuilder
                            .rootBeanDefinition(SecurityConfig.class);

                    if (useExpressions) {
                        logger.info("Creating access control expression attribute '" + access
                                + "' for [path=" + path + ",paramName=" + paramName + "]");
                        attributeBuilder.addConstructorArgValue(new String[]{access});
                        attributeBuilder.setFactoryMethod("createList");
                    } else {
                        attributeBuilder.addConstructorArgValue(access);
                        attributeBuilder.setFactoryMethod("createListFromCommaDelimitedString");
                    }

                    if (filterInvocationDefinitionMap.containsKey(matcher)) {
                        logger.warn("Duplicate URL defined: " + path
                                + ". The original attribute values will be overwritten");
                    }

                    filterInvocationDefinitionMap.put(matcher,
                            attributeBuilder.getBeanDefinition());
                }
                continue;
            }
            String access = urlElt.getAttribute(ATT_ACCESS);
            if (!StringUtils.hasText(access)) {
                continue;
            }
            BeanDefinition matcher = matcherType.createMatcher(path, method);

            BeanDefinitionBuilder attributeBuilder = BeanDefinitionBuilder
                    .rootBeanDefinition(SecurityConfig.class);

            if (useExpressions) {
                logger.info("Creating access control expression attribute '" + access
                        + "' for " + path);
                // The single expression will be parsed later by the
                // ExpressionFilterInvocationSecurityMetadataSource
                attributeBuilder.addConstructorArgValue(new String[]{access});
                attributeBuilder.setFactoryMethod("createList");

            } else {
                attributeBuilder.addConstructorArgValue(access);
                attributeBuilder.setFactoryMethod("createListFromCommaDelimitedString");
            }

            if (filterInvocationDefinitionMap.containsKey(matcher)) {
                logger.warn("Duplicate URL defined: " + path
                        + ". The original attribute values will be overwritten");
            }

            filterInvocationDefinitionMap.put(matcher,
                    attributeBuilder.getBeanDefinition());
        }

        if (addAuthenticatedAll && filterInvocationDefinitionMap.isEmpty()) {

            BeanDefinition matcher = matcherType.createMatcher("/**", null);
            BeanDefinitionBuilder attributeBuilder = BeanDefinitionBuilder
                    .rootBeanDefinition(SecurityConfig.class);
            attributeBuilder.addConstructorArgValue(new String[]{"authenticated"});
            attributeBuilder.setFactoryMethod("createList");
            filterInvocationDefinitionMap.put(matcher,
                    attributeBuilder.getBeanDefinition());
        }

        return filterInvocationDefinitionMap;
    }

}

2、自定義RequestMatcher,由於我們當前改動較少,而且是爲了講解原理,我也就難得去重新定義一個RequestMatcher了哈,直接在默認使用的AntPathRequestMatcher上更改:

/*
 * Copyright 2002-2015 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.springframework.security.web.util.matcher;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpMethod;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;

/**
 * Matcher which compares a pre-defined ant-style pattern against the URL (
 * {@code servletPath + pathInfo}) of an {@code HttpServletRequest}. The query string of
 * the URL is ignored and matching is case-insensitive or case-sensitive depending on the
 * arguments passed into the constructor.
 * <p>
 * Using a pattern value of {@code /**} or {@code **} is treated as a universal match,
 * which will match any request. Patterns which end with {@code /**} (and have no other
 * wildcards) are optimized by using a substring match &mdash; a pattern of
 * {@code /aaa/**} will match {@code /aaa}, {@code /aaa/} and any sub-directories, such as
 * {@code /aaa/bbb/ccc}.
 * </p>
 * <p>
 * For all other cases, Spring's {@link AntPathMatcher} is used to perform the match. See
 * the Spring documentation for this class for comprehensive information on the syntax
 * used.
 * </p>
 *
 * @author Luke Taylor
 * @author Rob Winch
 * @see AntPathMatcher
 * @since 3.1
 */
public final class AntPathRequestMatcher implements RequestMatcher {
    private static final Log logger = LogFactory.getLog(AntPathRequestMatcher.class);
    private static final String MATCH_ALL = "/**";

    private final Matcher matcher;
    private final String pattern;
    private final HttpMethod httpMethod;
    private final boolean caseSensitive;
    private String paramValue;
    private String paramName;

    /**
     * 新增的構造方法,支持上文提到的四個參數
     *
     * @param pattern
     * @param httpMethod
     * @param paramName
     * @param paramValue
     */
    public AntPathRequestMatcher(String pattern, String httpMethod, String paramName, String paramValue) {
        this(pattern, httpMethod, false);
        this.paramName = paramName;
        this.paramValue = paramValue;
    }

    /**
     * Creates a matcher with the specific pattern which will match all HTTP methods in a
     * case insensitive manner.
     *
     * @param pattern the ant pattern to use for matching
     */
    public AntPathRequestMatcher(String pattern) {
        this(pattern, null);
    }

    /**
     * Creates a matcher with the supplied pattern and HTTP method in a case insensitive
     * manner.
     *
     * @param pattern    the ant pattern to use for matching
     * @param httpMethod the HTTP method. The {@code matches} method will return false if
     *                   the incoming request doesn't have the same method.
     */
    public AntPathRequestMatcher(String pattern, String httpMethod) {
        this(pattern, httpMethod, false);
    }

    /**
     * Creates a matcher with the supplied pattern which will match the specified Http
     * method
     *
     * @param pattern       the ant pattern to use for matching
     * @param httpMethod    the HTTP method. The {@code matches} method will return false if
     *                      the incoming request doesn't doesn't have the same method.
     * @param caseSensitive true if the matcher should consider case, else false
     */
    public AntPathRequestMatcher(String pattern, String httpMethod, boolean caseSensitive) {
        Assert.hasText(pattern, "Pattern cannot be null or empty");
        this.caseSensitive = caseSensitive;

        if (pattern.equals(MATCH_ALL) || pattern.equals("**")) {
            pattern = MATCH_ALL;
            matcher = null;
        } else {
            if (!caseSensitive) {
                pattern = pattern.toLowerCase();
            }

            // If the pattern ends with {@code /**} and has no other wildcards or path
            // variables, then optimize to a sub-path match
            if (pattern.endsWith(MATCH_ALL)
                    && (pattern.indexOf('?') == -1 && pattern.indexOf('{') == -1 && pattern
                    .indexOf('}') == -1)
                    && pattern.indexOf("*") == pattern.length() - 2) {
                matcher = new SubpathMatcher(pattern.substring(0, pattern.length() - 3));
            } else {
                matcher = new SpringAntMatcher(pattern);
            }
        }

        this.pattern = pattern;
        this.httpMethod = StringUtils.hasText(httpMethod) ? HttpMethod
                .valueOf(httpMethod) : null;
    }

    /**
     * Returns true if the configured pattern (and HTTP-Method) match those of the
     * supplied request.
     *
     * @param request the request to match against. The ant pattern will be matched
     *                against the {@code servletPath} + {@code pathInfo} of the request.
     */
    public boolean matches(HttpServletRequest request) {
        if (httpMethod != null && StringUtils.hasText(request.getMethod())
                && httpMethod != valueOf(request.getMethod())) {
            if (logger.isDebugEnabled()) {
                logger.debug("Request '" + request.getMethod() + " "
                        + getRequestPath(request) + "'" + " doesn't match '" + httpMethod
                        + " " + pattern);
            }

            return false;
        }

        if (pattern.equals(MATCH_ALL)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Request '" + getRequestPath(request)
                        + "' matched by universal pattern '/**'");
            }

            return true;
        }

        String url = getRequestPath(request);
        String param = null;
        if (!StringUtils.isEmpty(paramName)) {
            //如果此項matcher配置的了paramName,則需要從request中獲取該參數
            //注:這裏僅僅演示通過getParameter直接獲取哈
            try {
                param = request.getParameter(paramName);
            } catch (Exception e) {
                //e.printStackTrace();
            }
        }
        if (logger.isDebugEnabled()) {
            logger.debug("Checking match of request : '" + url + "'; against '" + pattern
                    + "'");
        }
        if (!StringUtils.isEmpty(param)) {
            //在原來matches的基礎上增加param的比對
            return matcher.matches(url) && param.equals(paramValue);
        } else {
            //如果沒有獲取到對應的param參數就走原來的matches邏輯
            return matcher.matches(url);
        }
    }

    private String getRequestPath(HttpServletRequest request) {
        String url = request.getServletPath();

        if (request.getPathInfo() != null) {
            url += request.getPathInfo();
        }

        if (!caseSensitive) {
            url = url.toLowerCase();
        }

        return url;
    }

    public String getPattern() {
        return pattern;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof AntPathRequestMatcher)) {
            return false;
        }

        AntPathRequestMatcher other = (AntPathRequestMatcher) obj;
        return this.pattern.equals(other.pattern) && this.httpMethod == other.httpMethod
                && this.caseSensitive == other.caseSensitive;
    }

    @Override
    public int hashCode() {
        int code = 31 ^ pattern.hashCode();
        if (httpMethod != null) {
            code ^= httpMethod.hashCode();
        }
        if (paramName != null) {
            code ^= paramName.hashCode();
        }
        if (paramValue != null) {
            code ^= paramValue.hashCode();
        }
        return code;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Ant [pattern='").append(pattern).append("'");

        if (httpMethod != null) {
            sb.append(", ").append(httpMethod);
        }
        if (paramName != null) {
            sb.append(", ").append(paramName);
        }
        if (paramValue != null) {
            sb.append(", ").append(paramValue);
        }
        sb.append("]");

        return sb.toString();
    }

    /**
     * Provides a save way of obtaining the HttpMethod from a String. If the method is
     * invalid, returns null.
     *
     * @param method the HTTP method to use.
     * @return the HttpMethod or null if method is invalid.
     */
    private static HttpMethod valueOf(String method) {
        try {
            return HttpMethod.valueOf(method);
        } catch (IllegalArgumentException e) {
        }

        return null;
    }

    private static interface Matcher {
        boolean matches(String path);
    }

    private static class SpringAntMatcher implements Matcher {
        private static final AntPathMatcher antMatcher = new AntPathMatcher();

        private final String pattern;

        private SpringAntMatcher(String pattern) {
            this.pattern = pattern;
        }

        public boolean matches(String path) {
            return antMatcher.match(pattern, path);
        }
    }

    /**
     * Optimized matcher for trailing wildcards
     */
    private static class SubpathMatcher implements Matcher {
        private final String subpath;
        private final int length;

        private SubpathMatcher(String subpath) {
            assert !subpath.contains("*");
            this.subpath = subpath;
            this.length = subpath.length();
        }

        public boolean matches(String path) {
            return path.startsWith(subpath)
                    && (path.length() == length || path.charAt(length) == '/');
        }
    }
}

3.在xsd文件(spring-security-4.0.xsd)中增加節點,這裏主要更新intercept-url節點:

首先定義item和它的attlist

<xs:element name="item">
       <xs:annotation>
           <xs:documentation>Add additional headers to the response.
           </xs:documentation>
       </xs:annotation>
       <xs:complexType>
           <xs:attributeGroup ref="security:item.attlist"/>
       </xs:complexType>
</xs:element>

<xs:attributeGroup name="item.attlist">
       <xs:attribute name="paramName" type="xs:token">
           <xs:annotation>
               <xs:documentation>The name of the item to add.
               </xs:documentation>
           </xs:annotation>
       </xs:attribute>
       <xs:attribute name="paramValue" type="xs:token">
           <xs:annotation>
               <xs:documentation>The value for the item.
               </xs:documentation>
           </xs:annotation>
       </xs:attribute>
       <xs:attribute name="access" type="xs:token">
           <xs:annotation>
               <xs:documentation>loren add
               </xs:documentation>
           </xs:annotation>
       </xs:attribute>
</xs:attributeGroup>

然後將item綁定到 intercept-url 的complexType中:

<xs:element name="intercept-url">
    <xs:annotation>
        <xs:documentation>Specifies the access attributes and/or filter list for a particular set of URLs.
        </xs:documentation>
    </xs:annotation>
    <xs:complexType>
        <xs:choice minOccurs="0" maxOccurs="unbounded">
            <xs:element ref="security:item"/>
        </xs:choice>
        <xs:attributeGroup ref="security:intercept-url.attlist"/>
    </xs:complexType>
</xs:element>

我們還剩下最後一步,我們修改了源碼,肯定不能把代碼打成一個新的jar包然後替換項目中原引用的jar包,該怎麼做呢?這裏涉及到一個JVM加載類的機制,也是我們通過這種方式演示功能擴展要拋出的一個細節。

JVM通過類的全限定名根據雙親委派的方式加載class,一個class被加載之後就不會再被加載了。明顯,我們的servlet容器可不能這樣,我們一個容器可以運行多個web項目,而不同的項目可以依賴不同版本的同類jar包,所以它有“自己的一套”類加載機制(當然還有很多其它場景,比如JSP的熱更新等,但這不是我們現在的重點),可以說它違反了雙親委派機制。如果我們要項目使用我們自己修改後的class,我們可以把修改的源碼類拷貝出來修改,再在項目中創建一個和原類package相同的package,將修改的類放到新建的package中,這樣對於我們修改的源碼類,編譯之後,系統中就存在了兩個同包同名的class了(只是我們修改的class位於classes,原class位於lib的jar包中),我們只需要保證自己修改的class被先加載即可,那麼怎麼保證呢?容器會優先加載classes中的class,所以像上面說的那樣操作即可。

但是有另外一個情況,比如筆者的情況就是,該項目最終會生成一個jar包交給其它項目使用。情況就變成了:lib中的兩個jar包中存在了同包同名的class。這種情況下,具體先加載哪個class就依託於具體的操作系統文件排序了,我們可以使用maven的maven-dependency-plugin插件將自己修改的class解壓到classes中,如下圖配置,筆者項目的class都在com.test包下,其它包都是重寫的一些框架的類,把除了com.test包以外的所有class都拷貝到classes中(由於我們也修改了xsd文件,所以也要把xsd文件拷貝到classes中):

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <executions>
        <execution>
            <id>unpack</id>
            <phase>generate-resources</phase>
            <goals>
                <goal>unpack</goal>
            </goals>
            <configuration>
                <artifactItems>
                    <artifactItem>
                        <groupId>com.testGroup</groupId>
                        <artifactId>testArtifact</artifactId>
                        <version>1.0</version>
                        <type>jar</type>
                        <overWrite>true</overWrite>
                        <outputDirectory>target/classes</outputDirectory>
                        <includes>**/*.class,**/*.xsd</includes>
                        <excludes>com/test/**/*.class</excludes>
                    </artifactItem>
                </artifactItems>
            </configuration>
        </execution>
    </executions>
</plugin>

如下圖,爲項目結構概況,紅框內的爲我們重寫的3個源碼類(包括了xsd文件):

最後的最後,AccessDecisionManager的實現就是業務相關的了,比如本例中是如何處理access的值的,這裏就不做闡述了哈。

四、總結

本文主要講解了spring+spring-security的一些簡單原理,然後在security的基礎上進行了源碼改造,實現了我們的“個性化”需求。雖然這樣做能達到目的,但我們的主要目的還是是學習知識。通常情況下,如果有更好的解決方案,我們最好還是不要這樣做哈。

注:本文是博主的個人理解,如果有錯誤的地方,希望大家不吝指出,謝謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章