记录下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,当前应用中声明的接口不会被代理。
所以,代理可以和权限部分一起存在。