記錄下java web場景下幾個實現網關代理的庫。
爲什麼要網關
- 微服務下最基礎的部分,唯一入口,用於代理、認證、限流等等
- 即便還沒做成微服務的系統,如果涉及多個應用需要用同一個登錄才能訪問,這時候也需要一個最外層的代理來做;
計算機世界裏沒有加一層解決不了的事。
下面是幾個探索的方案。
基於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秒太短了
- 過濾器,這裏是其自己實現的,不能和攔截器整合,查看其過濾器實現
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,當前應用中聲明的接口不會被代理。
所以,代理可以和權限部分一起存在。