Springboot服務HTTP/HTTPS雙監(jiān)聽及路由的實現(xiàn)示例
背景
一般來說SpringCloud Gateway到后面服務的路由屬于內網交互,因此路由方式是否是Https就顯得不是那么重要了。事實上也確實如此,大多數的應用開發(fā)時基本都是直接Http就過去了,不會一開始就是直接上Https。然而隨著時間的推移,項目規(guī)模的不斷擴大,當被要求一定要走Https時,就會面臨一種困惑:將所有服務用一刀切的方式改為Https方式監(jiān)聽,同時還要將網關服務所有的路由方式也全部切為Https方式,一旦生產環(huán)境上線出問題將要面臨全量服務的歸滾,這時運維很可能跳出來說:生產環(huán)境幾十個服務,每個服務最少2個節(jié)點,全量部署和回滾不可能在短時間完成。另外測試同學也可能會說,現(xiàn)在沒有全量接口自動化回歸測試工具,做一個次人工的全量接口回歸測試也不現(xiàn)實。因此在這種情況下最穩(wěn)妥的方式是實現(xiàn):SpringCloud Gateway & SpringBoot RestController Http/Https雙支持,這樣可以做到分批分次進行切換,那么上面的困惑自然也就不存在了。
1. SpringBoot Http/Https監(jiān)聽雙支持
1.1 代碼實現(xiàn)
為了不對原來的Http監(jiān)聽產生任何影響,因此需要保障以下兩點:
1、原主端口(server.port)監(jiān)聽什么都不變,監(jiān)聽方式仍為http,附加端口監(jiān)聽方式為https。(需要繞開的問題是:如果一個服務有多個監(jiān)聽端口,主端口會優(yōu)先選擇https方式)
2、附加端口不進行nacos服務注冊(主要的考慮點還是不對原來的http監(jiān)聽和路由產生任何影響,這里我的方案是https監(jiān)聽端口號為http端口+10000)。
這樣就能實現(xiàn)SpringBoot服務主端口Http監(jiān)聽,附加端口Https監(jiān)聽。
實現(xiàn)代碼如下:
import org.apache.catalina.connector.Connector;
import org.apache.coyote.http11.Http11NioProtocol;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* HttpsConnectorAddInConfiguration
*
* @author chenx
*/
@Configuration
public class HttpsConnectorAddInConfiguration {
private static final int HTTPS_PORT_OFFSET = 10000;
@Value("${server.port}")
private int port;
@Value("${additional-https-connector.ssl.key-store:XXX.p12}")
private String keyStore;
@Value("${additional-https-connector.ssl.key-store-password:XXX}")
private String keyStorePassword;
@Value("${additional-https-connector.ssl.key-store-type:PKCS12}")
private String keyStoreType;
@Value("${additional-https-connector.ssl.enabled:false}")
private boolean enabled;
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> servletContainer() {
return server -> {
if (!this.enabled) {
return;
}
Connector httpsConnector = this.createHttpsConnector();
server.addAdditionalTomcatConnectors(httpsConnector);
};
}
/**
* createHttpsConnector
*
* @return
*/
private Connector createHttpsConnector() {
Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
connector.setScheme("https");
connector.setPort(this.port + HTTPS_PORT_OFFSET);
connector.setSecure(true);
Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
protocol.setSSLEnabled(true);
protocol.setKeystoreFile(this.keyStore);
protocol.setKeystorePass(this.keyStorePassword);
protocol.setKeystoreType(this.keyStoreType);
protocol.setSslProtocol("TLS");
return connector;
}
}
備注:
1、上述代碼中的配置默認值大家自行修改(key-store:XXX.p12,key-store-password:XXX),如果覺得配置additional-https-connector相關配置命名不合適也可自行修改。當配置好additional-https-connector相關配置(additional-https-connector.ssl.enabled是一個https附加端口監(jiān)聽的開關),啟動服務就可以看到類似如下的日志,同時查看nacos中的服務實例也會發(fā)現(xiàn)并沒有進行https端口的服務注冊;
2、這里我用的是p12自簽證書,證書需要放到項目的resouces目錄下(可以用keytool -genkey命令去生成一個)。

1.2 配置
配置示例如下,keyStore、keyStorePassword、keyStoreType使用代碼中的默認值,需要更換證書的時候再進行配置。
server:
port: 9021
tomcat:
min-spare-threads: 400
max-threads: 800
additional-https-connector:
ssl:
enabled: true
2. SpringCloud Gateway Http/Https路由雙支持
思路:在網關服務增加自定義配置(HttpsServiceConfig)來定義需要切換為https路由的服務列表,然后使用過濾器(HttpsLoadBalancerFilter)進行轉發(fā)uri的https重寫;
這樣就能實現(xiàn)在配置列表中的服務進行Https路由,否則保持原有Https路由。
2.1 代碼實現(xiàn)
- HttpsServiceConfig
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.endpoint.event.RefreshEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* HttpsServiceConfig
*
* @author chenx
*/
@Slf4j
@Component
@RefreshScope
@ConfigurationProperties(prefix = "bw.gateway")
public class HttpsServiceConfig {
private List<String> httpsServices;
private Set<String> httpsServiceSet = new HashSet<>();
public List<String> getHttpsServices() {
return this.httpsServices;
}
public void setHttpsServices(List<String> httpsServices) {
this.httpsServices = httpsServices;
this.updateHttpsServices();
}
public Set<String> getHttpsServiceSet() {
return this.httpsServiceSet;
}
/**
* handleRefreshEvent
*/
@EventListener(RefreshEvent.class)
public void handleRefreshEvent() {
this.updateHttpsServices();
}
/**
* updateHttpsServices
*/
private void updateHttpsServices() {
this.httpsServiceSet = CollectionUtils.isNotEmpty(this.httpsServices) ? new HashSet<>(this.httpsServices) : new HashSet<>();
log.info("httpsServiceSet updated, httpsServiceSet.size() = {}", this.httpsServiceSet.size());
}
}
- HttpsLoadBalancerFilter
import com.beam.work.gateway.common.FilterEnum;
import com.beam.work.gateway.config.HttpsServiceConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.Objects;
/**
* HttpsLoadBalancerFilter
*
* @author chenx
*/
@Slf4j
@RefreshScope
@Component
public class HttpsLoadBalancerFilter implements GlobalFilter, Ordered {
private static final int HTTPS_PORT_OFFSET = 10000;
private final LoadBalancerClient loadBalancer;
@Autowired
private HttpsServiceConfig httpsServiceConfig;
public HttpsLoadBalancerFilter(LoadBalancerClient loadBalancer) {
this.loadBalancer = loadBalancer;
}
@Override
public int getOrder() {
return FilterEnum.HTTPS_LOAD_BALANCER_FILTER.getCode();
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
boolean isRewriteToHttps = Objects.nonNull(route) && this.httpsServiceConfig.getHttpsServiceSet().contains(route.getId());
if (isRewriteToHttps) {
ServiceInstance instance = this.loadBalancer.choose(route.getUri().getHost());
if (Objects.nonNull(instance)) {
URI originalUri = exchange.getRequest().getURI();
URI httpsUri = UriComponentsBuilder.fromUri(originalUri)
.scheme("https")
.host(instance.getHost())
.port(instance.getPort() + HTTPS_PORT_OFFSET)
.build(true)
.toUri();
log.info("HttpsLoadBalancerFilter RewriteToHttps: {}", httpsUri.toString());
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, httpsUri);
}
}
return chain.filter(exchange);
}
}
備注:
1、這里實現(xiàn)了配置的刷新,因此需要進行服務的https路由切換時只需修改配置即可,而網關服務不需要重啟;
2、過濾器使用Set進行判斷,效率上肯定優(yōu)于對List的遍歷查找;
3、過濾器的Order建議放到最后,因此可以直接使用Integer.MAX_VALUE(我們的項目中有多個過濾器,并且通過FilterEnum枚舉去統(tǒng)一管理);
2.2 配置
配置示例:
spring:
cloud:
gateway:
enabled: true
httpclient:
ssl:
use-insecure-trust-manager: true
connect-timeout: 10000
response-timeout: 120000
pool:
max-idle-time: 15000
max-life-time: 45000
evictionInterval: 5000
routes:
- id: bw-star-favorite
uri: lb://bw-star-favorite
order: -1
predicates:
- Path=/star-favoritear/v1/**
bw:
gateway:
xssRequestFilterEnable: false
xssResponseFilterEnable: false
httpsServices:
- bw-star-favorite
備注:
1、需要變更的配置為:
- 開啟ssl信任(spring.cloud.gateway.httpclient.ssl):
- 設置https路由服務列表(bw.gateway.httpsServices)

結束語
通過上述兩步就能實現(xiàn)SpringCloud Gateway & SpringBoot RestController Http/Https雙支持,嚴謹的做法是還需要將FeignClient的調用進行Https化,上面的實現(xiàn)方式中之所以不對https端口進行注冊的原因就是避免Http方式的FeignClient去調用Https目標端口從而引發(fā)問題。關于FeignClient的Https切換實際上也可以借鑒網關的思路將請求uri重寫為端口號+10000的https請求即可。
那么通過這個思路就可以實現(xiàn):服務的分批、FeignClient分步Https路由切換,從而保障整個割接風險可控和平滑。
到此這篇關于Springboot服務HTTP/HTTPS雙監(jiān)聽及路由的實現(xiàn)示例的文章就介紹到這了,更多相關Springboot HTTP/HTTPS雙監(jiān)聽及路由內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
mybatisplus?復合主鍵(多主鍵)?CRUD示例詳解
這篇文章主要介紹了mybatisplus?復合主鍵(多主鍵)?CRUD實例詳解,本文通過實例代碼圖文相結合給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-03-03
SPRING FRAMEWORK BEAN作用域和生命周期原理解析
這篇文章主要介紹了SPRING FRAMEWORK BEAN作用域和生命周期原理解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-01-01
解決@Async(“taskExecutor“)異步線程報錯問題
這篇文章主要介紹了解決@Async(“taskExecutor“)異步線程報錯問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08

