Springboot版动态代理网关实现

背景

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

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中。

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
是的,Spring Boot可以很方便地实现网关代理。通常情况下,网关作为应用程序的入口点,可以在应用程序中处理一些共性的问题,如认证、授权、日志记录、请求转发、负载均衡等,从而简化了应用程序的开发和维护工作。 Spring Boot提供了一个名为Spring Cloud Gateway的子项目,可以轻松地实现网关代理。Spring Cloud Gateway基于异步非阻塞的Netty服务器实现,具有高性能和低资源消耗的优点。它支持各种路由规则、过滤器链和限流等功能,可以灵活地配置和扩展。 以下是一个简单的示例,演示如何使用Spring Cloud Gateway实现网关代理: 1. 添加Spring Cloud Gateway依赖 在Maven或Gradle中添加以下依赖: ```xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> ``` 2. 配置路由规则 在应用程序的配置文件中定义路由规则,例如: ```yaml spring: cloud: gateway: routes: - id: my-service uri: http://localhost:8081 predicates: - Path=/my-service/** ``` 上述配置表示将所有以“/my-service”开头的请求转发到“http://localhost:8081”地址。 3. 配置过滤器链 在需要时,可以添加多个过滤器,例如: ```yaml spring: cloud: gateway: routes: - id: my-service uri: http://localhost:8081 predicates: - Path=/my-service/** filters: - AddRequestHeader=X-Request-Foo, Bar - AddResponseHeader=X-Response-Baz, Qux ``` 上述配置表示在请求转发到目标地址之前,添加一个名为“X-Request-Foo”的请求头,并设置其值为“Bar”,在请求返回时,添加一个名为“X-Response-Baz”的响应头,并设置其值为“Qux”。 4. 启动应用程序 完成上述配置后,启动应用程序即可。此时,所有以“/my-service”开头的请求将被转发到“http://localhost:8081”地址,并经过过滤器链处理。 以上是一个简单的示例,实际使用中可能需要根据具体需求进行更复杂的配置和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值