本文介绍如何利用 Spring Cloud
微服务架构中的Zuul
的跨域配置实现反向代理服务功能。
反向代理
是代理服务器的一种。服务器根据客户端的请求,从其关联的一组或多组后端服务器(如Web服务器)上获取资源,然后再将这些资源返回给客户端,客户端只会得知反向代理的IP地址,而不知道在代理服务器后面的服务器簇的存在。这篇文章,主要介绍如何利用Zuul实现反向代理服务器中常见的“流量透传”、“请求转发”、“重定向”、“修改/获取服务器的Response”、“代理Https安全访问”等。
一、流量透传
用Zuul实现的反向代理功能,通过编辑 Spring Boot
配置文件application.yml
就能很方便地实现流量透传。
可以通过在配置文件中配置url
直接透传所有的代理流量:
1 | zuul: |
注意:Zuul作为统一网关代理所有后台接口,对于代码发起的http请求自然无需在意跨域的问题,但是对于提供给前端访问的接口,由于浏览器的安全策略对于跨域的资源访问会被拦截,因此需要配置sensitiveHeaders
来过滤掉相关headers
。
二、请求转发与重定向,获取get
与post
请求参数
对于需要转发的请求,除了在配置文件properties.yml
中配置指定url
之外,也可以配置过滤器来实现:
1 |
|
注意,这里的过滤器类型为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
3static:
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
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";
public String filterType() {
return ROUTE_TYPE;
}
public int filterOrder() {
// 99
return SIMPLE_HOST_ROUTING_FILTER_ORDER -1;
}
public boolean shouldFilter() {
String uri = RequestContext.getCurrentContext().getRequest().getRequestURI();
if(uri.contains(jsString1)){
System.out.println(uri);
return true;
}
return false;
}
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
9zuul:
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 |
|
其中工具类中的upcompress()
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public 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
8server:
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
48import 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
*/
public class TomcatHttpConfig
{
/**
* 配置内置的servlet容器工厂为tomcat.
* @return
*/
public TomcatServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
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;
}
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 | /** |
如果部署成功,会看到类似下面的提示信息: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
24org.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的简单介绍