目录导航
Java容器安全
由于Java反序列化漏洞带来的巨大冲击,很多我们平时常用的Web
中间件都出现了RCE
漏洞,如:Weblogic
、Jboss
、WebSphere
等。运行在容器中的Web应用
也因此遭受恶意攻击,容器自身的安全问题也引起了越来越多的人的关注。
Servlet容器基础
Servlet容器
即实现了Servlet 规范的Web应用服务器,负责管理Servlet的生命周期、URL访问请求和Servlet
映射、Servlet
注册/卸载等。

常见的Servlet容器
自Java EE 8
开始Java EE
已更名为Jakarta EE,所有的Servlet容器
都必须实现该规范。常见的Servlet容器
如下:
Tomcat
/TomEE
Jetty
Glassfish Server
Jboss
/Undertow
/WildFly
Resin
Weblogic Server
WebSphere Application Server
/Liberty
TongWeb
(东方通)Apusic
(金蝶)BES Application Server
(宝兰德)InforSuite Application Server
(中创)Primeton AppServer
(普元)
常见的安全问题
- 远程代码执行
- 反序列化漏洞
XSS
SSRF
,如:Weblogic uddiexplorer
- 未授权访问
Session
安全- 文件/目录解析漏洞,如:未知扩展名向左识别文件后缀、换行解析漏洞、空字符截断、编码绕过后缀、文件名解析逻辑漏洞等
- 任意文件上传(写入)漏洞,如:
WebDAV PUT
- 任意文件读取,如:
Tomcat AJP
- 后台管理问题,如:登陆爆破、默认口令、弱口令、部署war包
- 配置错误导致安全风险,如:目录遍历、信息泄露、CRLF注入、Nginx配置不当
- 参数解析,如:Http参数污染
- 拒绝服务攻击,如:由于各种机制导致的服务器响应慢导致的
DOS
、慢连接
BinCat
大家好,我是BinCat
,一个基于JavaEE API
实现的超简单(不安全的非标准的,仅用于学习Java容器原理)的Web Server
。

Http请求协议解析
Http协议(超文本传输协议,HyperText Transfer Protocol
)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP是万维网的数据通信的基础。要想能够处理Http请求就必须先解析Http请求,不同的Http请求方式对应的数据包也是不一样的。
GET请求包示例:
GET / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.64.1
Accept: */*
POST请求包示例:
POST /?s=java HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.64.1
Accept: */*
Cookie: Hm_lvt_f4c571d9b8811113b4f18e87a6dbe619=1597582351; Hm_lpvt_f4c571d9b889b22224f18e87a6dbe619=1599562693; JSESSIONID=LgxJ127kT7ymIGbC2T1TeipnMP9_2_CqJQjmrqOb
Content-Length: 17
Content-Type: application/x-www-form-urlencoded
id=123&name=admin
解析Http简要流程
解析POST请求的简单流程如下(非multipart或chunked请求
):
- 解析第一行的Http协议信息。
- 解析Http请求Header信息。
- 解析请求主体(Body)部分。
解析Http请求协议信息
接下来我们将以上述的POST包解析为例简单的实现Http协议解析。如上POST包,第一行数据中包含了请求方式、请求的URL地址以及Http协议版本信息(空格隔开):POST /?s=java HTTP/1.1
。那么我们只需要使用空白符号将字符串切割成数组即可完成解析。
解析Http请求协议示例代码片段:
// 从Socket中读取一行数据,读取请求的URL
String str = dis.readLine();
// 切割请求Http协议信息
String[] strs = str.split("\\s+");
// 解析Http请求方法类型
String method = strs[0];
// 解析Http请求URL地址
String url = strs[1];
// 解析Http请求版本信息
String httpVersion = strs[2];
解析Http请求Header信息
解析完Http请求协议后就应该继续解析Http Header信息了,Http请求头从第二行开始到一个空白行结束,Header中的键值对以:
分割,如下:
Host: localhost:8080
User-Agent: curl/7.64.1
Accept: */*
Content-Length: 17
Content-Type: application/x-www-form-urlencoded
解析Http头示例代码片段:
// 创建Header对象
Map<String, String> header = new ConcurrentHashMap<String, String>();
// 解析请求头信息
while (true) {
// 按行读取Header头信息
String line = dis.readLine();
// 当读取到空行时停止解析Header
if ("".equals(line)) {
break;
}
// 切割Header的Key/Value
String[] headers = line.split(":\\s*", -1);
header.put(headers[0], headers[1]);
}
解析完Header后剩下的也就是最后的Http请求主体部分了,浏览器会将请求的参数以&
为连接符拼接出多个参数,参数名称和参数值以=
分割,并且参数值默认会使用URL编码,如下:
id=123&name=admin
解析body中的请求参数时需要先从Header中读取请求的主体大小,即:Content-Length
,因为body中允许出现换行\n
等特殊内容,所以解析body时应该按字节读取数据。除此之外,解析Body中的请求参数之前应该先解析URL中的请求参数,即GET传参部分:/?s=java
,然后再解析body中的参数。
解析Http GET参数代码片段:
// 解析GET请求参数
if (url.contains("?")) {
String[] parameterStrs = url.split("\\?");
this.requestURL = parameterStrs[0];
// 初始化Http请求的QueryString
this.queryString = parameterStrs[1];
// 按"&"切割GET请求的参数
String[] parameters = queryString.split("&");
// 解析GET请求参数
for (String parameter : parameters) {
String[] tmp = parameter.split("=", -1);
if (tmp.length == 2) {
parameterMap.put(tmp[0], new String[]{URLDecoder.decode(tmp[1])});
}
}
}
Cookie解析
Cookie是非常Http请求中非常重要的用户凭证,Cookie位于请求头中的cookie
字段,多个Cookie
以;
分割,Cookie
的参数和参数值以=
切分。Cookie
中会存储一个叫JSESSIONID
(Java标准容器中叫JSESSIONID
),用于识别服务器端存储的用户会话信息。
示例Cookie:
Cookie: Hm_lvt_f4c571d9b8811113b4f18e87a6dbe619=1597582351; Hm_lpvt_f4c571d9b889b22224f18e87a6dbe619=1599562693; JSESSIONID=LgxJ127kT7ymIGbC2T1TeipnMP9_2_CqJQjmrqOb
示例Cookie解析代码片段:
// 解析Cookie
if (headerMap.containsKey("cookie")) {
// 切分Cookie字符串
String[] cookies = headerMap.get("cookie").split(";\\s+", -1);
// 初始化Cookie数组长度
this.cookie = new Cookie[cookies.length];
for (int i = 0; i < cookies.length; i++) {
String cookieStr = cookies[i];
String[] tmp = cookieStr.split("=", -1);
if (tmp.length == 2) {
// 创建Cookie对象
this.cookie[i] = new Cookie(tmp[0], URLDecoder.decode(tmp[1]));
}
}
}
解析Http请求主体
解析Http主体代码片段:
if ("POST".equalsIgnoreCase(method)) {
String contentType = header.get("Content-Type");
// 解析POST请求参数
if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) {
// 获取请求的主体长度
int contentLength = Integer.parseInt(header.get("Content-Length"));
// 创建一个和请求体一样大小的缓冲区
byte[] bytes = new byte[contentLength];
// 读取POST主体内容
dis.read(bytes);
// 解析POST请求内容
String body = new String(bytes, "ISO8859-1");
// 按"&"切割POST请求的参数
String[] parameters = body.split("&");
// 解析POST请求参数
for (String parameter : parameters) {
String[] tmp = parameter.split("=", -1);
if (tmp.length == 2) {
parameterMap.put(tmp[0], URLDecoder.decode(tmp[1]));
}
}
}
}
BinCat V1 – 简单的请求文件访问处理
实现一个简单的Web服务器非常容易,使用ServerSocket
在服务器端监听端口并等待浏览器请求,一旦接收到浏览器数据后就开始解析Http协议,最后将服务器端请求处理完后通过Socket
返回给浏览器即可。

V1版本,我们先实现一个简单的读取服务器静态文件的功能,在后续版逐渐完善。
BinCatServerV1实现代码:
package com.anbai.sec.server;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.logging.Logger;
/**
* ServerSocket示例
*/
public class BinCatServerV1 {
private static final Logger LOG = Logger.getLogger("info");
public static void main(String[] args) {
try {
// 设置服务监听端口
int port = 8080;
// 设置服务名称
String serverName = "BinCat-0.0.1";
// 创建ServerSocket,监听本地端口
ServerSocket ss = new ServerSocket(port);
LOG.info(serverName + " 启动成功,监听端口: " + port);
while (true) {
// 等待客户端连接
Socket socket = ss.accept();
try {
// 获取Socket输入流对象
InputStream in = socket.getInputStream();
// 获取Socket输出流对象
OutputStream out = socket.getOutputStream();
// 创建输出流对象
BufferedReader br = new BufferedReader(new InputStreamReader(in));
// 从Socket中读取一行数据
String str = br.readLine();
if (str == null) {
continue;
}
// 切割请求Http协议信息
String[] strs = str.split("\\s+");
// 解析Http请求URL地址
String url = strs[1];
// 输出服务器返回信息
String msg = "";
// 当前服务器运行目录下的文件
File file = new File(System.getProperty("user.dir"), url);
if (file.exists()) {
out.write("HTTP/1.1 200 OK\n".getBytes());
msg = file.getAbsolutePath();
} else {
out.write("HTTP/1.1 404 Not Found\n".getBytes());
msg = file.getAbsolutePath() + " Not Found!";
}
// 输出返回字节数
out.write(("Content-Length: " + msg.getBytes().length + "\n").getBytes());
// 写入换行
out.write("\n".getBytes());
// 将读取到的数据写入到客户端Socket
out.write(msg.getBytes());
in.close();
out.close();
} catch (IOException e) {
LOG.info("处理客户端请求异常:" + e);
} finally {
socket.close();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
启动BinCat
服务后控制台将输出:
九月 09, 2020 5:18:50 下午 com.anbai.sec.server.BinCatServerV1 main
信息: BinCat-0.0.1启动成功,监听端口:8080
浏览器请求localhost:8080即可在浏览器中输出当前请求的文件是否存在:

请求一个不存在的文件地址,浏览器将会输出错误信息,如请求localhost:8080/test:

从上图中我们可以看到响应的状态码和body都能够正确的被浏览器解析。
BinCat V2 – 简单解析请求参数
V2版本我们需要支持请求参数解析以及简单的HTML页面渲染功能。
BinCatServerV2实现代码:
package com.anbai.sec.server;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URLDecoder;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
/**
* ServerSocket Http 服务器示例
*/
public class BinCatServerV2 {
private static final Logger LOG = Logger.getLogger("info");
public static void main(String[] args) {
try {
// 设置服务监听端口
int port = 8080;
// 设置服务名称
String serverName = "BinCat-0.0.2";
// 创建ServerSocket,监听本地端口
ServerSocket ss = new ServerSocket(port);
LOG.info(serverName + " 启动成功,监听端口: " + port);
while (true) {
// 等待客户端连接
Socket socket = ss.accept();
try {
// 获取Socket输入流对象
InputStream in = socket.getInputStream();
// 获取Socket输出流对象
OutputStream out = socket.getOutputStream();
// 创建数据输出流对象
DataInputStream dis = new DataInputStream(in);
// 从Socket中读取一行数据,读取请求的URL
String str = dis.readLine();
if (str == null) {
socket.close();
continue;
}
// 切割请求Http协议信息
String[] strs = str.split("\\s+");
// 解析Http请求方法类型
String method = strs[0];
// 解析Http请求URL地址
String url = strs[1];
// 初始化Http请求URL地址
String requestURL = url;
// 初始化Http请求的QueryString
String queryString;
// 解析Http请求版本信息
String httpVersion = strs[2];
// 创建Header对象
Map<String, String> header = new ConcurrentHashMap<String, String>();
// 初始化请求参数数组
Map<String, String> parameterMap = new ConcurrentHashMap<String, String>();
// 解析GET请求参数
if (url.contains("?")) {
String[] parameterStrs = url.split("\\?");
requestURL = parameterStrs[0];
queryString = parameterStrs[1];
// 按"&"切割GET请求的参数
String[] parameters = queryString.split("&");
// 解析GET请求参数
for (String parameter : parameters) {
String[] tmp = parameter.split("=", -1);
if (tmp.length == 2) {
parameterMap.put(tmp[0], URLDecoder.decode(tmp[1]));
}
}
}
// 解析请求头信息
while (true) {
// 按行读取Header头信息
String line = dis.readLine();
// 当读取到空行时停止解析Header
if ("".equals(line)) {
break;
}
// 切割Header的Key/Value
String[] headers = line.split(":\\s*", -1);
header.put(headers[0], headers[1]);
}
// 输出服务器返回信息
StringBuffer msg = new StringBuffer();
// 处理Http请求,当浏览器请求主页时返回服务器信息
if ("/".equals(requestURL)) {
out.write("HTTP/1.1 200 OK\n".getBytes());
// 根据Http请求类型处理不同的请求
if ("GET".equalsIgnoreCase(method)) {
// 输出服务器处理结果
msg.append("<html>\n" +
"<head>\n" +
" <title>Login Test</title>\n" +
"</head>\n" +
"<body>\n" +
"<div style=\"margin: 30px;\">\n" +
" <form action=\"/\" method=\"POST\">\n" +
" Username:<input type=\"text\" name=\"username\" value=\"admin\"/><br/>\n" +
" Password:<input type=\"text\" name=\"password\" value=\"'=0#\"/><br/>\n" +
" <input type=\"submit\" value=\"Login\"/>\n" +
" </form>\n" +
"</div>\n" +
"</body>\n" +
"</html>");
} else if ("POST".equalsIgnoreCase(method)) {
String contentType = header.get("Content-Type");
// 解析POST请求参数
if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) {
// 获取请求的主体长度
int contentLength = Integer.parseInt(header.get("Content-Length"));
// 创建一个和请求体一样大小的缓冲区
byte[] bytes = new byte[contentLength];
// 读取POST主体内容
dis.read(bytes);
// 解析POST请求内容
String body = new String(bytes, "ISO8859-1");
// 按"&"切割POST请求的参数
String[] parameters = body.split("&");
// 解析POST请求参数
for (String parameter : parameters) {
String[] tmp = parameter.split("=", -1);
if (tmp.length == 2) {
parameterMap.put(tmp[0], URLDecoder.decode(tmp[1]));
}
}
// 定义SQL语句
String sql = "select id,username,password from sys_user where username = '" +
parameterMap.get("username") + "' and password = '" +
parameterMap.get("password") + "'";
msg.append("<font color='red'>JDBC 查询SQL:" + sql + "</font>\n");
msg.append("<h3>请求头:</h3>\n");
msg.append("<pre>\n");
for (String key : header.keySet()) {
msg.append(key + ": " + header.get(key) + "\n");
}
msg.append("<pre>\n");
msg.append("<h3>请求参数:</h3>\n");
// 循环遍历请求参数
for (String key : parameterMap.keySet()) {
msg.append(key + ": " + parameterMap.get(key) + "\n");
}
}
}
} else {
out.write("HTTP/1.1 404 Not Found\n".getBytes());
// 输出错误信息
msg.append("404");
}
// 输出Web服务器信息
out.write(("Server: " + serverName + "\n").getBytes());
// 输出返回的消息类型
out.write(("Content-Type: text/html; charset=UTF-8\n").getBytes());
// 请求响应内容
byte[] responseByte = msg.toString().getBytes();
// 输出返回字节数
out.write(("Content-Length: " + responseByte.length + "\n").getBytes());
// 写入换行
out.write("\n".getBytes());
// 将读取到的数据写入到客户端Socket
out.write(responseByte);
in.close();
out.close();
} catch (IOException e) {
LOG.info("处理客户端请求异常:" + e);
} finally {
socket.close();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
请求参数解析测试
访问Web服务测试http://localhost:8080:

提交登陆表单测试:

BinCat V3 – 实现Servlet3.x API
V1
和V2
我们完成了一个简单的文件访问服务和请求参数解析服务,V3
我们继续添加Servlet API
,从而理解Servlet
的工作原理。
添加Servlet3.x
依赖:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
</dependency>
创建com.anbai.sec.server.servlet.BinCatRequest
类并继承javax.servlet.http.HttpServletRequest
,然后需要实现HttpServletRequest
接口方法,作为一个非标准的Servlet容器
我们自然是没必要严格的是实现里面的所有方法,选择几个方法实现一下就行了。
注意:示例以下中省去了解析协议Servlet
接口的代码,完整代码请参考:com.anbai.sec.server.servlet
包下的完整实现代码。
HttpServletRequest实现
BinCatRequest.java示例代码片段:
package com.anbai.sec.server.servlet;
import org.javaweb.utils.StringUtils;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.net.Socket;
import java.net.URLDecoder;
import java.security.Principal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
/**
* BinCat 请求解析实现对象,解析Http请求协议和参数
*/
public class BinCatRequest implements HttpServletRequest {
// 客户端Socket连接对象
private final Socket clientSocket;
// Socket输入流对象
private final InputStream socketInputStream;
// Http请求头对象
private Map<String, String> headerMap;
// Http请求参数对象
private Map<String, String[]> parameterMap;
// Http请求attribute对象
private final Map<String, Object> attributeMap = new ConcurrentHashMap<String, Object>();
// Http请求Cookie对象
private Cookie[] cookie;
// Http请求Cookie对象
private final Map<String, String> cookieMap = new ConcurrentHashMap<String, String>();
// Http请求Session对象
private final Map<String, BinCatSession> sessionMap = new ConcurrentHashMap<String, BinCatSession>();
// Http请求方法类型
private String requestMethod;
// Http请求URL
private String requestURL;
// Http请求QueryString
private String queryString;
// Http请求协议版本信息
private String httpVersion;
// 是否已经解析过Http请求参数,防止多次解析请求参数
private volatile boolean parsedParameter = false;
// Http请求内容长度
private int contentLength;
// Http请求内容类型
private String contentType;
// 存储Session的ID名称
private static final String SESSION_ID_NAME = "JSESSIONID";
// Http请求主机名
private String host;
// Http请求主机端口
private int port;
private static final Logger LOG = Logger.getLogger("info");
public BinCatRequest(Socket clientSocket) throws IOException {
this.clientSocket = clientSocket;
this.socketInputStream = clientSocket.getInputStream();
// 解析Http协议
parse();
}
/**
* 解析Http请求协议,不解析Body部分
*
* @throws IOException
*/
private void parse() throws IOException {
// 此处省略Http请求协议解析、参数解析等内容...
}
/**
* 解析Http请求参数
*
* @throws IOException Http协议解析异常
*/
private synchronized void parseParameter() {
// 此处省略Http请求协议解析、参数解析等内容...
}
// 此处省略HttpServletRequest接口中的大部分方法,仅保留几个示例方法...
public String getHeader(String name) {
return this.headerMap.get(name);
}
public ServletInputStream getInputStream() throws IOException {
return new ServletInputStream() {
@Override
public int read() throws IOException {
return socketInputStream.read();
}
};
}
public String getParameter(String name) {
if (!parsedParameter) {
this.parseParameter();
}
if (parameterMap.containsKey(name)) {
return this.parameterMap.get(name)[0];
}
return null;
}
public String getRemoteAddr() {
return clientSocket.getInetAddress().getHostAddress();
}
public void setAttribute(String name, Object o) {
attributeMap.put(name, o);
}
}
HttpServletResponse实现
BinCatResponse.java示例代码片段:
package com.anbai.sec.server.servlet;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.URLEncoder;
import java.util.*;
public class BinCatResponse implements HttpServletResponse {
private final Socket socket;
private final Map<String, String> header;
private final ByteArrayOutputStream out;
private int status = 404;
private String statusMessage = "Not Found";
private String charset = "UTF-8";
private int contentLength = 0;
private String contentType = "text/html; charset=UTF-8";
private String location;
public BinCatResponse(Socket socket, Map<String, String> header, ByteArrayOutputStream out) {
this.socket = socket;
this.header = header;
this.out = out;
}
// 此处省略HttpServletResponse接口中的大部分方法,仅保留几个示例方法...
public void setHeader(String name, String value) {
this.header.put(name, value);
}
public String getHeader(String name) {
return header.get(name);
}
public PrintWriter getWriter() throws IOException {
return new PrintWriter(out);
}
}
HttpSession实现
BinCatSession.java示例代码片段:
package com.anbai.sec.server.servlet;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* BinCat Session实现
*/
public class BinCatSession implements HttpSession {
private final String sessionID;
// Http请求Session对象
private final Map<String, Object> sessionMap = new ConcurrentHashMap<String, Object>();
public BinCatSession(String sessionID) {
this.sessionID = sessionID;
}
// 此处省略HttpSession接口中的大部分方法,仅保留几个示例方法...
public Object getAttribute(String name) {
return this.sessionMap.get(name);
}
public void setAttribute(String name, Object value) {
this.sessionMap.put(name, value);
}
}
Servlet类注册
Servlet3.0
支持web.xml
和注解两种方式配置,但不管是通过那种方式都需要知道Servlet
的处理类和映射的URL
地址,这里为了方法理解我将解析web.xml
和扫描@WebServlet
注解的步骤省略了,直接改成了手动配置一个Servlet映射类对象。
注册Servlet类对象代码片段:
// 初始化Servlet映射类对象
final Set<Class<? extends HttpServlet>> servletList = new HashSet<Class<? extends HttpServlet>>();
// 手动注册Servlet类
servletList.add(TestServlet.class);
servletList.add(CMDServlet.class);
当接收到浏览器请求时候我们需要根据请求的URL地址来动态调用Servlet类相关的代码。
调用Servlet类处理Http请求代码片段:
// 处理Http请求URL
for (Class<? extends HttpServlet> clazz : servletList) {
WebServlet webServlet = clazz.getAnnotation(WebServlet.class);
String[] urlPatterns = webServlet.urlPatterns();
for (String urlPattern : urlPatterns) {
try {
// 检测请求的URL地址和Servlet的地址是否匹配
if (Pattern.compile(urlPattern).matcher(uri).find()) {
// 修改状态码
response.setStatus(200, "OK");
// 创建Servlet类实例
HttpServlet httpServlet = clazz.newInstance();
// 调用Servlet请求处理方法
httpServlet.service(request, response);
break;
}
} catch (IOException e) {
// 修改状态码
response.setStatus(500, "Internal Server Error");
e.printStackTrace();
}
}
}
BinCat V3实现
V3
简单的封装了BinCatRequest
、BinCatResponse
、BinCatSession
,还是先了部分的Servlet API
从而实现了一个最初级的Servlet容器
。
V3处理流程:
- 创建服务端
Socket
连接(ServerSocket
)。 - 手动注册
Servlet
类。 - 创建用于处理请求的
BinCatRequest
对象。 BinCatRequest
解析请求协议、请求头、请求参数、Cookie
等。- 创建用于处理请求的
BinCatResponse
对象。 - 解析
Servlet
类的@WebServlet
注解,反射调用Servlet
类方法处理Http请求。 - 输出响应信息以及
Servlet
处理结果。 - 关闭
Socket
连接。
BinCatServerV3实现代码:
package com.anbai.sec.server;
import com.anbai.sec.server.servlet.BinCatRequest;
import com.anbai.sec.server.servlet.BinCatResponse;
import com.anbai.sec.server.test.servlet.CMDServlet;
import com.anbai.sec.server.test.servlet.TestServlet;
import org.javaweb.utils.StringUtils;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import java.util.regex.Pattern;
/**
* ServerSocket Http 服务器示例
*/
public class BinCatServerV3 {
private static final Logger LOG = Logger.getLogger("info");
public static void main(String[] args) {
try {
// 设置服务监听端口
int port = 8080;
// 设置服务名称
String serverName = "BinCat-0.0.3";
// 创建ServerSocket,监听本地端口
ServerSocket ss = new ServerSocket(port);
// 初始化Servlet映射类对象
final Set<Class<? extends HttpServlet>> servletList = new HashSet<Class<? extends HttpServlet>>();
// 手动注册Servlet类
servletList.add(TestServlet.class);
servletList.add(CMDServlet.class);
LOG.info(serverName + " 启动成功,监听端口: " + port);
while (true) {
// 等待客户端连接
Socket socket = ss.accept();
try {
// 获取Socket输入流对象
InputStream in = socket.getInputStream();
// 获取Socket输出流对象
OutputStream out = socket.getOutputStream();
// 创建BinCat请求处理对象
BinCatRequest request = new BinCatRequest(socket);
// 创建BinCat请求处理结果输出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 创建BinCat请求处理结果Header对象
Map<String, String> responseHeader = new ConcurrentHashMap<String, String>();
// 创建BinCat响应处理对象
BinCatResponse response = new BinCatResponse(socket, responseHeader, baos);
// 请求URI地址
String uri = request.getRequestURI();
// 处理Http请求URL
for (Class<? extends HttpServlet> clazz : servletList) {
WebServlet webServlet = clazz.getAnnotation(WebServlet.class);
String[] urlPatterns = webServlet.urlPatterns();
for (String urlPattern : urlPatterns) {
try {
// 检测请求的URL地址和Servlet的地址是否匹配
if (Pattern.compile(urlPattern).matcher(uri).find()) {
// 修改状态码
response.setStatus(200, "OK");
// 创建Servlet类实例
HttpServlet httpServlet = clazz.newInstance();
// 调用Servlet请求处理方法
httpServlet.service(request, response);
break;
}
} catch (Exception e) {
// 修改状态码
response.setStatus(500, "Internal Server Error");
e.printStackTrace();
baos.write(("<pre>" + StringUtils.exceptionToString(e) + "</pre>").getBytes());
}
}
}
// 处理Http响应内容
out.write(("HTTP/1.1 " + response.getStatus() + " " + response.getMessage() + "\n").getBytes());
// 输出Web服务器信息
out.write(("Server: " + serverName + "\n").getBytes());
// 输出返回的消息类型
out.write(("Content-Type: " + response.getContentType() + "\n").getBytes());
// 输出返回字节数
out.write(("Content-Length: " + baos.size() + "\n").getBytes());
// 输出用户自定义的Header
for (String key : responseHeader.keySet()) {
out.write((key + ": " + responseHeader.get(key) + "\n").getBytes());
}
// 写入换行
out.write("\n".getBytes());
// 将读取到的数据写入到客户端Socket
out.write(baos.toByteArray());
in.close();
out.close();
} catch (Exception e) {
LOG.info("处理客户端请求异常:" + e);
} finally {
socket.close();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Servlet功能测试
为了验证BinCat
是否真的具备了Servlet
处理能力,我们写两个测试用例:TestServlet
和CMDServlet
。
TestServlet示例代码:
package com.anbai.sec.server.test.servlet;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
@WebServlet(name = "TestServlet", urlPatterns = "/TestServlet/")
public class TestServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
doPost(request, response);
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
OutputStream out = response.getOutputStream();
out.write(("Hello....<br/>Request Method:" + request.getMethod() + "<br/>Class:" + this.getClass()).getBytes());
}
}
浏览器请求http://localhost:8080/TestServlet/:

CMDServlet示例代码:
package com.anbai.sec.server.test.servlet;
import org.javaweb.utils.IOUtils;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
@WebServlet(name = "CMDServlet", urlPatterns = "/CMD/")
public class CMDServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
doPost(request, response);
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
String cmd = request.getParameter("cmd");
byte[] bytes = IOUtils.toByteArray(Runtime.getRuntime().exec(cmd).getInputStream());
OutputStream out = response.getOutputStream();
out.write(bytes);
out.flush();
out.close();
}
}
浏览器请求http://localhost:8080/CMD/?cmd=whoami:

使用curl
发送POST请求:curl -i localhost:8080/CMD/ -d "cmd=pwd"
,服务器可以正常接收POST参数,处理结果如图:

请求一个错误服务:

至此,我们已经实现了一个非常初级的Servlet容器
了。
BinCat V4 – 实现PHP文件解析
Quercus-QuercusServlet
Quercus
是一个Resin
实现的解析并运行php
文件的jar
库,其本质是使用QuercusServlet
处理所有访问.php
的文件请求,Quercus
会将php文件翻译成java class
文件并在JVM中执行。
添加Quercus
依赖:
<dependency>
<groupId>com.caucho</groupId>
<artifactId>quercus</artifactId>
<version>4.0.63</version>
</dependency>
然后创建一个Quercus
的Servlet
映射,因为BinCat
只支持注解,所以无法在QuercusServlet
类上添加@WebServlet
注解,但是我们可以写一个类去继承QuercusServlet
从而间接的完成Servlet
声明。
QuercusPHPServlet示例:
package com.anbai.sec.server.test.servlet;
import com.caucho.quercus.servlet.QuercusServlet;
import javax.servlet.annotation.WebServlet;
@WebServlet(name = "QuercusPHPServlet", urlPatterns = ".*\\.php$")
public class QuercusPHPServlet extends QuercusServlet {
}
BinCatConfig示例代码(方便统一的Servlet注册):
/**
* 手动注册Servlet并创建BinCatServletContext对象
*
* @param appClassLoader 应用的类加载器
* @return ServletContext Servlet上下文对象
*/
public static BinCatServletContext createServletContext(BinCatWebAppClassLoader appClassLoader) throws Exception {
BinCatServletContext servletContext = new BinCatServletContext(appClassLoader);
// 手动注册Servlet类
Class<Servlet>[] servletClass = new Class[]{
TestServlet.class,
CMDServlet.class,
QuercusPHPServlet.class
};
for (Class<Servlet> clazz : servletClass) {
Servlet servlet = clazz.newInstance();
WebServlet webServlet = clazz.getAnnotation(WebServlet.class);
if (webServlet != null) {
// 获取WebInitParam配置
WebInitParam[] webInitParam = webServlet.initParams();
// 动态创建Servlet对象
ServletRegistration.Dynamic dynamic = servletContext.addServlet(webServlet.name(), servlet);
// 动态设置Servlet映射地址
dynamic.addMapping(webServlet.urlPatterns());
// 设置Servlet启动参数
for (WebInitParam initParam : webInitParam) {
dynamic.setInitParameter(initParam.name(), initParam.value());
}
}
}
// 创建ServletContext
return servletContext;
}
因为QuercusServlet
创建时需要必须有ServletContext
对象,所以我们必须实现ServletContext
接口。除此之外,Servlet
创建时还需要调用Servlet
的初始化方法(public void init(ServletConfig config) throws ServletException
)。调用init
的时候还需要实现ServletConfig
接口。
初始化Servlet代码片段:
/**
* 初始化Servlet
*
* @param servletContext Servlet上下文
* @throws ServletException Servlet处理异常
*/
public static void initServlet(BinCatServletContext servletContext) throws ServletException {
Set<BinCatServletRegistrationDynamic> dynamics = servletContext.getRegistrationDynamics();
for (BinCatServletRegistrationDynamic dynamic : dynamics) {
Servlet servlet = dynamic.getServlet();
String servletName = dynamic.getServletName();
Map<String, String> initParameterMap = dynamic.getInitParameters();
servlet.init(new BinCatServletConfig(servletContext, servletName, initParameterMap));
}
}
BinCatServletContext实现
在Servlet
容器启动的时候必须创建一个ServletContext
(Servlet
上下文),用于管理容器中的所有Servlet
对象。在创建BinCatServletContext
的时候需要创建并初始化所有的Servlet
并存储到servletMap
中。
BinCatServletContext代码片段:
package com.anbai.sec.server.servlet;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.descriptor.JspConfigDescriptor;
import java.io.File;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class BinCatServletContext implements ServletContext {
// 创建一个装动态注册的Servlet的Map
private final Map<String, Servlet> servletMap = new HashMap<>();
// 创建一个装ServletContext初始化参数的Map
private final Map<String, String> initParameterMap = new HashMap<>();
// 创建一个装ServletContext属性对象的Map
private final Map<String, Object> attributeMap = new HashMap<>();
// 创建一个装Servlet动态注册的Set
private final Set<BinCatServletRegistrationDynamic> registrationDynamics = new LinkedHashSet<>();
// BinCatWebAppClassLoader,Web应用的类加载器
private final BinCatWebAppClassLoader appClassLoader;
public BinCatServletContext(BinCatWebAppClassLoader appClassLoader) throws Exception {
this.appClassLoader = appClassLoader;
}
// 此处省略ServletContext接口中的大部分方法,仅保留几个示例方法...
@Override
public Servlet getServlet(String name) throws ServletException {
return servletMap.get(name);
}
@Override
public Enumeration<Servlet> getServlets() {
Set<Servlet> servlets = new HashSet<Servlet>();
servlets.addAll(servletMap.values());
return Collections.enumeration(servlets);
}
@Override
public Enumeration<String> getServletNames() {
Set<String> servlets = new HashSet<String>();
servlets.addAll(servletMap.keySet());
return Collections.enumeration(servlets);
}
}
BinCatServletConfig实现
在创建BinCatServletContext
时我们指定了一个ServletConfig
实现:BinCatServletConfig
,ServletConfig
用于指定Servlet
启动时的配置信息。
BinCatServletConfig实现:
package com.anbai.sec.server.servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
public class BinCatServletConfig implements ServletConfig {
private final BinCatServletContext servletContext;
private final WebServlet webServlet;
private final WebInitParam[] webInitParam;
public BinCatServletConfig(BinCatServletContext servletContext, WebServlet webServlet) {
this.servletContext = servletContext;
this.webServlet = webServlet;
this.webInitParam = webServlet.initParams();
}
@Override
public String getServletName() {
return webServlet.name();
}
@Override
public ServletContext getServletContext() {
return this.servletContext;
}
@Override
public String getInitParameter(String name) {
for (WebInitParam initParam : webInitParam) {
String paramName = initParam.name();
if (paramName.equals(name)) {
return initParam.value();
}
}
return null;
}
@Override
public Enumeration<String> getInitParameterNames() {
Set<String> initParamSet = new HashSet<String>();
for (WebInitParam initParam : webInitParam) {
initParamSet.add(initParam.name());
}
return Collections.enumeration(initParamSet);
}
}
BinCatDispatcherServlet实现
为了方便后续的BinCat
版本处理Http请求和响应处理结果,我们简单的封装了BinCatDispatcherServlet
和BinCatResponseHandler
对象。BinCatDispatcherServlet
会根据浏览器请求的不同URL地址去调用对应的Servlet
服务,除此之外还提供了一个简单的静态资源文件处理逻辑和PHP
解析功能。
BinCatDispatcherServlet实现代码:
package com.anbai.sec.server.handler;
import com.anbai.sec.server.servlet.BinCatRequest;
import com.anbai.sec.server.servlet.BinCatResponse;
import com.anbai.sec.server.servlet.BinCatServletContext;
import com.anbai.sec.server.servlet.BinCatServletRegistrationDynamic;
import org.javaweb.utils.FileUtils;
import org.javaweb.utils.StringUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Collection;
import java.util.Set;
import java.util.regex.Pattern;
public class BinCatDispatcherServlet {
public void doDispatch(BinCatRequest req, BinCatResponse resp, ByteArrayOutputStream out) throws IOException {
// 请求URI地址
String uri = req.getRequestURI();
// 获取ServletContext
BinCatServletContext servletContext = (BinCatServletContext) req.getServletContext();
// 获取Http请求的文件
File requestFile = new File(req.getRealPath(uri));
// 处理Http请求的静态文件,如果文件存在(.php后缀除外)就直接返回文件内容,不需要调用Servlet
if (requestFile.exists() && requestFile.isFile() && !uri.endsWith(".php")) {
// 修改状态码
resp.setStatus(200, "OK");
// 解析文件的MimeType
String mimeType = Files.probeContentType(requestFile.toPath());
if (mimeType == null) {
String fileSuffix = FileUtils.getFileSuffix(requestFile.getName());
resp.setContentType("text/" + fileSuffix);
} else {
resp.setContentType(mimeType);
}
out.write(Files.readAllBytes(requestFile.toPath()));
} else {
// 遍历所有已注册得Servlet,处理Http请求
Set<BinCatServletRegistrationDynamic> dynamics = servletContext.getRegistrationDynamics();
for (BinCatServletRegistrationDynamic dynamic : dynamics) {
Collection<String> urlPatterns = dynamic.getMappings();
for (String urlPattern : urlPatterns) {
try {
// 检测请求的URL地址和Servlet的地址是否匹配
if (Pattern.compile(urlPattern).matcher(uri).find()) {
// 修改状态码
resp.setStatus(200, "OK");
// 调用Servlet请求处理方法
dynamic.getServlet().service(req, resp);
return;
}
} catch (Exception e) {
// 修改状态码,输出服务器异常信息到浏览器
resp.setStatus(500, "Internal Server Error");
e.printStackTrace();
out.write(("<pre>" + StringUtils.exceptionToString(e) + "</pre>").getBytes());
}
}
}
}
}
}
BinCatResponseHandler实现
BinCatResponseHandler
只是一个简单封装的用于向浏览器输出Http处理请求结果的对象。
BinCatResponseHandler实现代码:
package com.anbai.sec.server.handler;
import com.anbai.sec.server.servlet.BinCatResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;
public class BinCatResponseHandler {
public void processResult(BinCatResponse response, Map<String, String> responseHeader, String serverName,
OutputStream out, ByteArrayOutputStream baos) throws IOException {
// 处理Http响应内容
out.write(("HTTP/1.1 " + response.getStatus() + " " + response.getMessage() + "\n").getBytes());
// 输出Web服务器信息
out.write(("Server: " + serverName + "\n").getBytes());
// 输出返回的消息类型
out.write(("Content-Type: " + response.getContentType() + "\n").getBytes());
// 输出返回字节数
out.write(("Content-Length: " + baos.size() + "\n").getBytes());
// 输出用户自定义的Header
for (String key : responseHeader.keySet()) {
out.write((key + ": " + responseHeader.get(key) + "\n").getBytes());
}
// 写入换行
out.write("\n".getBytes());
// 将读取到的数据写入到客户端Socket
out.write(baos.toByteArray());
}
}
BinCat V4实现
V4
在V3
的基础上实现了ServletConfig
、ServletContext
接口,从而实现了Servlet
的实例化
和初始化
,BinCatDispatcherServlet
实现的Servlet
服务调用。
BinCatServerV4实现代码:
package com.anbai.sec.server;
import com.anbai.sec.server.config.BinCatConfig;
import com.anbai.sec.server.handler.BinCatDispatcherServlet;
import com.anbai.sec.server.handler.BinCatResponseHandler;
import com.anbai.sec.server.servlet.BinCatRequest;
import com.anbai.sec.server.servlet.BinCatResponse;
import com.anbai.sec.server.servlet.BinCatServletContext;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
/**
* ServerSocket Http 服务器示例
*/
public class BinCatServerV4 {
// 设置服务监听端口
private static final int PORT = 8080;
// 设置服务名称
private static final String SERVER_NAME = "BinCat-0.0.4";
private static final Logger LOG = Logger.getLogger("info");
public static void main(String[] args) {
try {
// 创建ServerSocket,监听本地端口
ServerSocket ss = new ServerSocket(PORT);
// 创建BinCatServletContext对象
BinCatServletContext servletContext = BinCatConfig.createServletContext();
// 初始化Servlet
BinCatConfig.initServlet(servletContext);
LOG.info(SERVER_NAME + " 启动成功,监听端口: " + PORT);
while (true) {
// 等待客户端连接
Socket socket = ss.accept();
try {
// 获取Socket输入流对象
InputStream in = socket.getInputStream();
// 获取Socket输出流对象
OutputStream out = socket.getOutputStream();
// 创建BinCat请求处理对象
BinCatRequest request = new BinCatRequest(socket, servletContext);
// 创建BinCat请求处理结果输出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 创建BinCat请求处理结果Header对象
Map<String, String> responseHeader = new ConcurrentHashMap<String, String>();
// 创建BinCat响应处理对象
BinCatResponse response = new BinCatResponse(socket, responseHeader, baos);
// 创建BinCatDispatcherServlet对象,用于分发Http请求
BinCatDispatcherServlet dispatcherServlet = new BinCatDispatcherServlet();
// 创建BinCatResponseHandler对象,用于处理Http请求结果
BinCatResponseHandler responseHandler = new BinCatResponseHandler();
// 使用BinCatDispatcherServlet处理Servlet请求
dispatcherServlet.doDispatch(request, response, baos);
// 响应服务器处理结果
responseHandler.processResult(response, responseHeader, SERVER_NAME, out, baos);
in.close();
out.close();
} catch (Exception e) {
LOG.info("处理客户端请求异常:" + e);
} finally {
socket.close();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
BinCat PHP解析测试
我们需要在javaweb-sec
项目根目录创建一个测试文件,如info.php
:
<?php phpinfo();?>

启动BinCat V4后访问http://localhost:8080/info.php:

复制一个最新版本的Discuz
到javaweb-sec
目录,尝试安装Discuz
,访问:http://localhost:8080/discuz/install/index.php

Discuz
环境检测正常:

测试BinCat
的PHP
解析功能正常,只是开始安装Discuz
时无法下一步,无异常和错误卡了,无法完成安装。
BinCat V5 – 支持SpringBoot应用
时至今日(2020年9月
),SpringBoot
因其配置非常简单功能强大,已经成为了绝大部分微服务项目的首选架构,Servlet 3+
的新特性也为SpringBoot
的便捷配置提供了非常大的帮助。我们将使用BinCat V5
启动并运行一个用SpringBoot
实现的Blog
应用,从而来学习Servlet
容器的工作原理。
创建基于SpringBoot的javasec-blog项目
首先我们在javaweb-sec
项目下创建一个javasec-test
的模块(用于存储javasec
文章所用到的测试项目),然后我们在javasec-test
模块中创建一个javasec-blog
模块(一个标准的SpringBoot
项目),javasec-blog
项目是一个用于演示的博客项目。

javasec-blog War项目构建
SpringBoot
不但支持嵌入式部署也支持传统的war
包部署方式,但需要注意的是war
包部署的时候需要做一些特殊的修改。BinCat
目前只实现了基于war
部署的方式,所以我们需要将javasec-blog
打成一个war
包。
构建项目的时候可参考如下步骤(1-3步默认已修改,不需要关注):
- 修改
pom.xml
添加<packaging>war</packaging>
,默认是jar
。 - 修改
pom.xml
的build
、plugins
标签,添加:<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>${maven-deploy-plugin.version}</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin>
- 修改
SpringBoot
启动类代码,示例中是:JavaWebBlogApplication
。继承org.springframework.boot.web.servlet.support.SpringBootServletInitializer
,然后重写configure
方法。如下:
package com.anbai.sec.blog.config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
/**
* @author yz
*/
@SpringBootApplication(scanBasePackages = "com.anbai.sec.blog.*")
public class JavaWebBlogApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(JavaWebBlogApplication.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(JavaWebBlogApplication.class);
}
}
4.修改javaweb-sec-source/javasec-test/javasec-blog/src/main/resources/application.properties
配置文件中的数据库信息。
5.新建Mysql
数据库javaweb-blog
,并导入javaweb-sec-source/javasec-test/javaweb-blog.sql
。
6.使用maven
命令构建整个javaweb-sec
项目(第二次构建的时候可以单独build blog项目),在javaweb-sec
根目录执行:mvn clean install
。
项目构建成功后结构如下:

BinCat V5实现
因为V5
要实现正常的运行一个SpringBoot
项目,所以我们需要写一个支持单应用的Servlet
容器,而且还需要实现之前版本未实现的其他Servlet
接口。
BinCatWebAppClassLoader实现
为了实现加载Web应用的资源和类文件,我们需要实现一个类简单的加载器,用于加载WEB-INF
目录下的classes
和libs
中的所有文件。
BinCatWebAppClassLoader代码:
package com.anbai.sec.server.loader;
import java.net.URL;
import java.net.URLClassLoader;
public class BinCatWebAppClassLoader extends URLClassLoader {
public BinCatWebAppClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
}
为了统一加载Web应用的资源文件,我们还需要将BinCatWebAppClassLoader
的类示例设置为当前线程的类加载器,如下:
// 定义需要加载的Web应用路径,默认配置的路径必须先使用maven编译:javasec-blog项目
String webAppFile = System.getProperty("user.dir") +
"/javaweb-sec-source/javasec-test/javasec-blog/target/javasec-blog-1.0.0/";
// 创建BinCatWebAppClassLoader,加载Web应用
BinCatWebAppClassLoader appClassLoader = BinCatConfig.createAppClassLoader(webAppFile);
// 设置当前线程的上下文类加载器
Thread.currentThread().setContextClassLoader(appClassLoader);
这样一来在整个Web应用
(示例中指的是javasec-blog
项目)默认将会使用BinCatWebAppClassLoader
来实现资源文件和类文件的加载了。
BinCatWebAppClassLoader初始化
BinCatWebAppClassLoader
在初始化的时候需要加载所有应用的WEB-INF
目录的类和资源文件,但由于V5
版本我们只想实现单应用的部署,所以我们只需要加载我们预设好的Web应用
目录就好了。在初始化BinCatWebAppClassLoader
的时候将WEB-INF/classes
目录和WEB-INF/libs
目录下的所有jar文件
都添加到BinCatWebAppClassLoader
当中。
BinCatWebAppClassLoader创建并加载Web应用资源代码:
public static BinCatWebAppClassLoader createAppClassLoader(String webAppFile) throws IOException {
File webRoot = new File(webAppFile);
File webInfoDir = new File(webRoot, "WEB-INF");
File libDir = new File(webInfoDir, "lib");
File classesDir = new File(webInfoDir, "classes");
Set<URL> classPathURL = new HashSet<>();
File[] libs = libDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".jar");
}
});
// 加载lib目录下所有的jar文件
for (File lib : libs) {
classPathURL.add(lib.toURL());
}
// 加载classes目录的所有资源文件
classPathURL.add(classesDir.toURL());
// 创建Web应用的类加载器
return new BinCatWebAppClassLoader(
classPathURL.toArray(new URL[classPathURL.size()]), BinCatConfig.class.getClassLoader()
);
}
BinCatServletContext实现
在V4
版本中我们虽然已经实现了一个BinCatServletContext
,但是我们并没有实现Servlet
的动态注册功能,而且V4
实现的Servlet
注册和初始化过程都是静态的,我们需要将整个过程升级为动态的。
- 为了能够在
BinCatServletContext
中获取到我们自定义的Web应用
的类加载器,我们需要在创建BinCatServletContext
的时候将将其缓存到类对象中。 - 创建一个
Map<String, Servlet> servletMap
和Set<BinCatServletRegistrationDynamic> registrationDynamics
对象用于缓存动态注册的Servlet
。 - 创建一个
Map<String, String> initParameterMap
对象用于记录ServletContext
初始化参数。 - 创建一个
Map<String, Object> attributeMap
,用于记录ServletContext
中的属性对象(attribute
)。
BinCatServletContext代码:
package com.anbai.sec.server.servlet;
import com.anbai.sec.server.loader.BinCatWebAppClassLoader;
import javax.servlet.*;
import javax.servlet.descriptor.JspConfigDescriptor;
import java.io.File;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
public class BinCatServletContext implements ServletContext {
// 创建一个装动态注册的Servlet的Map
private final Map<String, Servlet> servletMap = new HashMap<>();
// 创建一个装ServletContext初始化参数的Map
private final Map<String, String> initParameterMap = new HashMap<>();
// 创建一个装ServletContext属性对象的Map
private final Map<String, Object> attributeMap = new HashMap<>();
// 创建一个装Servlet动态注册的Set
private final Set<BinCatServletRegistrationDynamic> registrationDynamics = new LinkedHashSet<>();
// BinCatWebAppClassLoader,Web应用的类加载器
private final BinCatWebAppClassLoader appClassLoader;
// 此处省略ServletContext接口中的大部分方法,仅保留几个实现了的示例方法...
public BinCatServletContext(BinCatWebAppClassLoader appClassLoader) throws Exception {
this.appClassLoader = appClassLoader;
}
@Override
public Servlet getServlet(String name) throws ServletException {
return servletMap.get(name);
}
@Override
public Enumeration<Servlet> getServlets() {
Set<Servlet> servlets = new HashSet<Servlet>(servletMap.values());
return Collections.enumeration(servlets);
}
@Override
public Enumeration<String> getServletNames() {
Set<String> servlets = new HashSet<String>(servletMap.keySet());
return Collections.enumeration(servlets);
}
@Override
public String getRealPath(String path) {
return new File(System.getProperty("user.dir"), path).getAbsolutePath();
}
public Map<String, String> getInitParameterMap() {
return initParameterMap;
}
@Override
public String getInitParameter(String name) {
return initParameterMap.get(name);
}
@Override
public Enumeration<String> getInitParameterNames() {
return Collections.enumeration(initParameterMap.keySet());
}
@Override
public boolean setInitParameter(String name, String value) {
if (!initParameterMap.containsKey(name)) {
initParameterMap.put(name, value);
return true;
}
return false;
}
@Override
public Object getAttribute(String name) {
return attributeMap.get(name);
}
@Override
public Enumeration<String> getAttributeNames() {
return Collections.enumeration(attributeMap.keySet());
}
@Override
public void setAttribute(String name, Object object) {
attributeMap.put(name, object);
}
@Override
public void removeAttribute(String name) {
attributeMap.remove(name);
}
@Override
public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) {
servletMap.put(servletName, servlet);
BinCatServletRegistrationDynamic dynamic = new BinCatServletRegistrationDynamic(servletName, servlet, this);
registrationDynamics.add(dynamic);
return dynamic;
}
@Override
public ClassLoader getClassLoader() {
return this.appClassLoader;
}
public Map<String, Servlet> getServletMap() {
return servletMap;
}
public Set<BinCatServletRegistrationDynamic> getRegistrationDynamics() {
return registrationDynamics;
}
}
BinCatServletContext初始化
在初始化BinCatServletContext
时我们通过手动注册Servlet
的方式初始化了几个容器内置的Servlet
,并通过扫描初始化Servlet
类注解的方式将Servlet
在BinCatServletContext
中注册。
BinCatServletContext初始化代码:
/**
* 手动注册Servlet并创建BinCatServletContext对象
*
* @param appClassLoader 应用的类加载器
* @return ServletContext Servlet上下文对象
*/
public static BinCatServletContext createServletContext(BinCatWebAppClassLoader appClassLoader) throws Exception {
BinCatServletContext servletContext = new BinCatServletContext(appClassLoader);
// 手动注册Servlet类
Class<Servlet>[] servletClass = new Class[]{
TestServlet.class,
CMDServlet.class,
QuercusPHPServlet.class
};
for (Class<Servlet> clazz : servletClass) {
Servlet servlet = clazz.newInstance();
WebServlet webServlet = clazz.getAnnotation(WebServlet.class);
if (webServlet != null) {
// 获取WebInitParam配置
WebInitParam[] webInitParam = webServlet.initParams();
// 动态创建Servlet对象
ServletRegistration.Dynamic dynamic = servletContext.addServlet(webServlet.name(), servlet);
// 动态设置Servlet映射地址
dynamic.addMapping(webServlet.urlPatterns());
// 设置Servlet启动参数
for (WebInitParam initParam : webInitParam) {
dynamic.setInitParameter(initParam.name(), initParam.value());
}
}
}
// 创建ServletContext
return servletContext;
}
BinCatServletRegistrationDynamic实现
BinCatServletContext
中核心的是addServlet
方法的动态注册Servlet
,如下:
@Override
public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) {
servletMap.put(servletName, servlet);
BinCatServletRegistrationDynamic dynamic = new BinCatServletRegistrationDynamic(servletName, servlet, this);
registrationDynamics.add(dynamic);
return dynamic;
}
当Web应用
调用addServlet
注册一个Servlet
对象时会返回一个ServletRegistration.Dynamic
对象,通过这个Dynamic
对象可以设置Servlet
的映射信息和初始化信息。但因为ServletRegistration.Dynamic
是一个接口,所以我们必须实现此接口的所有方法。
BinCatServletRegistrationDynamic代码:
package com.anbai.sec.server.servlet;
import javax.servlet.MultipartConfigElement;
import javax.servlet.Servlet;
import javax.servlet.ServletRegistration;
import javax.servlet.ServletSecurityElement;
import java.util.*;
public class BinCatServletRegistrationDynamic implements ServletRegistration.Dynamic {
private final String servletName;
private final Set<String> servletMapping = new LinkedHashSet<>();
private final Map<String, String> initParametersMap = new HashMap<>();
private final Servlet servlet;
private final BinCatServletContext servletContext;
public BinCatServletRegistrationDynamic(String servletName, Servlet servlet, BinCatServletContext servletContext) {
this.servletName = servletName;
this.servlet = servlet;
this.servletContext = servletContext;
}
@Override
public void setLoadOnStartup(int loadOnStartup) {
}
@Override
public Set<String> setServletSecurity(ServletSecurityElement constraint) {
return null;
}
@Override
public void setMultipartConfig(MultipartConfigElement multipartConfig) {
}
@Override
public void setRunAsRole(String roleName) {
}
@Override
public void setAsyncSupported(boolean isAsyncSupported) {
}
@Override
public Set<String> addMapping(String... urlPatterns) {
Collections.addAll(servletMapping, urlPatterns);
return servletMapping;
}
@Override
public Collection<String> getMappings() {
return servletMapping;
}
@Override
public String getRunAsRole() {
return null;
}
@Override
public String getName() {
return servletName;
}
@Override
public String getClassName() {
return servlet.getClass().getName();
}
@Override
public boolean setInitParameter(String name, String value) {
if (!initParametersMap.containsKey(name)) {
initParametersMap.put(name, value);
return true;
}
return false;
}
@Override
public String getInitParameter(String name) {
return initParametersMap.get(name);
}
@Override
public Set<String> setInitParameters(Map<String, String> initParameters) {
initParametersMap.putAll(initParameters);
return initParametersMap.keySet();
}
@Override
public Map<String, String> getInitParameters() {
return initParametersMap;
}
public Servlet getServlet() {
return servlet;
}
public String getServletName() {
return servletName;
}
}
实现Web应用的启动
在启动我们的示例应用之前我们已经创建了一个BinCatWebAppClassLoader
和BinCatServletContext
,其中BinCatWebAppClassLoader
已经加载了javasec-blog
项目的WEB-INF
信息、BinCatServletContext
也初始化了几个BinCat
内置的Servlet
。万事俱备,我们现在只欠如何启动Servlet
容器了。
从Servlet3.0
开始,Servlet
除了可以从web.xml
启动,还可以通过SPI
机制动态获取ServletContainerInitializer(简称SCI)
并通过SCI
启动Servlet容器
。因为V5
并未打算实现传统的web.xml
启动方式,而是只想实现SCI
启动方式(这也是SpringBoot
以war包
部署的启动方式)。
ServletContainerInitializer
是一个接口类,仅定义了一个叫onStartup
的方法,所有实现了该接口的类只要重写onStartup
方法将可以实现Servlet容器
的启动。
ServletContainerInitializer代码:
package javax.servlet;
import java.util.Set;
/**
* Interface which allows a library/runtime to be notified of a web
* application's startup phase and perform any required programmatic
* registration of servlets, filters, and listeners in response to it.
*
* <p>Implementations of this interface may be annotated with
* {@link javax.servlet.annotation.HandlesTypes HandlesTypes}, in order to
* receive (at their {@link #onStartup} method) the Set of application
* classes that implement, extend, or have been annotated with the class
* types specified by the annotation.
*
* <p>If an implementation of this interface does not use <tt>HandlesTypes</tt>
* annotation, or none of the application classes match the ones specified
* by the annotation, the container must pass a <tt>null</tt> Set of classes
* to {@link #onStartup}.
*
* <p>When examining the classes of an application to see if they match
* any of the criteria specified by the <tt>HandlesTypes</tt> annontation
* of a <tt>ServletContainerInitializer</tt>, the container may run into
* classloading problems if any of the application's optional JAR
* files are missing. Because the container is not in a position to decide
* whether these types of classloading failures will prevent
* the application from working correctly, it must ignore them,
* while at the same time providing a configuration option that would
* log them.
*
* <p>Implementations of this interface must be declared by a JAR file
* resource located inside the <tt>META-INF/services</tt> directory and
* named for the fully qualified class name of this interface, and will be
* discovered using the runtime's service provider lookup mechanism
* or a container specific mechanism that is semantically equivalent to
* it. In either case, <tt>ServletContainerInitializer</tt> services from web
* fragment JAR files excluded from an absolute ordering must be ignored,
* and the order in which these services are discovered must follow the
* application's classloading delegation model.
*
* @see javax.servlet.annotation.HandlesTypes
*
* @since Servlet 3.0
*/
public interface ServletContainerInitializer {
/**
* Notifies this <tt>ServletContainerInitializer</tt> of the startup
* of the application represented by the given <tt>ServletContext</tt>.
*
* <p>If this <tt>ServletContainerInitializer</tt> is bundled in a JAR
* file inside the <tt>WEB-INF/lib</tt> directory of an application,
* its <tt>onStartup</tt> method will be invoked only once during the
* startup of the bundling application. If this
* <tt>ServletContainerInitializer</tt> is bundled inside a JAR file
* outside of any <tt>WEB-INF/lib</tt> directory, but still
* discoverable as described above, its <tt>onStartup</tt> method
* will be invoked every time an application is started.
*
* @param c the Set of application classes that extend, implement, or
* have been annotated with the class types specified by the
* {@link javax.servlet.annotation.HandlesTypes HandlesTypes} annotation,
* or <tt>null</tt> if there are no matches, or this
* <tt>ServletContainerInitializer</tt> has not been annotated with
* <tt>HandlesTypes</tt>
*
* @param ctx the <tt>ServletContext</tt> of the web application that
* is being started and in which the classes contained in <tt>c</tt>
* were found
*
* @throws ServletException if an error has occurred
*/
public void onStartup(Set<Class<?>> c, ServletContext ctx)
throws ServletException;
}
因为BinCatWebAppClassLoader
已经加载了示例项目的所有资源文件,所以我们可以使用BinCatWebAppClassLoader
来获取到示例项目中的所有路径为:META-INF/services/javax.servlet.ServletContainerInitializer
的资源文件。
BinCatWebAppClassLoader classLoader = (BinCatWebAppClassLoader) servletContext.getClassLoader();
String servletService = "META-INF/services/javax.servlet.ServletContainerInitializer";
// 获取当前ClassLoader中的所有ServletContainerInitializer配置
Enumeration<URL> resources = classLoader.getResources(servletService);
SCI
资源文件中配置的是实现了SCI
接口的Java类
,一个SCI
资源文件可能会配置多个实现类(多个类以换行分割)。读取到SCI
资源文件后我们将可以使用反射的方式去加载资源文件中配置的Java类
了。
读取SCI配置并加载SCI实现类示例:

上图示例中可以看到我们通过读取BinCatWebAppClassLoader
中的SCI
资源时获取到了一个类名为:org.springframework.web.SpringServletContainerInitializer
的类。SpringServletContainerInitializer
实现了ServletContainerInitializer
,所以想要启动示例的SpringBoot
项目仅需要调用该类的onStartup
方法将可以了。onStartup
方法需要两个参数:Set<Class<?>> c
和ServletContext ctx
,即可SCI启动类对象集合
和Servlet上下文
。因为ServletContext
我们可以轻易的获取到,所以我们重点是如何获取到SCI启动类对象集合
了。
通过阅读源码不难发现,在实现了SCI
的接口类类名上会有一个叫做@HandlesTypes
的注解,读取到这个注解的值将可以找到处理SCI启动类对象集合
的类对象,然后反向去找该类对象的实现类将可以找到所有需要被SCI
启动的类数组了。
SpringServletContainerInitializer示例:

创建SCI
实现类示例和获取该示例的HandlesTypes
配置方式如下:
Class<?> initClass = Class.forName(className, true, classLoader);
HandlesTypes handlesTypes = initClass.getAnnotation(HandlesTypes.class);
ServletContainerInitializer sci = (ServletContainerInitializer) initClass.newInstance();
sciClassMap.put(sci, new HashSet<Class<?>>());
if (handlesTypes != null) {
Class[] handlesClass = handlesTypes.value();
handlesTypesMap.put(sci, handlesClass);
}
获取到@HandlesTypes
配置的类名后剩下的就是如何在示例项目中的所有类中找出@HandlesTypes
配置的类实例了,如上图中SpringServletContainerInitializer
配置的@HandlesTypes
是WebApplicationInitializer
,那么我们现在就必须通过扫包和类文件的方式找出所有WebApplicationInitializer
的子类,然后再通过上一步创建出来的SCI
实例调用onStartup
方法完成Servlet
容器启动。
为了扫描当前类加载所有的类对象,我们需要先获取出当前类加载加载的所有的类名称,然后再依次扫描这些类是否是@HandlesTypes
中配置的类(如:WebApplicationInitializer
)的子类。
获取BinCatWebAppClassLoader加载的所有的类名代码:
/**
* 获取BinCatWebAppClassLoader类加载器加载的所有class类名
*
* @param classLoader 类加载器
* @param sciClassMap SCI类对象
* @param handlesTypesMap SCI类对象配置的HandlesTypes对象映射Map
* @return
* @throws Exception
*/
private static void findInitializerClass(
BinCatWebAppClassLoader classLoader,
Map<ServletContainerInitializer, Set<Class<?>>> sciClassMap,
Map<ServletContainerInitializer, Class<?>[]> handlesTypesMap) throws Exception {
// 创建一个存储所有被BinCatWebAppClassLoader加载的类名称对象
Set<String> classList = new HashSet<>();
// 获取BinCatWebAppClassLoader加载的所有URL地址
URL[] urls = classLoader.getURLs();
for (URL url : urls) {
File file = new File(url.toURI());
// 遍历所有的jar文件
if (file.isFile() && file.getName().endsWith(".jar")) {
JarFile jarFile = new JarFile(file);
Enumeration<JarEntry> jarEntry = jarFile.entries();
while (jarEntry.hasMoreElements()) {
JarEntry entry = jarEntry.nextElement();
String fileName = entry.getName();
// 遍历jar文件中的所有class文件,并转换成java类名格式,如com/anbai/Test.class会转换成com.anbai.Test
if (fileName.endsWith(".class")) {
String className = fileName.replace(".class", "").replace("/", ".");
classList.add(className);
}
}
} else if (file.isDirectory()) {
// 遍历所有classes目录下的.class文件,并转换成java类名格式
Collection<File> files = FileUtils.listFiles(file, new String[]{"class"}, true);
for (File classFile : files) {
String className = classFile.toString().substring(file.toString().length())
.replace(".class", "").replaceAll("^/", "").replace("/", ".");
classList.add(className);
}
}
}
// 通过ASM方式获取所有Java类的继承关系,并判断是否是HandlesTypes配置中的类的子类
for (String className : classList) {
// 通过ASM的方式获取当前类的所有父类(包括继承和实现的所有类)
Set<String> superClassList = ClassUtils.getSuperClassListByAsm(className, classLoader);
// 遍历所有HandlesTypes配置
for (ServletContainerInitializer sci : handlesTypesMap.keySet()) {
// 获取HandlesTypes配置的类数组对象
Class[] handlesTypesClass = handlesTypesMap.get(sci);
// 遍历所有HandlesTypes配置的类数组对象
for (Class typesClass : handlesTypesClass) {
// 获取HandlesTypes配置的类名称
String typeClassName = typesClass.getName();
// 检测当前Java类是否是HandlesTypes配置的类的子类,如果是就记录下来
if (superClassList.contains(typeClassName) && !className.equals(typeClassName)) {
// 获取SCI启动类对象集合
Set<Class<?>> sciClass = sciClassMap.get(sci);
// 反射加载当前类对象
Class clazz = classLoader.loadClass(className);
// 将找到的SCI启动类添加到集合中
sciClass.add(clazz);
}
}
}
}
}
整个扫包过程会比较缓慢,因为一次性扫描了1万多个类,并且还是用ASM
解析了这1万多个类的继承关系。最终我们将会得出一个Map<ServletContainerInitializer, Set<Class<?>>> sciClassMap
对象,我们通过遍历这个sciClassMap
并依次调用onStartup
方法即可实现Servlet容器
的启动。
启动所有SCI代码:
for (ServletContainerInitializer initializer : sciClassMap.keySet()) {
Set<Class<?>> initClassSet = sciClassMap.get(initializer);
// 调用Servlet容器初始化的onStartup方法,启动容器
initializer.onStartup(initClassSet, servletContext);
}
initializer.onStartup(initClassSet, servletContext);
因为传入了ServletContext
,所以initializer
很有可能会通过ServletContext
动态注册一些Filter
、Servlet
、Listener
,而这些Filter
、Servlet
、Listener
和我们BinCat
的内置的Servlet
都处于未初始化的状态,这个时候我们就必须要做一些初始化工作(V5
版本只支持Servlet
,并不支持Filter
和Listener
)了。
初始化ServletContext中的所有Servlet代码:
initServlet(servletContext);
完成以上所有逻辑后我们的BinCat
也就算启动成功了,剩下的就是如何处理浏览器请求了。

Servlet请求处理
V5
依旧是根据浏览器请求的URL地址调用对应的Servlet
的service
方法处理Servlet
请求,访问javasec-blog
首页测试:http://localhost:8080/

请求的/
最终会调用SpringMVC
的org.springframework.web.servlet.DispatcherServlet
类处理请求,如下:

dynamic.getServlet().service(req, resp)
最终会调用Spring
的DispatcherServlet
类的父类org.springframework.web.servlet.FrameworkServlet
的service
方法,如下:

访问文章详情页测试:http://localhost:8080/?p=12

至此,耗时大约一周时间,我们的BinCat
从支持解析简单的HelloWorld
到如今已经实现了启动单Web应用
的SpringBoot
了,当然这里面充斥这各种各样的Bug和安全问题,我们的目标并不是实现一个国产化Java中间件
,而是将BinCat
变成一个存在各种各样漏洞的靶场。
转载请注明出处及链接