开发者

Springboot服务HTTP/HTTPS双监听及路由的实现示例

目录
  • 背景
  • 1. SpringBoot Http/Https监听双支持
    • 1.1 代码实现
    • 1.2 配置
  • 2. SpringCloud Gateway Http/Https路由双支持
    • 2.1 代码实现
    • 2.2 配置
  • 结束语

    背景

    一般来说SpringCloud Gateway到后面服务的路由属于内网交互,因此路由方式是否是Https就显得不是那么重要了。事实上也确实如此,大多数的应用开发时基本都是直接Http就过去了,不会一开始就是直接上Https。然而随着时间的推移,项目规模的不断扩大,当被要求一定要走Https时,就会面临一种困惑:将所有服务用一刀切的方式改为Https方式监听,同时还要将网关服务所有的路由方式也全部切为Https方式,一旦生产环境上线出问题将要面临全量服务的归滚,这时运维很可能跳出来说:生产环境几十个服务,每个服务最少2个节点,全量部署和回滚不可能在短时间完成。另外测试同学也可能会说,现在没有全量接口自动化回归测试工具,做一个次人工的全量接口回归测试也不现实。因此在这种情况下最稳妥的方式是实现:SpringCloud Gateway & SpringBoot RestController Http/Https双支持,这样可以做到分批分次进行切换,那么上面的困惑自然也就不存在了。

    1. SpringBoot Http/Https监听双支持

    1.1 代码实现

    为了不对原来的Http监听产生任何影响,因此需要保障以下两点:

    1、原主端口(server.port)监听什么都不变,监听方式仍为http,附加端口监听方式为https。(需要绕开的问题是:如果一个服务有多个监听端口,主端口会优先选择https方式)

    2、附加端口不进行nacos服务注册(主要的考虑点还是不对原来的http监听和路由产生任何影响,这里我的方案是https监听端口号为http端口+10000)。

    这样就能实现SpringBoot服务主端口Http监听,附加端口Https监听。

    实现代码如下:

    import org.apache.catalina.connector.Connector;
    import org.apache.coyote.http11.Http11NioProtocol;
    import org.springframework.beans.factory.annotation.Value;
    import or编程g.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(TomcatServletWebServ编程客栈erFactory.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、上述代码中的配置http://www.devze.com默认值大家自行修改(key-store:XXX.p12,key-store-password:XXX),如果觉得配置ad编程ditional-https-connector相关配置命名不合适也可自行修改。当配置好additional-https-connector相关配置(additional-https-connector.ssl.enabled是一个https附加端口监听的开关),启动服务就可以看到类似如下的日志,同时查看nacos中的服务实例也会发现并没有进行https端口的服务注册;

    2、这里我用的是p12自签证书,证书需要放到项目的resouces目录下(可以用keytool -genkey命令去生成一个)。

    Springboot服务HTTP/HTTPS双监听及路由的实现示例

    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)进行转发uri的https重写;

    这样就能实现在配置列表中的服务进行Https路由,否则保持原有Https路由。

    2.1 代码实现

    • 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;
    
    iphpmport 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、这里实现了配置的刷新,因此需要进行服务的https路由切换时只需修改配置即可,而网关服务不需要重启;

    2、过滤器使用Set进行判断,效率上肯定优于对List的遍历查找;

    3、过滤器的Order建议放到最后,因此可以直接使用Integer.MAX_VALUE(我们的项目中有多个过滤器,并且通过FilterEnum枚举去统一管理);

    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)

    Springboot服务HTTP/HTTPS双监听及路由的实现示例

    结束语

    通过上述两步就能实现SpringCloud Gateway & SpringBoot RestController Http/Https双支持,严谨的做法是还需要将FeignClient的调用进行Https化,上面的实现方式中之所以不对https端口进行注册的原因就是避免Http方式的FeignClient去调用Https目标端口从而引发问题。关于FeignClient的Https切换实际上也可以借鉴网关的思路将请求uri重写为端口号+10000的https请求即可。

    那么通过这个思路就可以实现:服务的分批、FeignClient分步Https路由切换,从而保障整个割接风险可控和平滑。

    到此这篇关于Springboot服务HTTP/HTTPS双监听及路由的实现示例的文章就介绍到这了,更多相关Springboot HTTP/HTTPS双监听及路由内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜