spring应用集成网关代理

记录下java web场景下几个实现网关代理的库。

为什么要网关

  1. 微服务下最基础的部分,唯一入口,用于代理、认证、限流等等
  2. 即便还没做成微服务的系统,如果涉及多个应用需要用同一个登录才能访问,这时候也需要一个最外层的代理来做;

计算机世界里没有加一层解决不了的事。

下面是几个探索的方案。

基于Spring Cloud Gateway

如果基于spring cloud这一套,那么目前gateway一定是首选。
用法不多说,只谈一点:
由于gateway基于webflux实现,和 starter-web 模块不兼容, 由于项目中大多依赖了web模块,考虑到迁移的成本,所以才有了下面的方式。(如果是新的项目,网关最好一开始就提出来)

smiley-http-proxy-servlet

https://github.com/mitre/HTTP-Proxy-Servlet

该项目是一个轻量级实现,纯粹基于http代理。

加入依赖:

        <dependency>
            <groupId>org.mitre.dsmiley.httpproxy</groupId>
            <artifactId>smiley-http-proxy-servlet</artifactId>
            <version>1.11</version>
        </dependency>

由于其基于servlet,所以我们注册一个servlet bean:

import org.mitre.dsmiley.httpproxy.ProxyServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class ProxyServletConfiguration implements EnvironmentAware {

    @Bean
    public ServletRegistrationBean servletRegistrationBean() {
        ServletRegistrationBean servletRegistrationBean
                = new ServletRegistrationBean(new ProxyServlet(), "/api/*");
        Map<String, String> params = new HashMap<>(4);
        params.put("targetUri", "http://localhost:8083");
        params.put("log", "true");
        servletRegistrationBean.setInitParameters(params);
        return servletRegistrationBean;
    }
}

当urlMapping写成/api/*时,实际请求的是api后面的url。比如:

proxyServlet: proxy POST uri: /api/auth/external/login -- http://localhost:8083/auth/external/login

写成/*就可以从根路径代理。或者把targetUri写成http://localhost:8083/api.

假如要代理到多个服务,就不行了,你可以注册多个servlet bean:但是只会有第一个被采用


    @Bean
    public ServletRegistrationBean servletRegistrationBean2() {
        ServletRegistrationBean servletRegistrationBean
                = new ServletRegistrationBean(new ProxyServlet(), "/api/sdk/*");
        Map<String, String> params = new HashMap<>(4);
        params.put("targetUri", "http://localhost:8084");
        params.put("log", "true");
        servletRegistrationBean.setInitParameters(params);
        return servletRegistrationBean;
    }

    @Bean
    public ServletRegistrationBean servletRegistrationBean() {
        ServletRegistrationBean servletRegistrationBean
                = new ServletRegistrationBean(new ProxyServlet(), "/api/*");
        Map<String, String> params = new HashMap<>(4);
        params.put("targetUri", "http://localhost:8083");
        params.put("log", "true");
        servletRegistrationBean.setInitParameters(params);
        return servletRegistrationBean;
    }

charon-spring-boot-starter

charon-spring-boot-starter项目也是个开源项目,不是很火,但是功能挺齐全。

import com.github.mkopylec.charon.configuration.CharonConfigurer;
import com.github.mkopylec.charon.configuration.RequestMappingConfigurer;
import com.github.mkopylec.charon.forwarding.RestTemplateConfigurer;
import com.github.mkopylec.charon.forwarding.interceptors.rewrite.RequestServerNameRewriterConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;

import static com.github.mkopylec.charon.forwarding.TimeoutConfigurer.timeout;
import static com.jd.urbancomputing.just.app.gateway.interceptor.CustomInterceptorConfigurer.customInterceptorConfigurer;

/**
 * @author jimo
 * @version 1.0.0
 * @date 2020/4/29 9:10
 */
@Configuration
public class CharonConfiguration {

    @Bean
    CharonConfigurer charonConfigurer() {
        return CharonConfigurer.charonConfiguration()
                .set(customInterceptorConfigurer())
                .set(RestTemplateConfigurer.restTemplate()
                        .set(timeout().connection(Duration.ofSeconds(1))
                                .read(Duration.ofSeconds(100)).write(Duration.ofSeconds(100))))
                .add(RequestMappingConfigurer.requestMapping("api")
                        .set(RequestServerNameRewriterConfigurer
                                .requestServerNameRewriter()
                                .outgoingServers("localhost:8083")
                        )
                        .pathRegex("/api/auth/.*")
                ).add(RequestMappingConfigurer.requestMapping("sdk")
                        // .set(rateLimiter().configuration(custom().timeoutDuration(Duration.ofSeconds(100))))
                        .set(RequestServerNameRewriterConfigurer
                                .requestServerNameRewriter()
                                .outgoingServers("localhost:8084")
                        )
                        .pathRegex("/api/sdk/.*")
                );
    }
}

这里需要注意的2点:

  1. 超时时间:默认1秒太短了
  2. 过滤器,这里是其自己实现的,不能和拦截器整合,查看其过滤器实现

zuul

Zuul应该是普遍被采用的,在spring cloud把它抛弃之前就已经作为Netflix的开源项目广为人知。

实际上,我们不适用spring cloud时可以选择zuul,其用法也很简单:

加入依赖:注意这里的版本选择,要和springboot的版本匹配

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
            <version>2.x.x.RELEASE</version>
        </dependency>

然后,配置代理的url:

zuul:
  routes:
    app-1:
      path: /api/app1/**
      url: http://10.10.10.10:8080
      strip-prefix: false
    app-2:
      path: /api/app2/**
      url: http://10.10.10.11:8080
      strip-prefix: false

直接通过url代理,strip-prefix默认是true,为true时会把path部分给去掉,到达后续应用时就缺少前缀了。

然后声明过滤器:

@Slf4j
@Component
public class ZuulProxyAuthFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        final RequestContext ctx = RequestContext.getCurrentContext();
        final HttpServletRequest req = ctx.getRequest();
        log.info("访问URL:{}", req.getRequestURI());
        Object user = req.getSession().getAttribute("user");
        if (user == null) {
            throw new AuthenticationFailException("未登录");
        }
        final HttpServletResponse resp = ctx.getResponse();
        resp.setHeader("username", user.toString());
        return null;
    }
}

该过滤器只会过滤被代理的url,当前应用中声明的接口不会被代理。

所以,代理可以和权限部分一起存在。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章