最近在調試一個項目時常常需要對接口進行抓包查看,接口位於微信的公衆號內,目前每次調試時都是用的 fiddler進行抓包查看的。但每次打開fiddler去查看對應的接口並找到對應的參數感覺還是有點複雜,正好今天是週末,打算自己來研究下它的原理並自己通過java來寫一個(之所以知道java可以實現這個功能是因爲著名的web安全檢測工具 burpsuite 就是用java寫的)
分析
在使用fiddler或burpsuite時其抓包的原理都是通過代理服務器來實現的。fiddler或burpsuite通過自己創建一個代理服務器對需要攔截的socket請求進行一次中轉,其過程有點像中間人的方式,從而可以實現對請求和響應的攔截和修改。
知道了原理後,那麼通過JAVA編寫一個用於轉發socket的程序就可以實現請求的攔截了.
爲了開發的方便與高效,這裏採用netty框架來顯示代理服務器的開發
本文需要的依賴包爲:
<!-- https://mvnrepository.com/artifact/io.netty/netty-all --> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.42.Final</version> </dependency>
http請求代理
微信公衆號內的接口目前全都是https的,直接開發https代理程序有一定難度,所以筆者決定在實現https的接口抓包之前還是先來搞定http的抓包攔截功能
在開發之前先梳理下思圖:這裏以谷歌瀏覽器訪問百度網站爲例,先畫下其訪問流程圖
http請求無代理
對於http請求無代理的情況其過程很簡單,客戶端向服務器發起請求,服務端響應此請求即可
http請求有代理
由於http請求太過簡單,其所有的數據傳輸也都是明文傳輸了。其最大的安全性是很容易受到中間人攻擊(MITM)。與MITM類比,那麼此處的http代理服務器也就是中間人了。作爲中間人服務器,它對於客戶端的請求可以進行攔截、查看、過濾、轉發、篡改等,由代理服務器處理完畢後再決定是否轉發給目標服務器。同時對於目標服務器的響應也由中間的代理服務器先進行處理一遍,再決定怎樣傳回給客戶端。
如果用netty來實現http的代理服務器其主要代碼如下:
public class HttpProxyHandler extends ChannelInboundHandlerAdapter implements IProxyHandler {
private Logger logger = LoggerFactory.getLogger(HttpProxyHandler.class);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
logger.debug("[HttpProxyHandler]");
if (msg instanceof HttpRequest) {
HttpRequest httpRequest = (HttpRequest) msg;
//獲取客戶端請求
ClientRequest clientRequest = ProxyRequestUtil.getClientRequest(ctx.channel());
if (clientRequest == null) {
//從本次請求中獲取
Attribute<ClientRequest> clientRequestAttribute = ctx.channel().attr(CLIENTREQUEST_ATTRIBUTE_KEY);
clientRequest = ProxyRequestUtil.getClientReuqest(httpRequest);
//將clientRequest保存到channel中
clientRequestAttribute.setIfAbsent(clientRequest);
}
//如果是connect代理請求,返回成功以代表代理成功
if (sendSuccessResponseIfConnectMethod(ctx, httpRequest.method().name())) {
logger.debug("[HttpProxyHandler][channelRead] sendSuccessResponseConnect");
ctx.channel().pipeline().remove("httpRequestDecoder");
ctx.channel().pipeline().remove("httpResponseEncoder");
ctx.channel().pipeline().remove("httpAggregator");
ReferenceCountUtil.release(msg);
return;
}
if (clientRequest.isHttps()) {
//https請求不在此處轉發
super.channelRead(ctx, msg);
return;
}
sendToServer(clientRequest, ctx, msg);
return;
}
super.channelRead(ctx, msg);
}
/**
* 如果是connect請求的話,返回連接建立成功
*
* @param ctx ChannelHandlerContext
* @param methodName 請求類型名
* @return 是否爲connect請求
*/
private boolean sendSuccessResponseIfConnectMethod(ChannelHandlerContext ctx, String methodName) {
if (Constans.CONNECT_METHOD_NAME.equalsIgnoreCase(methodName)) {
//代理建立成功
//HTTP代理建立連接
HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, Constans.CONNECT_SUCCESS);
ctx.writeAndFlush(response);
return true;
}
return false;
}
@Override
public void sendToServer(ClientRequest clientRequest, ChannelHandlerContext ctx, Object msg) {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(ctx.channel().eventLoop())
// 註冊線程池
.channel(ctx.channel().getClass())
// 使用NioSocketChannel來作爲連接用的channel類
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
//添加接收遠程server的handler
ch.pipeline().addLast(new HttpRequestEncoder());
ch.pipeline().addLast(new HttpResponseDecoder());
ch.pipeline().addLast(new HttpObjectAggregator(6553600));
//代理handler,負責給客戶端響應結果
ch.pipeline().addLast(new HttpProxyResponseHandler(ctx.channel()));
}
});
//連接遠程server
ChannelFuture cf = bootstrap.connect(clientRequest.getHost(), clientRequest.getPort());
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
//連接成功
future.channel().writeAndFlush(msg);
logger.debug("[operationComplete] connect remote server success!");
} else {
//連接失敗
logger.error("[operationComplete] 連接遠程server失敗了");
ctx.channel().close();
}
}
});
}
@Override
public void sendToClient(ClientRequest clientRequest, ChannelHandlerContext ctx, Object msg) {
}
}
上面的代碼爲轉發部分的處理代碼,其具體完整實現可以查看文末的github地址
對於http請求響應的處理代碼爲:
public class HttpProxyResponseHandler extends ChannelInboundHandlerAdapter {
private Logger logger = LoggerFactory.getLogger(HttpProxyResponseHandler.class);
private Channel clientChannel;
public HttpProxyResponseHandler(Channel clientChannel) {
this.clientChannel = clientChannel;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof FullHttpResponse) {
FullHttpResponse response = (FullHttpResponse) msg;
logger.debug("[channelRead][FullHttpResponse] 接收到遠程的數據1 content:{}", response.content().toString(Charset.defaultCharset()));
} else if (msg instanceof DefaultHttpResponse) {
DefaultHttpResponse response = (DefaultHttpResponse) msg;
logger.debug("[channelRead][FullHttpResponse] 接收到遠程的數據 content:{}", response.toString());
} else if (msg instanceof DefaultHttpContent) {
DefaultHttpContent httpContent = (DefaultHttpContent) msg;
logger.debug("[channelRead][DefaultHttpContent] 接收到遠程的數據 content:{}", httpContent.content().toString(Charset.defaultCharset()));
} else {
logger.debug("[channelRead] 接收到遠程的數據 " + msg.toString());
}
//發送給客戶端
clientChannel.writeAndFlush(msg);
}
}
https請求攔截
https的請求相對於http的請求流程稍微複雜一點,目前的瀏覽器主要採用tls1.2版本和tls1.3版本,在開發https的代理之前,先看一下https採用tls1.2的握手過程是怎麼樣的
https tls1.2無代理
其過程可以通過wireshark抓包進行分析
通過tls and ip.addr=[目錄ip]對https通信過程中的數據進行過濾
以下爲我分析的https中使用的tls1.2版本客戶端與服務端的握手過程簡要分析,其中參數了一些大牛的文章
- - TLS/SSL 協議詳解 (16) client key exchange(https://blog.csdn.net/mrpre/article/details/77868396)
- - TLS/SSL 協議詳解(12) server key exchange(https://blog.csdn.net/mrpre/article/details/77867831)
- - TLS/SSL 協議詳解 (30) SSL中的RSA、DHE、ECDHE、ECDH流程與區別(https://blog.csdn.net/mrpre/article/details/78025940)
- - Https:TLS 握手協議(https://www.jianshu.com/p/3d7046932040)
- - HTTPS協議詳解(四):TLS/SSL握手過程(https://www.cnblogs.com/huanxiyun/articles/6554085.html)
這其中的知識點比較多,如感興趣可以仔細看下上面我在梳理過程中所畫的時序圖,如想深入研究可以進入上方的鏈接進行深入學習。
我的理解後,其主要就是這樣的:CA保證了通信雙方的身份的真實性,基於公私鑰交換確保了通信過程中的安全性
對https請求進行代理分析
回到本文主題,那麼想要對https請求進行代理應該如何實現呢?
在瞭解了https的通信過程後,那麼我們有兩種辦法可以對https的請求進行代理:
- 獲取到所要代理網站https證書頒發機構的私鑰,也就是ca根證書的私鑰,然後自己再重新頒發一個新的證書返回給被代理的客戶端
- 自己生成一個ca證書,然後導入到將要被代理的客戶端中,讓其信任,隨後再針對將要代理的請求動態生成https證書
通過分析後我們可以知道,想要獲取到ca根證書的私鑰是不太可能的,據說ca根證書都是離線存儲的,一般人拿不到的(一個https證書一年收費上千塊不是開玩笑的),ca的代理機構的證書也是這個道理。
那麼通過上面的再次分析後通過方案1來進行請求代理的可行性還高一些,其代理過程可以簡單如下圖:
在分析過後並自己畫一個流程圖後對於https的代理實現流程清晰多了,其實目前市面上的許多支持https的代理軟件都是採用的這種方式來實現的,無論是常見的抓包利器fidder還是大名鼎鼎的安全測試工具BurpSuite都是基於此種方式來做的實現
https代理基於netty的實現
在有了上面的分析後,其實想要自己去實現一個https的代理服務器還是有一定難度的,https握手的細節實現就足以讓人費事費力了。但在同樣大名鼎鼎的netty框架面前這些都是小事兒!netty中的SslContext類幫我們完成了這些細節的實現,我們只管如何調用它遍可完成對https的握手了,框架就是框架,強大哇!
由於時間關係,對於其實現的具體代碼這裏不做詳細分析了,我已把代碼提交到github上了.
開源項目easyHttpProxy
其具體的實現可以參考源碼:https://github.com/puhaiyang/easyHttpProxy
爲了使用的方便,我也將此項目上傳到了maven公網,其maven爲:
<dependency>
<groupId>com.github.puhaiyang</groupId>
<artifactId>easy-http-proxy</artifactId>
<version>0.0.1</version>
</dependency>
使用時添加依賴包後,調用
EasyHttpProxyServer.getInstace().start(6667);
即可,其中6667爲代理服務器監聽的端口號,目前已支持了http/https並針對其他請求直接進行轉發.
如果不想自己生成證書,記得將jar包中的ca.crt、ca.key、ca_private.der拷貝的項目的運行根目錄下,即classes path下,要不然https代理時會找不到ca根證書會出錯。
同時,記得將ca.crt導入到根證書
具體步驟可見此截圖:
歡迎留言評論,共同學習共同進步!