Shiro踩坑记(二):使用RequiresXXX的注解后,访问对应请求返回404
问题描述:
我在项目中的某个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
即可。
Shiro踩坑记(二):使用RequiresXXX的注解后,访问对应请求返回404的更多相关文章
- 【React踩坑记二】react项目实现JS路由跳转
这里使用的是4.31版本的react-router-dom "react-router-dom": "^4.3.1", 直接使用以下代码即可实现路由跳转 thi ...
- Shiro踩坑记(一):关于shiro-spring-boot-web-starter自动注解无法注入authorizer的问题
一)问题描述: 我在一个Spring的项目中使用shiro搭建权限控制框架.主要通过shiro-spring-boot-web-starter包快速集成Shiro.但是项目无法启动,报没有author ...
- crm踩坑记(二)
Linux tmux 如何查看 tmux如何进行滚动呢? prefix + [, prefix为tmux的前置动作,默认是ctrl + b. 使用方向键或者pageUp来进行翻页. q可以退出滚动模式 ...
- Visual Studio For MacOS 踩坑记(二)
Visual Studio For MacOS安装安卓SDK. 系统默认安装了安卓6.0 API23的SDK. 但是我需要安卓7.0的,API24. 遂安装. SDK可以下载成功,但是用Visu ...
- iOS自动化打包上传的踩坑记
http://www.cocoachina.com/ios/20160624/16811.html 很久以前就看了很多关于iOS自动打包ipa的文章, 看着感觉很简单, 但是因为一直没有AppleDe ...
- [技术博客] 敏捷软工——JavaScript踩坑记
[技术博客] 敏捷软工--JavaScript踩坑记 一.一个令人影响深刻的坑 1.脚本语言的面向对象 面向对象特性是现代编程语言的基本特性,JavaScript中当然集成了面向对象特性.但是Java ...
- Spark踩坑记——Spark Streaming+Kafka
[TOC] 前言 在WeTest舆情项目中,需要对每天千万级的游戏评论信息进行词频统计,在生产者一端,我们将数据按照每天的拉取时间存入了Kafka当中,而在消费者一端,我们利用了spark strea ...
- Spark踩坑记——数据库(Hbase+Mysql)
[TOC] 前言 在使用Spark Streaming的过程中对于计算产生结果的进行持久化时,我们往往需要操作数据库,去统计或者改变一些值.最近一个实时消费者处理任务,在使用spark streami ...
- 【踩坑记】从HybridApp到ReactNative
前言 随着移动互联网的兴起,Webapp开始大行其道.大概在15年下半年的时候我接触到了HybridApp.因为当时还没毕业嘛,所以并不清楚自己未来的方向,所以就投入了HybridApp的怀抱. Hy ...
随机推荐
- 三通道低功耗ASK低频唤醒接收器PAN3501完全替代AS3933/GC3933
低频唤醒接收器PAN3501软硬件兼容AS3933/GC3933,且新增了寄存器功能,可直接替换,供应稳定,高性价比. 产品介绍: PAN3501是一款最多三个通道接收的低功耗ASK接收机,可用 ...
- 微信小程序placeholder设置自定义颜色
原地址链接:https://blog.csdn.net/august_leo/article/details/80877382 这是微信小程序input组件的官方文档描述,下图红框里的placehol ...
- 天天写order by,你知道Mysql底层执行原理吗?
前言 文章首发于微信公众号[码猿技术专栏]. 在实际的开发中一定会碰到根据某个字段进行排序后来显示结果的需求,但是你真的理解order by在 Mysql 底层是如何执行的吗? 假设你要查询城市是苏州 ...
- Vue生成分享海报(含二维码)
本文已同步到专业技术网站 www.sufaith.com, 该网站专注于前后端开发技术与经验分享, 包含Web开发.Nodejs.Python.Linux.IT资讯等板块. 功能需求: 海报有1张背景 ...
- 会话技术(session/cookie)
session可保存int double bool array string object:cookie只能保存stringsession 可通过php.ini文件查看存放的位置:cookie不同浏览 ...
- C++语言实现链式栈
在之前写的C语言实现链式栈篇博文中,我已经给大家大概介绍了关于链式栈的意义以及相关操作,我会在下面给大家分享百度百科对链式栈的定义,以及给大家介绍利用C++实现链式栈的基本操作. 百度百科链式栈 链式 ...
- Powershell 输出信息过多,结尾显示省略号
有时候我们通过powershell指令去查询某些信息时,因为输出结果过多,导致一部分重要信息被省略号代替,如下图 面对这种情况无论是 |fl 还是 out-file 亦或是 export-csv都无 ...
- 008-进制-C语言笔记
008-进制-C语言笔记 学习目标 1.[掌握]include预处理指令 2.[掌握]多文件开发 3.[了解]认识进制 4.[掌握]进制之间的互相转换 5.[掌握]原码,反码,补码 6.[掌握]位运算 ...
- 程序员小张的第一篇博文 --记Markdown的使用学习
1.前言 为了即将到来的面试做准备,以及记录一下平日里自己的学习过程和生活日常,我开始进驻博客园啦!这就是我的第一篇博客(有点小激动)~ 作为一只新手,首先记录一下今晚的编写博文的学习过程吧~ 2.使 ...
- sprint3总结 && sprint4计划
sprint3总结 在一周时间里,逻辑部分顺利的将数据库,查词,UI部分连接到一起.并且各部分也针对新的要求做出了一些修改,目前数据库和查词alpha版已经完成,UI部分还需要一些美化,逻辑部分也还需 ...