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

問題描述:

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

哪一個步驟產生了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
session

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

所以咱們將關注點放在handle的執行順序上。
咱們獲得的haHttpRequestHandlerAdapter對象。它的handle方法以下:async

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

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

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

@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並不是是咱們指望的。post

getHandler解析的Handler爲何不對

首先看DispatchServletgetHandler方法。ui

@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方法:this

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方法中:url

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便可。

相關文章
相關標籤/搜索