問題描述:
我在項目中的某個Controller上添加了@RequirePermissions
注解,希望在執行該請求前,可以先進行權限驗證。但是當我請求該Controller時,返回的確是404錯誤。
首先我懷疑的是因為權限不足而拋出了404錯誤。但是我發現我在AController
的請求方法1上加了@RequiresPermession
注釋,但是請求方法2同樣也報了404錯誤。所以應該不是shiro對權限進行了攔截,更像是整個controller的請求映射都沒被Spring正常解析。
哪個步驟產生了404錯誤
我們知道SpringMVC處理請求轉發的地方是在DispatchServlet
的doDispatch
方法中。
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
的執行順序上。
我們得到的ha
是HttpRequestHandlerAdapter
對象。它的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為什么不對
首先看DispatchServlet
的getHandler
方法。
@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
沒能夠為我們對應的處理器?了解下RequestMappingHandlerMapping
的getHandler
方法:
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
即可。