背景

首先使用代理,希望接口结果直接是代理方的,不希望有封装;
其次使用一些云原生网关时,业务有时会希望干预一下代理流程,比如请求前做点通知、事后存点日志,这些云原生网关不好处理,还是得代码处理;
所以引出下文。

servlet方式(smiley-http-proxy-servlet)

特点

  • 基于Servlet进行服务代理,只需要进行相关的配置之后,就能进行服务代理
  • 有特殊要求要对代理服务进行改造,如下文需要进行动态的路由代理,增删路由不需要修改代码/重启服务

Spring Boot示例

这里以动态网关进行示例

Maven文件

<!-- https://mvnrepository.com/artifact/org.mitre.dsmiley.httpproxy/smiley-http-proxy-servlet -->
<dependency>
	<groupId>org.mitre.dsmiley.httpproxy</groupId>
	<artifactId>smiley-http-proxy-servlet</artifactId>
	<version>1.11</version>
</dependency>

自定义扩展(核心)

  • init方法:注入logbean及redistemplate
  • service方法:用于根据路径判断不同路由进入不同地址中
  • doExecute方法:进行pre操作或after操作
public class MyProxyServlet extends ProxyServlet {

    /**
     * redis连接实例
     */
    private  ValueOperations valueOperations = null;
    private LogDao sysQueryLogDao = null;

    /**
     * 临时定义代理地址
     */
    private final Map<String, String> urlMap = new HashMap<String, String>(){
        {
            put("hao123", "http://www.hao123.com");
            put("baidu", "http://www.baidu.com");
        }
    };

    @Override
    public void init() throws ServletException {
        super.init();
        sysQueryLogDao = SpringContextUtils.getBean("sysQueryLogDao", LogDao.class);
        valueOperations = SpringContextUtils.getBean("valueOperations", ValueOperations.class);
    }

    @Override
    protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws ServletException, IOException {
        // TODO 后续可更改成从redis/数据库取代理路径,实现动态反向代理
        
        // 初始切换路径
        String path = servletRequest.getPathInfo();
        String firstPath = getFirstPath(path);
        String url = urlMap.get(firstPath);
        if (StringUtils.isEmpty(url)) {
            throw new ServletException("代理路径不正确,请找管理员确认路径");
        }

        if (servletRequest.getAttribute(ATTR_TARGET_URI) == null) {
            servletRequest.setAttribute(ATTR_TARGET_URI, url);
        }

        if (servletRequest.getAttribute(ATTR_TARGET_HOST) == null) {
            URL trueUrl = URLUtil.url(url);
            servletRequest.setAttribute(ATTR_TARGET_HOST, new HttpHost(trueUrl.getHost(), trueUrl.getPort(), trueUrl.getProtocol()));
        }

        String method = servletRequest.getMethod();
        // 替换多余路径
        String proxyRequestUri = this.rewriteUrlFromRequest(servletRequest);
        proxyRequestUri = proxyRequestUri.replaceFirst("/"+firstPath, "");

        Object proxyRequest;
        if (servletRequest.getHeader("Content-Length") == null && servletRequest.getHeader("Transfer-Encoding") == null) {
            proxyRequest = new BasicHttpRequest(method, proxyRequestUri);
        } else {
            proxyRequest = this.newProxyRequestWithEntity(method, proxyRequestUri, servletRequest);
        }

        this.copyRequestHeaders(servletRequest, (HttpRequest)proxyRequest);
        setXForwardedForHeader(servletRequest, (HttpRequest)proxyRequest);
        HttpResponse proxyResponse = null;

        try {
            proxyResponse = this.doExecute(servletRequest, servletResponse, (HttpRequest)proxyRequest);
            int statusCode = proxyResponse.getStatusLine().getStatusCode();
            servletResponse.setStatus(statusCode, proxyResponse.getStatusLine().getReasonPhrase());
            this.copyResponseHeaders(proxyResponse, servletRequest, servletResponse);
            if (statusCode == 304) {
                servletResponse.setIntHeader("Content-Length", 0);
            } else {
                this.copyResponseEntity(proxyResponse, servletResponse, (HttpRequest)proxyRequest, servletRequest);
            }
        } catch (Exception var11) {
            this.handleRequestException((HttpRequest)proxyRequest, var11);
        } finally {
            if (proxyResponse != null) {
                EntityUtils.consumeQuietly(proxyResponse.getEntity());
            }

        }
    }

    @Override
    protected HttpResponse doExecute(HttpServletRequest servletRequest, HttpServletResponse servletResponse, HttpRequest proxyRequest) throws IOException {
        HttpResponse response = null;

        // 拦截校验
        String token = servletRequest.getHeader("ex_proxy_token");
        if (StringUtils.isEmpty(token)) {
            // TODO 检验token正确性
            response = new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_UNAUTHORIZED, ""));
            BasicHttpEntity basicHttpEntity = new BasicHttpEntity();
            basicHttpEntity.setContent(new ByteArrayInputStream("token校验失败,请检查ex_proxy_token".getBytes()));
            response.setEntity(basicHttpEntity);
        } else {
            // TODO 写入日志
            String curl = Request2CurlUtil.getCurl(servletRequest);
            SysQueryLogEntity log = new SysQueryLogEntity();
            sysQueryLogDao.insert(log);

            response = super.doExecute(servletRequest, servletResponse, proxyRequest);
        }

        return response;
    }

    /**
     * 父类私有方法复制
     * @param servletRequest
     * @param proxyRequest
     */
    private void setXForwardedForHeader(HttpServletRequest servletRequest, HttpRequest proxyRequest) {
        if (this.doForwardIP) {
            String forHeaderName = "X-Forwarded-For";
            String forHeader = servletRequest.getRemoteAddr();
            String existingForHeader = servletRequest.getHeader(forHeaderName);
            if (existingForHeader != null) {
                forHeader = existingForHeader + ", " + forHeader;
            }

            proxyRequest.setHeader(forHeaderName, forHeader);
            String protoHeaderName = "X-Forwarded-Proto";
            String protoHeader = servletRequest.getScheme();
            proxyRequest.setHeader(protoHeaderName, protoHeader);
        }
    }

    /**
     * 获取第一个路径
     * @param path 路径参数
     * @return 第一个路径
     */
    private String getFirstPath(String path) {
        path = path.substring(1, path.length());
        int index = path.indexOf("/");
        if (index > 0) {
            return path.substring(0, index);
        }
        return path;
    }
}

配置文件

这里由于必须要填写targetUri,而目标uri在service方法被修改过,所以这里乱填个null值就行了

@Configuration
public class ProxyServletConfiguration {

    private final static String SERVLET_URL = "/proxy/*";

    @Bean
    public ServletRegistrationBean proxyServletRegistration() {
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyProxyServlet(), SERVLET_URL);
        //设置网址以及参数
        Map<String, String> params = ImmutableMap.of("targetUri", "null", "log", "true");
        registrationBean.setInitParameters(params);
        return registrationBean;
    }
}

使用方式

启动服务后,调用localhost:8080/proxy/baidu,即会跳转到百度中;调用localhost:8080/proxy/hao123,即会跳转到hao123中。

Logo

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

更多推荐