通过 Handler 转发请求

在一些情况下,我们可能需要查看客户端的请求转发到另个一个服端,包括请求头、请求体等。可以参考本文章实现

打印请求信息

拦截所有请求

配置一个 HttpRequestHanlderConfig 拦截所有请求

import com.litongjava.jfinal.aop.annotation.AConfiguration;
import com.litongjava.jfinal.aop.annotation.AInitialization;
import com.litongjava.tio.boot.server.TioBootServer;
import com.litongjava.tio.http.server.router.RequestRoute;


@AConfiguration
public class HttpRequestHanlderConfig {

  @AInitialization
  public void config() {
    // 获取router
    RequestRoute r = TioBootServer.me().getRequestRoute();

    // 创建handler
    IndexRequestHandler indexRequestHandler = new IndexRequestHandler();

    // 添加action
    r.add("/*", indexRequestHandler::index);
  }
}

示例代码

import java.util.Map;

import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpMethod;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.http.common.RequestLine;

public class IndexRequestHandler {

  public HttpResponse index(HttpRequest request) {
    StringBuffer printRequest = getRequest(request);
    return TioRequestContext.getResponse().setString(printRequest.toString());
  }

  public static StringBuffer getRequest(HttpRequest httpRequest) {
    RequestLine requestLine = httpRequest.getRequestLine();
    HttpMethod requestMethod = requestLine.getMethod();

    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append(requestLine.toString()).append("\r\n");

    // 打印请求头信息
    Map<String, String> requestHeaders = httpRequest.getHeaders();
    for (Map.Entry<String, String> e : requestHeaders.entrySet()) {
      stringBuffer.append(e.getKey()).append(":").append(e.getValue()).append("\r\n");
    }

    stringBuffer.append("\r\n");
    // 设置请求体
    if (HttpMethod.POST.equals(requestMethod)) {
      String contentType = httpRequest.getContentType();
      if (contentType != null) {
        if (contentType.startsWith("application/json")) {
          stringBuffer.append(httpRequest.getBodyString());

        } else if (contentType.startsWith("application/x-www-form-urlencoded")) {
          Map<String, Object[]> params = httpRequest.getParams();
          for (Map.Entry<String, Object[]> e : params.entrySet()) {
            // 添加参数
            stringBuffer.append(e.getKey()).append(":").append(e.getValue()[0]);
          }

        } else if (contentType.startsWith("multipart/form-data")) {
          Map<String, Object[]> params = httpRequest.getParams();
          for (Map.Entry<String, Object[]> e : params.entrySet()) {
            Object value = e.getValue()[0];
            // 添加参数
            if (value instanceof String) {
              stringBuffer.append(e.getKey()).append(":").append(e.getValue()[0]).append("\r\n");
            } else {
              stringBuffer.append(e.getKey()).append(":").append("binary").append("\r\n");
            }
          }
        } else {
          stringBuffer.append(httpRequest.getBodyString());
        }
      } else {
        stringBuffer.append(httpRequest.getBodyString());
      }
    } else if (HttpMethod.PUT.equals(requestMethod)) {
      stringBuffer.append(httpRequest.getBodyString());
    }

    return stringBuffer;
  }
}

输出信息示例

示例 1:JSON 格式的请求体

请求信息:

POST /hi?text=how%20are%20you? HTTP/1.1
content-length:23
host:localhost
content-type:application/json
connection:keep-alive
accept-encoding:gzip, deflate, br
accept:*/*
{
    "key":"value"
}

示例 2:表单格式的请求体

请求信息:

POST /hi?text=how%20are%20you? HTTP/1.1
content-length:9
host:localhost
connection:keep-alive
content-type:application/x-www-form-urlencoded
accept-encoding:gzip, deflate, br
accept:*/*
text:how are you?
key:value

通过上述代码和示例,我们可以轻松地在控制台输出客户端请求的详细信息,便于调试和分析。

代理请求

使用 okHttp 将请求转发到远程系统

示例代码

import java.io.IOException;
import java.util.Map;

import com.litongjava.open.chat.constants.OpenAiConstatns;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.http.common.RequestLine;
import com.litongjava.tio.http.server.util.HttpServerRequestUtils;
import com.litongjava.tio.http.server.util.HttpServerResponseUtils;
import com.litongjava.tio.utils.environment.EnvUtils;
import com.litongjava.tio.utils.http.OkHttpClientPool;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

//@Slf4j
public class IndexRequestHandler {

  public HttpResponse index(HttpRequest httpRequest) {
    HttpResponse httpResponse = TioRequestContext.getResponse();
    printRequest(httpRequest);
    // 修改授权信息
    String authorization = EnvUtils.get("OPENAI_API_KEY");
    httpRequest.addHeader("authorization", "Bearer "+authorization);

    // response.setBody(requestString.toString().getBytes());
    Request okHttpReqeust = HttpServerRequestUtils.toOkHttp(OpenAiConstatns.server_url, httpRequest);
    OkHttpClient httpClient = OkHttpClientPool.getHttpClient();

    try (Response okHttpResponse = httpClient.newCall(okHttpReqeust).execute()) {

      HttpServerResponseUtils.fromOkHttp(okHttpResponse, httpResponse);
      printResponse(okHttpResponse);

      httpResponse.setHasGzipped(true);
	  // httpResponse.removeHeaders("Set-Cookie");
      httpResponse.removeHeaders("Transfer-Encoding");
      httpResponse.removeHeaders("Server");
      httpResponse.removeHeaders("Date");
      httpResponse.setHeader("Connection", "close");

    } catch (IOException e) {
      e.printStackTrace();
    }

    return httpResponse;
  }

  private void printResponse(Response okHttpResponse) {
    if (okHttpResponse.isSuccessful()) {
      try {
        System.out.println("response:\n" + okHttpResponse.body().string());
      } catch (IOException e) {
        e.printStackTrace();
      }
    }

  }

  private StringBuffer printRequest(HttpRequest httpRequest) {
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append("request:\n");
    RequestLine requestLine = httpRequest.getRequestLine();
    stringBuffer.append(requestLine.toString()).append("\n");
    Map<String, String> headers = httpRequest.getHeaders();
    for (Map.Entry<String, String> e : headers.entrySet()) {
      stringBuffer.append(e.getKey() + ":" + e.getValue()).append("\n");
    }

    // 请求体
    String contentType = httpRequest.getContentType();
    if (contentType != null) {
      if (contentType.startsWith("application/json")) {
        stringBuffer.append(httpRequest.getBodyString());

      } else if (contentType.startsWith("application/x-www-form-urlencoded")) {
        Map<String, Object[]> params = httpRequest.getParams();
        for (Map.Entry<String, Object[]> e : params.entrySet()) {
          stringBuffer.append(e.getKey() + ": " + e.getValue()[0]).append("\n");
        }

      } else if (contentType.startsWith("multipart/form-data")) {
        Map<String, Object[]> params = httpRequest.getParams();
        for (Map.Entry<String, Object[]> e : params.entrySet()) {
          Object value = e.getValue()[0];
          // 添加参数
          if (value instanceof String) {
            stringBuffer.append(e.getKey()).append(":").append(e.getValue()[0]).append("\n");
          } else {
            stringBuffer.append(e.getKey()).append(":").append("binary \n");
          }
        }
      }
    }

    System.out.println(stringBuffer.toString());
    return stringBuffer;
  }
}

转发请求并记录到数据库

创建一张数据表

CREATE TABLE sys_http_forward_statistics (
  id BIGINT NOT NULL,
  ip VARCHAR,
  ip_region VARCHAR,
  method VARCHAR(10),
  uri VARCHAR(256),
  elapsed BIGINT,
  request_header json,
  request_body text,
  response_status int,
  response_body text,
  remark VARCHAR (256),
  creator VARCHAR (64) DEFAULT '',
  create_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updater VARCHAR (64) DEFAULT '',
  update_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted SMALLINT NOT NULL DEFAULT 0,
  tenant_id BIGINT NOT NULL DEFAULT 0
);

这张表 sys_http_forward_statistics 用于记录 HTTP 请求的相关数据,包括请求 IP 地址、请求头、请求体、响应状态码、响应体、耗时等。create_timeupdate_time 字段记录了数据的创建和更新时间,deleted 用于软删除,tenant_id 用于多租户支持。

添加一个 Handler,拦截所有请求

package com.litongjava.maxkb.config;

import com.litongjava.jfinal.aop.annotation.AConfiguration;
import com.litongjava.jfinal.aop.annotation.AInitialization;
import com.litongjava.maxkb.httphandler.AppRequestForwardHandler;
import com.litongjava.tio.boot.server.TioBootServer;
import com.litongjava.tio.http.server.router.RequestRoute;

@AConfiguration
public class HttpRequestHanlderConfig {

  @AInitialization
  public void config() {
    // 获取路由
    RequestRoute r = TioBootServer.me().getRequestRoute();

    // 创建处理器
    AppRequestForwardHandler indexRequestHandler = new AppRequestForwardHandler();

    // 添加处理动作,拦截所有请求
    r.add("/*", indexRequestHandler::index);
  }
}

HttpRequestHanlderConfig 类通过 TioBootServer 获取路由,并为所有请求路径(/*)添加 AppRequestForwardHandler 进行拦截。这个配置类将拦截所有进入的 HTTP 请求,并交给自定义处理器处理。

AppRequestForwardHandler 中调用 TioHttpProxy

package com.litongjava.maxkb.httphandler;

import com.litongjava.jfinal.aop.Aop;
import com.litongjava.maxkb.service.AppForwardRequestService;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.boot.http.forward.TioHttpProxy;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;

public class AppRequestForwardHandler {

  private String remoteServerUrl = "http://192.168.1.2:7006";

  public HttpResponse index(HttpRequest httpRequest) {
    HttpResponse httpResponse = TioRequestContext.getResponse();
    AppForwardRequestService forwardRequestService = Aop.get(AppForwardRequestService.class);
    // 通过 TioHttpProxy 进行请求转发
    TioHttpProxy.reverseProxy(remoteServerUrl, httpRequest, httpResponse, true, forwardRequestService);
    return httpResponse;
  }
}

AppRequestForwardHandler 中,TioHttpProxy 被用于实现请求转发功能。reverseProxy 方法接收远程服务器地址、当前的 HttpRequestHttpResponse 对象,并调用服务类 AppForwardRequestService 记录请求和响应数据。

ForwardRequestService 负责解析数据并入库

import java.util.Map;

import com.litongjava.db.activerecord.Db;
import com.litongjava.db.activerecord.Record;
import com.litongjava.tio.boot.http.forward.RequestProxyCallback;
import com.litongjava.tio.http.common.HeaderName;
import com.litongjava.tio.http.common.HeaderValue;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.RequestLine;
import com.litongjava.tio.utils.hutool.ZipUtil;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ProxyRequestService implements RequestProxyCallback {

  public void saveRequest(long id, String ip, HttpRequest httpRequest) {
    StringBuffer stringBuffer = new StringBuffer();
    RequestLine requestLine = httpRequest.getRequestLine();

    stringBuffer.append(requestLine.toString()).append("\n");
    Map<String, String> headers = httpRequest.getHeaders();

    // 处理请求体
    String contentType = httpRequest.getContentType();
    if (contentType != null) {
      if (contentType.startsWith("application/json")) {
        stringBuffer.append(httpRequest.getBodyString());
      } else if (contentType.startsWith("application/x-www-form-urlencoded")) {
        Map<String, Object[]> params = httpRequest.getParams();
        if (params != null) {
          for (Map.Entry<String, Object[]> e : params.entrySet()) {
            stringBuffer.append(e.getKey() + ": " + e.getValue()[0]).append("\n");
          }
        }
      } else if (contentType.startsWith("application/from-data")) {
        Map<String, Object[]> params = httpRequest.getParams();
        for (Map.Entry<String, Object[]> e : params.entrySet()) {
          Object value = e.getValue()[0];
          if (value instanceof String) {
            stringBuffer.append(e.getKey()).append(":").append(e.getValue()[0]).append("\n");
          } else {
            stringBuffer.append(e.getKey()).append(":").append("binary \n");
          }
        }
      }
    }

    String method = requestLine.getMethod().toString();
    String path = requestLine.getPath();
    Record record = Record.by("id", id).set("ip", ip).set("ip_region", Ip2RegionUtils.searchIp(ip))
        .set("method", method).set("uri", path).set("request_header", headers).set("request_body", stringBuffer.toString());

    String[] jsonFields = { "request_header" };
    boolean saveResult = Db.save("sys_http_forward_statistics", record, jsonFields);

    if (!saveResult) {
      log.error("保存数据库失败:{}", "sys_http_forward_statistics");
    }
  }

  @Override
  public void saveResponse(long id, long elapsed, int statusCode, Map<HeaderName, HeaderValue> headers,
      HeaderValue contentEncoding, byte[] body) {
    Record record = Record.by("id", id).set("elapsed", elapsed).set("response_status", statusCode);

    if (body != null && body.length > 0) {
      if (contentEncoding != null && HeaderValue.Content_Encoding.gzip.equals(contentEncoding)) {
        String value = new String(ZipUtil.unGzip(body));
        log.info("响应内容:{},{}", id, value);
        record.set("response_body", value);
      } else {
        record.set("response_body", new String(body));
      }
    }

    String tableName = "sys_http_forward_statistics";
    try {
      boolean update = Db.update(tableName, record);
      if (!update) {
        log.error("更新表失败:{},{}", tableName, id);
      }
    } catch (Exception e) {
      log.error("更新表出错:{},{},{}", tableName, id, e.getMessage());
    }
  }
}

ProxyRequestService 实现了 RequestProxyCallback 接口,并处理转发请求的日志记录。saveRequest 方法负责保存请求数据,包括请求头和请求体。saveResponse 方法则负责保存响应数据,包括响应状态码和响应体。

总结

通过上述步骤,我们实现了一个基于 Tio-Boot 的 HTTP 请求转发与日志记录系统。该系统能够将所有经过的 HTTP 请求进行拦截,转发到指定的服务器,并将请求与响应的详细信息记录到数据库中,方便后续分析与调试。