[http-proxy-servlet] 支持双向认证的流量转发的 Java 组件

Http-Proxy-Servlet

Http-Proxy-Servlet 能提供流量转发功能,并且支持 HTTPS 双向认证。它可以很方便的整合到你的 SpringBoot 程序中,甚至你可以在此基础上搭建一个"网关"程序。

快速上手

引入依赖:

<dependency>
    <groupId>com.github.LinYuanBaoBao</groupId>
    <artifactId>http-proxy-servlet</artifactId>
    <version>1.0.2-RELEASE</version>
</dependency>

在 SpringBoot 中使用

本示例你可以从 http-proxy-servlet-example-http 中获取

在 SpringBoot 中使用 Http-Proxy-Servlet 非常的简单,通过 SpringMVC 提供的 ServletWrappingController 将 Servlet 包装成一个 Controller,并通过 SimpleUrlHandlerMapping 将 Controller 与 /proxy 接口进行绑定。

@Configuration
public class ProxyServletConfiguration {

    private final String PROXY_CONTROLLER = "proxyController";
    private final String PROXY_API = "/proxy";

    @Bean
    public ServletWrappingController proxyController() throws Exception {
        // Servlet 配置
        Properties properties = new Properties();
        properties.put(ProxyServletConfig.P_CONTEXT, PROXY_API);
        properties.put(ProxyServletConfig.P_LOG, true);
        properties.put(ProxyServletConfig.P_READ_TIMEOUT, -1);
        properties.put(ProxyServletConfig.P_CONNECTION_REQUEST_TIMEOUT, -1);
        properties.put(ProxyServletConfig.P_CONNECT_TIMEOUT, -1);

        ServletWrappingController controller = new ServletWrappingController();
        controller.setServletClass(SpringProxyServlet.class);
        controller.setBeanName(PROXY_CONTROLLER);
        controller.setInitParameters(properties);
        controller.afterPropertiesSet();
        return controller;
    }

    @Bean
    public SimpleUrlHandlerMapping proxyControllerMapping() {
        SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
        Properties urlProperties = new Properties();
        urlProperties.put(PROXY_API + "/**", PROXY_CONTROLLER);
        mapping.setMappings(urlProperties);
        mapping.setOrder(Integer.MAX_VALUE - 2);
        return mapping;
    }

    public static class SpringProxyServlet extends ProxyServlet {
        @Override
        public void initHandler() {
            addHandler(new HttpClientProxyInitHandler());
            addHandler(new ProxyHandler());
            addHandler(new ResponseHandler());
        }
    }

}

因为 ProxyServlet 是个抽象类,需要实现 initHandler() 方法初始化处理器,上面代码中添加了三个 Handler 用于支持基本的 HTTP 流量转发功能:

  • HttpClientProxyInitHandler:前置处理器,用于初始化 HttpClientProxy 代理对象
  • ProxyHandler:代理处理器,用于执行代理请求
  • ResponseHandler:后置处理器,用于将代理请求结果响应回客户端

你也可以直接使用 DefaultProxyServlet,其同样初始化了这三个处理器。

添加 TargetHandler 处理器

单单如此还不足以实现流量转发功能,你还需要自定义一个前置处理器,用于告诉 ProxyServlet 你准备将把请求代理至何处。

这需要你自定义一个 TargetHandler 并在业务逻辑中获取待目标的主机地址,如下面代码:

@Component
public class TargetHandler implements Handler {

    @Override
    public void doHandle(RequestContext requestContext) {
        // 将请求代理至 localhost:8081 
        TargetHost targetHost = new TargetHost("localhost",8081);
        requestContext.setTargetHost(targetHost);
    }

    @Override
    public HandlerType getType() {
        return HandlerType.PRE_PROXY;
    }

    @Override
    public Integer getOrder() {
        return Integer.MIN_VALUE;
    }

}

Handler 类型有三种:

  • PRE_PROXY:前置处理器,它将会在请求代理前运行
  • PROXY:请求代理处理器
  • POST_PROXY:前置处理器,它将会在请求代理后运行

getOrder() 方法返回值将会决定 Handler 在处理链中的执行顺序,其值越小越先执行,值相同则会根据添加的先后顺序。

  • 对于需要最先执行的 Handler 通常会使用 Integer.MIN_VALUE 值,如用于初始化 Proxy 代理对象的处理器。
  • 对于需要最后执行的 Handler 通常会使用 Integer.MAX_VALUE 值,如用于将代理请求结果响应会客户端的处理器。

修改 initHandler() 方法,将 TargetHandler 添加至处理器链,如下:

public static class SpringProxyServlet extends ProxyServlet {
    @Override
    public void initHandler() {
        addHandler(new TargetHandler());
        addHandler(new HttpClientProxyInitHandler());
        addHandler(new ProxyHandler());
        addHandler(new ResponseHandler());
    }
}

测试

此时运行程序,假设访问代理服务 http://localhost:8080/proxy/v1/applications 的请求将会被代理至 http://localhost:8081/v1/applications

支持 HTTPS 双向认证

本示例你可以从 http-proxy-servlet-example-https 中获取

若代理的目标服务支持 Https,并且目标服务还需验证请求客户端的身份,那么你可以参考下面示例实现 Https 双向认证。

首先对先前的 TargetHandler 做一些修改:

public class TargetHandler implements Handler {

    @Override
    public void doHandle(RequestContext requestContext) {
        TargetHost targetHost = new TargetHost("localhost", 8082);
        requestContext.setTargetHost(targetHost);
        requestContext.setScheme(Scheme.HTTPS);
    }

    @Override
    public HandlerType getType() {
        return HandlerType.PRE_PROXY;
    }

    @Override
    public Integer getOrder() {
        return Integer.MIN_VALUE;
    }

}

我们需要将请求 Scheme 类型设置为 HTTPS。

其次确保 getOrder() 返回值为 Integer.MIN_VALUE,HttpsClientProxyInitHandler 会根据 Scheme 决定是否创建 HTTPS 连接,因此需要 TargetHandler 先与 HttpsClientProxyInitHandler 执行。

修改 initHandler() 方法,将 HttpClientProxyInitHandler 替换为 HttpsClientProxyInitHandler

public static class SpringProxyServlet extends ProxyServlet {
  @Override
  public void initHandler() {

    addHandler(new TargetHandler());

    // HTTPS 双向认证
    KeyStoreLoader keyStoreLoader = new KeyStoreLoader() {
      @Override
      public KeyStoreModel loadKeyStoreModel(RequestContext requestContext) {
        try {
          KeyStore keyStore = KeyStore.getInstance("PKCS12");
          File certificateFile = ResourceUtils.getFile("classpath:client.p12");
          KeyStore.PasswordProtection password = new KeyStore.PasswordProtection("123456".toCharArray());
          keyStore.load(FileUtils.openInputStream(certificateFile), password.getPassword());
          return new KeyStoreModel(keyStore, password);
        } catch (Exception e) {
          e.printStackTrace();
        }
        return null;
      }
    };
    DefaultSSLContextInitializer sslContextInitializer = new DefaultSSLContextInitializer(keyStoreLoader);
    DefaultHttpsConnectionSocketFactoryRegister httpsConnectionSocketFactoryRegister = new DefaultHttpsConnectionSocketFactoryRegister(sslContextInitializer);
    addHandler(new HttpsClientProxyInitHandler(httpsConnectionSocketFactoryRegister));

    addHandler(new ProxyHandler());
    addHandler(new ResponseHandler());
  }
}

HttpsClientProxyInitHandler 构造函数需要一个 HttpsConnectionSocketFactoryRegister 接口实现,用于创建 HTTPS-Socket 连接工厂,这里使用 DefaultHttpsConnectionSocketFactoryRegister,需要注意的是它暂未对 hostname 进行验证,你可以自己实现验证逻辑,并调用 setHostnameVerifier() 方法配置它。

DefaultHttpsConnectionSocketFactoryRegister 构造函数需要一个 SSLContextInitializer 接口实现,用于初始化 SSLContext 上下文对象,这里使用的 DefaultSSLContextInitializer 它会跳过对服务端的证书校验,你可以继承该类重写 loadTrustManager() 方法实现校验逻辑。

此外 DefaultSSLContextInitializer 并不清楚你客户端的证书保存于何处,因为它可能跟你的业务逻辑有关,因此你需要实现 KeyStoreLoader 接口获取 KeyStore

上面代码中,我本地测试用的客户端证书位于项目资源目录下,并名为 client.p12,其密码为 123456,证书类型为 PKCS12。

测试

此时如果代理的目标服务端已信任 client.p12 证书,则请求将会成功。

你可以参考 此文章 搭建一个双向认证服务端尝试一下。

自定义 Proxy 代理对象

真正转发流量的并非是 Handler 而是 Proxy,由 Proxy 去完成实际代理并将结果响应回调用方。目前仅提供了基于 HttpClient 实现的 HttpClientProxy

如果有需要自定义你可以继承自 AbstractProxy 并实现以下三个方法:

  • proxy():执行代理请求
  • response():将代理请求结果响应回调用方
  • destroy():销毁代理对象资源

也可以继承 HttpClientProxy 并在原有的基础上做修改,如修改响应结果,可以重写 setResponseEntity() 方法。

扩展参考

通过跳板机将请求代理至内网主机

如果你需要将请求代理至内网主机,你可以利用跳板机的端口转发功能,这里推荐使用 Jcraft,你的代码可能会是这个样子:

private Integer buildTunnel(Session jumpSession, String targetIp, Integer targetPort) throws JSchException {
  return jumpSession.setPortForwardingL(0, targetIp, targetPort);
}

作者信息

聪明的杰瑞 - 掘金博客、邮箱:765371578@qq.com

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345