学习evilpan研究url解析
这篇文章是写shiro代码的中间梳理的,因为我觉得,单从shiro的角度来挖掘url解析流程,不够整体化,再加之看到evilpan写的文章,于是想要复现一下他学习的思路。就是自己写文章,然后卡主了看一下。 浅谈 URL 解析与鉴权中的陷阱 URL 解析与鉴权中的陷阱 —— Spring 篇 我们的目标是贯穿整个解析流程,并且将每个类细致的解释清楚。
本文介绍了使用 URL 进行鉴权的一类威胁模型,并以两个符合标准的 Servlet 容器 Tomcat 和 Resin 为例介绍了二者的路由查找方法,根据路由查找的过程提出了一系列可能的 URL 变异方式;然后对几个现实中的鉴权案例进行分析,包括某典型应用手搓的鉴权代码以及成熟的鉴权方案 Shiro,其中都存在或者出现过鉴权绕过的场景,从中我们可以加深对 URL 鉴权的理解,从而写出更加健壮和安全的代码。 另外,本文只是介绍了 Servlet 标准应用的路由特性,而现代 Java Web 应用中更多是基于 Spring 生态的全家桶方案,Spring MVC 包揽了 Web 容器的所有路由并使用自身的寻址实现,且配合 Spring Security 也形成一套 URL 鉴权方案。这部分限于篇幅原因并未在本文中提及,后续有机会的话会另起一篇文章进行介绍。
Servlet容器
Tomcat
什么是Servlet[1]?什么又是Servlet容器?Servlet是JavaEE框架的一个组件,用于web开发。Servlet是运行在容器之中的基本的java应用程序。Servlet的职责是用于接受请求,处理,并且发送一个响应。container先调用servlet的init方法将其实例化并注册到container。servlet初始化之后,容器将请求重定向到其service方法,然后再逐步委托给恰当的doGet或者doPost方法。 关于Container,最开始会创建ServletContext,用于充当server或者Container的内存,他会记住所有servlet,filter和listener,其生命周期和Container相同。 当访问浏览器时,容器会创建HttpServletRequest/HttpServletResponse,其中携带其他信息,比如headers,parameters和请求体,我们可以在doXXX中设置Response。 请求结束之后Request被垃圾回收,HttpSession用来维持客户端请求和连接,容器创建用户会话(session),返回一个JSESSIONID存储到用户cookie中,作为用户的唯一标识。 我们可以通过将变量放在ServletContext来实现共享数据。 一个标准的Servlet接口如下
public interface Servlet {
void init(ServletConfig var1) throws ServletException;
ServletConfig getServletConfig();
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
String getServletInfo();
void destroy();
}
Servlet容器有如下:Tomcat,Jetty,Resin
### CoyoteAdapter 一个请求处理器的实现,将处理委托给Coyote处理器。我们需要关心其service函数。
@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {
...
postParseSuccess = postParseRequest(req, request, res, response);
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
...
}
postParseRequest()
:在解析HTTP报头之后执行此函数,其内部需要关心的是将访问的url映射成一个mappingData,其中还包括了host、context、wrapper等。具体路径如下
CoyteAdapter.postParseRequest()
Mapper.map()
Mapper.internalMap()
internalMap函数的映射逻辑如下
private void internalMap(CharChunk host, CharChunk uri, String version, MappingData mappingData)
throws IOException {
// Context mapping
ContextList contextList = mappedHost.contextList;
MappedContext[] contexts = contextList.contexts;
int pos = find(contexts, uri);
// Wrapper mapping
if (!contextVersion.isPaused()) {
internalMapWrapper(contextVersion, uri, mappingData);
}
}
Context代表我们部署的应用名称,比如Tomcat部署后会在前缀增加一个xxx_war_wxploded
匹配上后进行包装,调用internalMapWrapper,下文进行拆解。 ### Mapper 上一步先定位到了Context,即我们应用的容器,这一步是叫做Wrapper mapping.是Wrapper的映射。 wrapper是context的子容器,他的职责是:加载它表示的 servlet 并分配它的一个实例。注意,每个Servlet都有一个映射的url进行匹配,并且都需要重写doGet、doPost等其中函数之一。
private void internalMapWrapper(ContextVersion contextVersion, CharChunk path, MappingData mappingData) throws IOException {
// Rule 1 -- Exact Match
MappedWrapper[] exactWrappers = contextVersion.exactWrappers;
internalMapExactWrapper(exactWrappers, path, mappingData);
// Rule 2 -- Prefix Match
MappedWrapper[] wildcardWrappers = contextVersion.wildcardWrappers;
if (mappingData.wrapper == null) {
internalMapWildcardWrapper(wildcardWrappers, contextVersion.nesting, path, mappingData);
}
// Rule 3 -- Extension Match
MappedWrapper[] extensionWrappers = contextVersion.extensionWrappers;
if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
internalMapExtensionWrapper(extensionWrappers, path, mappingData, true);
}
// Rule 4 -- Welcome resources processing for servlets
// Rule 4a -- Welcome resources processing for exact macth
// Rule 4b -- Welcome resources processing for prefix match
// Rule 4c -- Welcome resources processing
/*
* welcome file processing - take 2 Now that we have looked for welcome files with a physical backing, now look
* for an extension mapping listed but may not have a physical backing to it. This is for the case of index.jsf,
* index.do, etc. A watered down version of rule 4
*/
// Rule 7 -- Default servlet
}
我们需要从一些匹配规则中找到wrapper,然后会将wrapper赋值给mappingData,如何匹配?匹配的细致规则怎么找?evilpan给出了,看官方文档Java Servlet Specification 12章节[2]。
- 首先尝试查找 Servlet 路由的 精确匹配;
- 递归查找最长前缀匹配,以 / 字符为每一级目录树的分隔,即前缀匹配的单位是目录;
- 如果 URL 路径的最后一个片段(Segment)包含后缀,容器会尝试使用后缀匹配对应的 Servlet,比如对于 .jsp 后缀使用 JspServlet;
- 如果上述规则都没有成功匹配,容器将会尝试根据请求的 URL 去匹配对应的资源,这通常会使用一个容器自带的默认 Servlet 去处理。
在标准中还提到了几个值得注意的点:
- 在匹配 ContextRoot 的时候也是使用最长前缀匹配;
- 在 URL 进行匹配时候都是 大小写敏感的;
对于配置映射的
- 映射值以 / 开头且以 /* 结尾的用于前缀匹配映射;
- 映射值以 *. 开头的使用后缀匹配;
- 空字符是一个特殊的映射值,指向 context-root;
- 仅包含字符 / 的映射值表示对应应用的默认 Servlet;
- 其他所有的值都被认为是精确匹配;
因此,对于应用中定义的不同映射,都可以根据寻址逻辑按照顺序找到最佳的匹配。
### Wrappers 回忆杀结束,继续回到上述 internalMapWrapper 的代码中。按照查找的顺序,Tomcat 会依次从下面的 Wrapper 中进行匹配:
- exactWrappers: 用于精确匹配的 Wrapper,比如值为 /api/flag 的 FlagServlet;
- wildcardWrappers: 用于(最长)前缀匹配的通配符 Wrapper,比如值为 /admin/* 的 Servlet;
- extensionWrappers: 用于后缀匹配的 Wrapper,比如值为 *.txt 的 Servlet。在 Tomcat 中默认有两个,分别是 jsp 和 jspx,都对应 org.apache.jasper.servlet.JspServlet;
- welcomeResources: 如果 URL 的最后一个字符是 /,则会尝试匹配欢迎页面,默认是 index.html、index.htm、index.jsp;对于没有物理文件的欢迎页面,比如 index.do、index.jsf 等,会根据后缀匹配的方式在内置资源文件中查找;
- defaultWrapper: 在前面都没有匹配的情况下,使用 Tomcat 的默认 Servlet 去进行处理,对应类是 org.apache.catalina.servlets.DefaultServlet,用于请求磁盘文件或者 jar 包中的文件。
大体来说,每一个 Servlet 对应一个 Wrapper 实例,而根据 URL 查找 Wrapper 的过程也就是对应 Servlet 路由查找的过程。
### decodeURI 上面是访问到映射添加相关资源的过程,下边是url编码相关
protected boolean postParseRequest(org.apache.coyote.Request req, Request request, org.apache.coyote.Response res,
Response response) throws IOException, ServletException {
MessageBytes decodedURI = req.decodedURI();
// Parse (and strip out) the path parameters
parsePathParameters(req, request);
// URI decoding
// %xx decoding of the URL
req.getURLDecoder().convert(decodedURI.getByteChunk(), connector.getEncodedSolidusHandlingInternal());
// Normalization
if (normalize(req.decodedURI())) {
// Character decoding
convertURI(decodedURI, request);
}
}
第一步parsePathParameters