前端请求进入内网后,内网服务需要访问外部服务,安全策略要求内网服务不能直接访问外网,所以决定在网关部署nginx,并配置为正向代理,内网服务配置nginx为正向代理。内网服务既有基于spring-boot的微服务,也有传统的Tomcat应用。所以分别进行测试验证。具体过程如下。

    一、对spring-boot应用进行验证

    参考网上使用正向代理方法,优先考虑对代码无侵扰的两个方法。一个是在java命令行增加-Dhttp.proxyHost=代理ip -Dhttp.proxyPort=代理端口;一个是操作系统级别设置环境变量http_proxy=代理ip:代理端口,并且在java命令行增加-Djava.net.userSystemProxies,方法2无效。验证结果,方法一可行,方法二无效。

    二、对Tomcat应用进行验证

    首先在catalina.sh中的JAVA_OPTS环境变量中增加-Dhttp.proxyHost=代理ip -Dhttp.proxyPort=代理端口,发现并未生效,又增加了-Dhttp.proxySet=true,正向代理仍未生效,此路不通。然后,设置环境变量http_proxy,再在catalina.sh文件中的JAVA_OPTS增加-Djava.net.userSystemProxies,测试一下,仍然无效。看来只能侵入代码了,所以在应用启动过程中使用@PostConstruct注解在代码中设置:

        System.setProperty("http.proxyHost", "代理ip");

        System.setProperty("http.proxyPort", "代理端口");

        满怀期待的运行,结果仍是竹篮打水。

        不过根据前面的问题能看出,虚拟机启动时设置的代理IP和端口属性并未对Tomcat中部署的应用的外部请求产生影响,再考虑到应用中请求外部HTTP服务使用的是apche的HttpClient,猜想HttpClient有自己的代理处理机制。赶紧下载httpcomponents-client和httpcomponents-core两个包的源码并进行研究,发现确实如此。而在Srping-boot编写的应用,使用RestTemplate进行外部HTTP调用时,能够使用通过JAVA_OPTS设置的代理,分析应该是底层直接使用了JDK的HTTP相关类库。

    三、解决办法

        直接修改代码,具体片段如下:

        CloseableHttpClient client = null;
        HttpGet httpGet = new HttpGet(url);

        HttpHost proxy = new HttpHost("10.111.2.24", 80);
        RequestConfig defaultRequestConfig = RequestConfig.custom()
.setConnectTimeout(60000).setSocketTimeout(60000).setProxy(proxy).build();
        client = HttpClients.custom().setDefaultRequestConfig(defaultRequestConfig).build();

       打包部署后,测试。通过抓包工具查看数据包,果然在代理端收到了Tomcat服务发来的数据。

     四、结论

     在Tomcat中使用正向代理时,如果使用HttpClient则必须修改代码,或者直接使用JDK自带的net包中的类自己封装。否则只能升级到spring-boot了,^_^。
     五、参考代码

    以下是设置代理后,HttpClient包中发送Http请求时获取代理并发送请求的代码:

        在HttpClientBuilder的builder方法中,以下代码设置HTTP请求的路由
        HttpRoutePlanner routePlannerCopy = this.routePlanner;
        if (routePlannerCopy == null) {
            SchemePortResolver schemePortResolverCopy = this.schemePortResolver;
            if (schemePortResolverCopy == null) {
                //先设置缺省的端口解析器,如果HTTP请求中是IP则解析端口。如果是域名,对于http请求域名设置为80,否则为443
                schemePortResolverCopy = DefaultSchemePortResolver.INSTANCE;
            }
            //代码中通过RequestConfig defaultRequestConfig = RequestConfig.custom()
            //.setConnectTimeout(60000).setSocketTimeout(60000).setProxy(proxy).build();
            //设置了proxy,所以进入第一个分支
            if (proxy != null) {
                //在路由策略中设置代理                
                routePlannerCopy = new DefaultProxyRoutePlanner(proxy, schemePortResolverCopy);
            } else if (systemProperties) {//该属性缺省为false
                routePlannerCopy = nDefaultProxyRoutePlannerew SystemDefaultRoutePlanner(
                        schemePortResolverCopy, ProxySelector.getDefault());
            } else {
                routePlannerCopy = new DefaultRoutePlanner(schemePortResolverCopy);
            }
        }

       在DefaultProxyRoutePlanner(在父类DefaultRoutePlanner)中有如下方法确定路由:
       @Override
       public HttpRoute determineRoute(
            final HttpHost host,
            final HttpRequest request,
            final HttpContext context) throws HttpException {
 
            HttpHost proxy = config.getProxy();
            ……
            if (proxy == null) {
                return new HttpRoute(target, local, secure);
            } else {//走如下路径
                return new HttpRoute(target, local, proxy, secure);
            }
       }
       在具体的HttpClient实现类InternalHttpClient类(继承自CloseableHttpClient)的doExecute方法中:
       protected CloseableHttpResponse doExecute(
            final HttpHost target//目标地址,
            final HttpRequest request//HttpGet或者HttpPost,
            final HttpContext context//zbl:为空) throws IOException, ClientProtocolException {
            //获取路由,调用上面的方法获取到到代理的路由
            final HttpRoute route = determineRoute(target, wrapper, localcontext);
            return this.execChain.execute(route, wrapper, localcontext, execAware);
        } catch (final HttpException httpException) {
            throw new ClientProtocolException(httpException);
        }
    }

    通过看代码,发现通过设置HttpClientBuilder的systemProperties属性,使用Java虚拟机配置的代理(就是前面的-D配置的IP和端口)。

    HttpClients.custom().useSystemProperties().setDefaultRequestConfig(defaultRequestConfig).build();

    修改代码,打包运行,结果不出所料。

    补充https正向代理:

    针对spring-boot,使用-Dhttp.proxyHost=代理ip -Dhttp.proxyPort=代理端口,不能代理https请求到指定的代理服务器;使用-Dhttps.proxyHost=代理ip -Dhttps.proxyPort=代理端口,请求被代理到了nginx但却返回400 Bad Request,经查本质原因是nginx缺省安装不支持HTTPS代理,所以需要在nginx中增加https模块并重新编译。

    重新编译并安装nginx后,再次验证,使用-Dhttps.proxyHost=代理ip -Dhttps.proxyPort=代理端口,请求被代理到nginx。也可以直接在代码中直接设置:

RestTemplate restTemplate = new RestTemplate(new SimpleClientHttpRequestFactory() {{
                setProxy(new java.net.Proxy(java.net.Proxy.Type.HTTP, new InetSocketAddress(proxyIp, proxyPort)));。然后就可以正常进行请求。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐