Shiro踩坑記(二):使用RequiresXXX的注解后,訪問對應請求返回404


問題描述:

我在項目中的某個Controller上添加了@RequirePermissions注解,希望在執行該請求前,可以先進行權限驗證。但是當我請求該Controller時,返回的確是404錯誤。
首先我懷疑的是因為權限不足而拋出了404錯誤。但是我發現我在AController的請求方法1上加了@RequiresPermession注釋,但是請求方法2同樣也報了404錯誤。所以應該不是shiro對權限進行了攔截,更像是整個controller的請求映射都沒被Spring正常解析。

哪個步驟產生了404錯誤

我們知道SpringMVC處理請求轉發的地方是在DispatchServletdoDispatch方法中。

	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				//如果是Multipart請求,則先處理
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// Determine handler for the current request.
				//根據請求找到對應HandlerMapping,在通過HandlerMapping返回對應的處理器執行鏈HandlerExecuteChain
				mappedHandler = getHandler(processedRequest);
				//找不到對應的映射,則拋出404異常
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// Determine handler adapter for the current request.
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				String method = request.getMethod();
				boolean isGet = "GET".equals(method);
				//GET 和 HEAD請求 如果資源沒更新,則直接返回
				if (isGet || "HEAD".equals(method)) {
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (logger.isDebugEnabled()) {
						logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
					}
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}

				//請求的預處理,其實就是應用攔截器的preHandle方法
				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				//正式由Controller處理請求,
				// Actually invoke the handler.
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}

				//根據Controller返回的視圖名,解析視圖
				applyDefaultViewName(processedRequest, mv);
				//后置處理,應用攔截器的后置處理方法
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
			//處理異常或是渲染視圖
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new NestedServletException("Handler processing failed", err));
		}
		finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			}
			else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}

一種懷疑是在getHandler時,找不到對應的executeHandlerChain,所以產生了404錯誤。但是在斷點中我們發現依舊可以獲取到相應的executeHandlerChain

貌似沒有問題(其實如果夠細心且了解MappingHandler的話,此時應該已經能看出問題了)。
繼續往下,直到過了前置處理依舊沒有問題(說明基本上不是攔截器造成的404錯誤)。
而再往下發現經過ha.handle()方法后,返回的mv對象為null,而此時看response對象已經出現了404的錯誤。

因此我們將關注點放在handle的執行順序上。
我們得到的haHttpRequestHandlerAdapter對象。它的handle方法如下:

@Override
	@Nullable
	public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		((HttpRequestHandler) handler).handleRequest(request, response);
		return null;
	}

HandlerAdapter是一個處理器適配器。主要是適配不同類型的處理器。而此時的Handler類型是ResourceHttpRequestHandler
其中handleRequest方法如下:

	@Override
	public void handleRequest(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		// For very general mappings (e.g. "/") we need to check 404 first
		//根據請求路徑,解析對應的靜態資源		
		Resource resource = getResource(request);
		//如果找不到對應資源,則拋出404錯誤
		if (resource == null) {
			logger.trace("No matching resource found - returning 404");
			response.sendError(HttpServletResponse.SC_NOT_FOUND);
			return;
		}

		
		if (HttpMethod.OPTIONS.matches(request.getMethod())) {
			response.setHeader("Allow", getAllowHeader());
			return;
		}

		// Supported methods and required session
		checkRequest(request);

		// Header phase
		if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) {
			logger.trace("Resource not modified - returning 304");
			return;
		}

		// Apply cache settings, if any
		prepareResponse(response);

		// Check the media type for the resource
		MediaType mediaType = getMediaType(request, resource);
		if (mediaType != null) {
			if (logger.isTraceEnabled()) {
				logger.trace("Determined media type '" + mediaType + "' for " + resource);
			}
		}
		else {
			if (logger.isTraceEnabled()) {
				logger.trace("No media type found for " + resource + " - not sending a content-type header");
			}
		}

		// Content phase
		if (METHOD_HEAD.equals(request.getMethod())) {
			setHeaders(response, resource, mediaType);
			logger.trace("HEAD request - skipping content");
			return;
		}

		ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
		if (request.getHeader(HttpHeaders.RANGE) == null) {
			Assert.state(this.resourceHttpMessageConverter != null, "Not initialized");
			setHeaders(response, resource, mediaType);
			this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);
		}
		else {
			Assert.state(this.resourceRegionHttpMessageConverter != null, "Not initialized");
			response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
			ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(request);
			try {
				List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
				response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
				this.resourceRegionHttpMessageConverter.write(
						HttpRange.toResourceRegions(httpRanges, resource), mediaType, outputMessage);
			}
			catch (IllegalArgumentException ex) {
				response.setHeader("Content-Range", "bytes */" + resource.contentLength());
				response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
			}
		}
	}

其中需要關系的部分是getResource方法,因為找不到對應的Resource,而產生了404錯誤。我們也找到了404錯誤的原因。
找到404的原因后,繼續分析。ResourceHttpRequestHandler是負責處理靜態資源的。正常情況下,我們到控制器的請求不應該是由ResourceHttpRequestHandler處理。因此,我們得到的Handler並非是我們期望的。

getHandler解析的Handler為什么不對

首先看DispatchServletgetHandler方法。

	@Nullable
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
			//遍歷內部的HandlerMapping(內置處理器),返回該請求映射的處理器
			for (HandlerMapping hm : this.handlerMappings) {
				if (logger.isTraceEnabled()) {
					logger.trace(
							"Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
				}
				//返回處理器,並形成處理器鏈
				HandlerExecutionChain handler = hm.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}

DispatcherServlet在初始化時會創建內置的一些HandlerMapping。常見的有SimpleUrlHandlerMapping(映射請求和靜態資源),RequestMappingHandlerMapping(映射請求和@RequestMapping注解的Controller中的方法),BeanNameUrlHandlerMapping(映射請求和處理器bean,映射關系由bean Name確定)等。
為什么RequestMappingHandlerMapping沒能夠為我們對應的處理器?了解下RequestMappingHandlerMappinggetHandler方法:

	public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
	//調用內部獲取處理器的方法(模板模式)
		Object handler = getHandlerInternal(request);
		//如果處理器為空 則使用默認的處理器
		if (handler == null) {
			handler = getDefaultHandler();
		}
		if (handler == null) {
			return null;
		}
		//如果返回的處理器是bean Name,則獲取bean對象
		// Bean name or resolved handler?
		if (handler instanceof String) {
			String handlerName = (String) handler;
			handler = obtainApplicationContext().getBean(handlerName);
		}
        
        //形成處理器執行鏈(主要是添加攔截器)
		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
		//如果是跨域請求,則設置跨域的配置
		if (CorsUtils.isCorsRequest(request)) {
			CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request);
			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
			CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
		}
		return executionChain;
	}

查找處理器的邏輯主要是是在getHandlerInternal方法中:

protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
        //根據請求解析路徑
		String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
		if (logger.isDebugEnabled()) {
			logger.debug("Looking up handler method for path " + lookupPath);
		}
		this.mappingRegistry.acquireReadLock();
		try {
		    //獲取對應的處理器方法
			HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
			if (logger.isDebugEnabled()) {
				if (handlerMethod != null) {
					logger.debug("Returning handler method [" + handlerMethod + "]");
				}
				else {
					logger.debug("Did not find handler method for [" + lookupPath + "]");
				}
			}
			return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
		}
		finally {
			this.mappingRegistry.releaseReadLock();
		}
	}

lookupHandlerMethod方法則是從MappingRegistry中獲取匹配url的方法。在根據URL匹配的精度確認最后的方法。ReqeustMappingHandlerMapping找不到處理器,說明MappingRegistry並沒有解析到對應的處理器方法。

RequstMappingHandlerMapping的初始化過程

RequestMappingHandlerMapping實現了InitializingBean接口。在其afterPropertiesSet方法中實現了將
處理器映射方法mappingRegistry的邏輯。具體實現在其父類AbstractHandlerMethodMapping中。

	//初始化時檢測處理器方法
	@Override
	public void afterPropertiesSet() {
		initHandlerMethods();
	}

	//掃描上下文中的bean,注冊對應的處理器方法
	protected void initHandlerMethods() {
		if (logger.isDebugEnabled()) {
			logger.debug("Looking for request mappings in application context: " + getApplicationContext());
		}
		//獲取上下文中的bean name
		String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
				BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
				obtainApplicationContext().getBeanNamesForType(Object.class));

		//遍歷bean names
		for (String beanName : beanNames) {
			if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
				Class<?> beanType = null;
				try {
					beanType = obtainApplicationContext().getType(beanName);
				}
				catch (Throwable ex) {
					// An unresolvable bean type, probably from a lazy bean - let's ignore it.
					if (logger.isDebugEnabled()) {
						logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
					}
				}
				//是否為標准處理器(RequestMappingHandlerMapping的實現根據類上是否有@Controller或是@RequestMapping注釋)
				if (beanType != null && isHandler(beanType)) {
					//篩選對應的方法並注冊
					detectHandlerMethods(beanName);
				}
			}
		}
		handlerMethodsInitialized(getHandlerMethods());
	}

接下來就是在RequestMappingHandlerMapping初始化的過程中斷點調試,看看是什么問題:

可以看到相應的控制器被代理過后丟失了注釋。而這里的代理並非是AspectJ的創建的,而是com.sun.Proxy對象。
如果在啟動時觀察對應控制器的bean的創建情況,可以發現這個bean被增強了兩次:
第一次增強:

第二次增強:

可以看到第二次增強過后bean丟失了@Controller的注釋。

解決方案

我們已經知道造成404的真正原因是Controller初始化時被增強了兩次。並在第二次增強時丟掉了注釋。導致了該Controller無法被正常映射。因此我們只需要關閉一次增強過程即可。事實上,由於已經存在了ProxyCreator,因此ShiroAnnotationProcessorAutoConfiguration中的DefaultAdvisorAutoProxyCreator就不再需要了。
所以可以通過在配置文件中將shiro.annotations.enabled屬性設置為false。或者是直接在項目的配置中exclude掉ShiroAnnotationProcessorAutoConfiguration。然后再聲明AuthorizationAttributeSourceAdvisor即可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM