Fork me on GitHub

Spring Cloud - 基于Zuul的http反向代理服务(2)

此处输入图片的描述
本文介绍如何利用 Spring Cloud 微服务架构中的Zuul 的跨域配置实现反向代理服务功能。

反向代理是代理服务器的一种。服务器根据客户端的请求,从其关联的一组或多组后端服务器(如Web服务器)上获取资源,然后再将这些资源返回给客户端,客户端只会得知反向代理的IP地址,而不知道在代理服务器后面的服务器簇的存在。这篇文章,主要介绍如何利用Zuul实现反向代理服务器中常见的“流量透传”、“请求转发”、“重定向”、“修改/获取服务器的Response”、“代理Https安全访问”等。

一、流量透传

用Zuul实现的反向代理功能,通过编辑 Spring Boot 配置文件application.yml就能很方便地实现流量透传。
可以通过在配置文件中配置url直接透传所有的代理流量:

1
2
3
4
5
6
7
8
zuul:
sensitiveHeaders: Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Origin,Access-Control-Max-Age,P3P
routes:
auth:
sensitiveHeaders: Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Origin,Access-Control-Max-Age,P3P
rootpath:
path: /**
url: http://www.baidu.com

注意:Zuul作为统一网关代理所有后台接口,对于代码发起的http请求自然无需在意跨域的问题,但是对于提供给前端访问的接口,由于浏览器的安全策略对于跨域的资源访问会被拦截,因此需要配置sensitiveHeaders来过滤掉相关headers

二、请求转发与重定向,获取getpost请求参数

对于需要转发的请求,除了在配置文件properties.yml中配置指定url之外,也可以配置过滤器来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Component
public class RewriteFilter extends ZuulFilter {
@Override
public String filterType() {
return "route";
}
@Override
public int filterOrder() {
return 6;
}
@Override
public boolean shouldFilter() {
return false;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if(ctx.getRequest().getRequestURL().toString().contains("rewrite")){
ctx.set("requestURI", "/static/css/default/portal/footer.css");
}
//替换js文件url
if(ctx.getRequest().getRequestURL().toString().contains("frontstyle.css")){
String uri = request.getRequestURI().toString();
String newUri = uri.replaceAll("frontstyle", "bootstrap.min");
System.out.println("-----------------------------------------------------------");
System.out.println(newUri);
ctx.set("requestURI", newUri);
}
//替换js文件url
if(ctx.getRequest().getRequestURL().toString().contains("layout")){
String uri = request.getRequestURI().toString();
String newUri = uri.replaceAll("layout", "footer");
System.out.println("-----------------------------------------------------------");
System.out.println(newUri);
ctx.set("requestURI", newUri);
}
return null;
}
}

注意,这里的过滤器类型为route
同样地,在过滤器中,可以很容易地用RequestContext对象获取get或者post参数。

三、重定向到本地文件

为了重定向客户端请求的资源到反向代理服务器的本地资源,需要以下步骤:

  • 开启静态资源的URL直接访问(以thymeleaf为例),引入相关依赖,并将需要访问的静态资源放在项目的resources/files目录下:

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
  • 反向代理请求拦截与重定向配置

    1
    2
    3
    static:
    path: /jquery-1.12.4.min.js
    url: http://localhost
  • 除了可以通过上述配置,还可以通过自定义一个配置类。此时的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    @Component
    public class LocalFileFilter extends ZuulFilter {
    private final String jsString1 = "/static/js/jquery/jquery-1.12.4.min.js";
    private final String newJsString1 = "/files/jquery-1.12.4.min.js";
    @Override
    public String filterType() {
    return ROUTE_TYPE;
    }
    @Override
    public int filterOrder() {
    // 99
    return SIMPLE_HOST_ROUTING_FILTER_ORDER -1;
    }
    @Override
    public boolean shouldFilter() {
    String uri = RequestContext.getCurrentContext().getRequest().getRequestURI();
    if(uri.contains(jsString1)){
    System.out.println(uri);
    return true;
    }
    return false;
    }
    @Override
    public Object run() throws ZuulException {
    RequestContext ctx = RequestContext.getCurrentContext();
    try {
    ctx.set("requestURI", newJsString1);
    ctx.setRouteHost(new URL("http://localhost"));
    } catch (MalformedURLException e) {
    e.printStackTrace();
    }
    return null;
    }
    }

用过滤器实现请求转发/重定向的时候,与配置文件的url无关,因为在过滤器中,setRouteHost()方法修改了请求的host。

1
2
3
4
5
6
7
8
9
zuul:
sensitiveHeaders: Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Origin,Access-Control-Max-Age,P3P
ignoredPatterns: /files/**
routes:
auth:
sensitiveHeaders: Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Origin,Access-Control-Max-Age,P3P
rootpath:
path: /**
url: http://www.baidu.com

注意: 如果在虚拟机下访问本地主机,一定要设置ignoredPatterns属性,避免Zuul本身又会拦截对本地静态资源的请求。

四、获取Response

post类型的过滤器中,利用RequestContext对象的两个方法可以实现对Response的获取,分别是:getResponseDataStream()getResponseBody()。在返回的数据中,getResponseDataStream()getResponseBody()中不为空的那个才包含了存储的数据,即需要根据具体的场景二选一。

需要注意的是,用InputStream responseDataStream = requestContext.getResponseDataStream()会导致这个流不可以重新被读,因此,如果反向代理端要给客户端返回Response,需要重新利用setResponseDataStream()或者setResponseBody()方法写入数据流。
并且,并且过滤器的filterOrder不能超过1000

示例代码 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Component
public class ModifyDomFilter extends ZuulFilter {

@Override
public String filterType() {
return POST_TYPE;
}

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

@Override
public boolean shouldFilter() {
String uri = RequestContext.getCurrentContext().getRequest().getRequestURI();
if(uri.contains("png") || uri.contains("js")|| uri.contains("get") || uri.contains("clockInfo")||uri.contains("isShwoClock")){
return false;
}
return true;
}

@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
InputStream responseDataStream = ctx.getResponseDataStream();
try {
String a = StringUtils.newStringUtf8(GZIPUtil.uncompress(StreamUtils.copyToByteArray(responseDataStream)));
System.out.println(a);
InputStream is = new ByteArrayInputStream(GZIPUtil.compress(a));
ctx.setResponseDataStream(is);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

}

其中工具类中的upcompress()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static byte[] uncompress(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return null;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
try {
GZIPInputStream ungzip = new GZIPInputStream(in);
byte[] buffer = new byte[256];
int n;
while ((n = ungzip.read(buffer)) >= 0) {
out.write(buffer, 0, n);
}
} catch (IOException e) {
}
return out.toByteArray();
}

五、配置https连接

自己颁发https证书

需要用到Open-SSL 工具,其组成主要包括以下三个组件:

  • OpenSSL:多用途的命令行工具
  • libcrypto:加密算法库
  • libssl:加密模块应用库,实现了ssl及tls
    OpenSSL可以实现:秘钥证书管理、对称加密和非对称加密 。

利用OpenSSL的命令行工具生成秘钥和证书的详细过程如下,按照这个流程就能生成Https证书,整个过程可以概括为:

  • 生成服务器的公钥和私钥
  • 生成客户端的公钥和私钥
  • 创建CSR请求文件,生成CA证书

具体流程可以参考我前面的文章:如何自己签发Https证书

Spring Boot配置多端口监听

因为Http请求和Https请求不会通过同一个端口通信,因此,还需要在Spring Boot中增加一个Tomcat容器,实现对两个端口的监听。

具体的实现有两种方法:

方法一:配置文件+配置类

配置文件如下:

1
2
3
4
5
6
7
8
server:
port: 80
https:
port: 443
baidu:
key-store: classpath:out.keystore
key-store-password: 123123
key-password: 123123

配置类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import org.apache.catalina.Context;
import org.apache.catalina.connector.Connector;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @author wufan
*/
@Configuration
public class TomcatHttpConfig
{

/**
* 配置内置的servlet容器工厂为tomcat.
* @return
*/
@Bean
public TomcatServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint constraint = new SecurityConstraint();
constraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
constraint.addCollection(collection);
context.addConstraint(constraint);
}
};
tomcat.addAdditionalTomcatConnectors(httpConnector());
return tomcat;
}

@Bean
public Connector httpConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
//Connector监听的http的端口号
connector.setPort(80);
connector.setSecure(false);
//监听到http的端口号后转向到的https的端口号
connector.setRedirectPort(8443);
return connector;
}
}

这种方法能实现同时监听两个端口得单个证书需求的反向代理服务器,但是没办法同时配置多个证书。

方法二:配置多证书版本

新建一个类来同时完成Tomcat容器增加和多证书配置,并将对象交给Bean容器管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* @author wufan
*/
@Component
public class MultipleCerConfig {
@Value("${https.port}")
private Integer port;

@Value("${https.baidu.key-store-password}")
private String key_store_password;

@Value("${https.baidu.key-password}")
private String key_password;

@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
tomcat.addAdditionalTomcatConnectors(createSslConnector());
return tomcat;
}

/**
* 给 SSLHostConfig 加证书
*/
private SSLHostConfigCertificate getCert(SSLHostConfig sslHostConfig,String alias,String key_store_name, String key_store_password,String key_password) throws IOException {
File keystore = new ClassPathResource(key_store_name).getFile();
SSLHostConfigCertificate cert =
new SSLHostConfigCertificate(sslHostConfig, SSLHostConfigCertificate.Type.RSA);
cert.setCertificateKeystoreFile(keystore.getAbsolutePath());
//如有需要,配置别名
//cert.setCertificateKeyAlias(alias);
cert.setCertificateKeystorePassword(key_store_password);
cert.setCertificateKeyPassword(key_password);
return cert;
}

/**
* 根据不同的 hostname 配置不同的 SSLHostConfig
*/
private SSLHostConfig getSSLHostConfig(String hostname,String key_store_name, String key_store_password,String key_password) throws IOException {
SSLHostConfig sslHostConfig = new SSLHostConfig();
// 根据不同的需求,给证书添加 hostname
sslHostConfig.setHostName(hostname);
SSLHostConfigCertificate cert = getCert(sslHostConfig,"tomcat",key_store_name ,key_store_password,key_password);
sslHostConfig.addCertificate(cert);
return sslHostConfig;
}

/**
* 配置多证书
*/
private Connector createSslConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
try {
SSLHostConfig defaultSslHostConfig = new SSLHostConfig();
SSLHostConfigCertificate cert = getCert(defaultSslHostConfig,"default","htx-server.jks" ,key_store_password,key_password);
defaultSslHostConfig.addCertificate(cert);


SSLHostConfig sslHostConfig1 = getSSLHostConfig("www.baidu.com","htx-server.jks" ,key_store_password,key_password);
SSLHostConfig sslHostConfig2 = getSSLHostConfig("sina.com","sina.jks" ,"114114","114114");

connector.addSslHostConfig(defaultSslHostConfig);
connector.addSslHostConfig(sslHostConfig1);
connector.addSslHostConfig(sslHostConfig2);


connector.setAttribute("SSLEnabled", "true");
connector.addUpgradeProtocol(new Http2Protocol());
connector.setScheme("https");
connector.setSecure(true);
connector.setPort(port);

return connector;
}
catch (IOException ex) {
throw new IllegalStateException("can't access keystore: [" + "keystore"
+ "] or truststore: [" + "keystore" + "]", ex);
}
}
}

如果部署成功,会看到类似下面的提示信息:

1
Tomcat started on port(s): 8443 (https) 80 (http) with context path ''

导入多证书报错~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
org.apache.catalina.LifecycleException: Protocol handler start failed
at org.apache.catalina.connector.Connector.startInternal(Connector.java:1008) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
at org.apache.catalina.core.StandardService.addConnector(StandardService.java:226) [tomcat-embed-core-9.0.19.jar:9.0.19]
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.addPreviouslyRemovedConnectors(TomcatWebServer.java:259) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.start(TomcatWebServer.java:197) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.startWebServer(ServletWebServerApplicationContext.java:311) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.finishRefresh(ServletWebServerApplicationContext.java:164) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:552) [spring-context-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:142) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:316) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at com.test.httpszuul.HttpszuulApplication.main(HttpszuulApplication.java:15) [classes/:na]
Caused by: java.lang.IllegalArgumentException: No SSLHostConfig element was found with the hostName [_default_] to match the defaultSSLHostConfigName for the connector [https-jsse-nio-8443]
at org.apache.tomcat.util.net.AbstractJsseEndpoint.initialiseSsl(AbstractJsseEndpoint.java:76) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
at org.apache.tomcat.util.net.NioEndpoint.bind(NioEndpoint.java:227) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
at org.apache.tomcat.util.net.AbstractEndpoint.bindWithCleanup(AbstractEndpoint.java:1116) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
at org.apache.tomcat.util.net.AbstractEndpoint.start(AbstractEndpoint.java:1202) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
at org.apache.coyote.AbstractProtocol.start(AbstractProtocol.java:568) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
at org.apache.catalina.connector.Connector.startInternal(Connector.java:1005) ~[tomcat-embed-core-9.0.19.jar:9.0.19]
... 14 common frames omitted

其他

对于代理服务器的本地验证,需要搭建一个虚拟机扮演客户端角色,并且虚拟机的DNS服务器设置为本机。这里可以用Vmware开启的虚拟机设置为“仅主机模式”,详细过程可以参考博客VMware仅主机模式访问外网。 接下来通过修改虚拟机的host(对于Linux修改/etc/hosts文件)将本机的IP作为域名解析的IP地址。

项目代码详见github,希望同学们不吝赐教。


参考链接:
1.VMware仅主机模式访问外网
2.How to get response body in Zuul post filter?
3.Multiple ssl certificates (multiple domains) to same spring boot application
4.spring cloud/spring boot同时支持http和https访问
5.Java Code Examples for org.apache.tomcat.util.net.SSLHostConfig
6.关于keystore的简单介绍

-------------本文结束感谢阅读-------------