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,當前應用中聲明的接口不會被代理。

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

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