Spring配置文件中:密码明文改为密文处理方式(通用方法)

news2025/1/10 1:27:50

目录

一、背景    

二、思路

A) 普通方式

B) 适合bootstrap.properties方式

三、示例

A) 普通方式(连接Redis集群)

A) 普通方式(连接RocketMQ)

B) 适合bootstrap.properties方式

四、总结


一、背景    

  SpringBoot和SpringCloud中涉及多个配置文件,配置文件中对于密码默认是明文方式,这种方式在生产环境一般是不被允许的。为避免配置文件中出现明文,应当在配置文件中配置为密文,然后在启动时在程序内部完成解密。

    本文提供了通用的处理方式,可以适配以下几类配置文件:

  • 本地bootstrap.properties   在Spring的Bean创建之前的配置
  • 本地application.properties   在Spring的配置,包括带profile环境的配置
  • 配置中心上的配置(例如nacos上的Data ID)   

   为了适应配置文件涉及密码由明文改为密文,需要分为两步:

①将配置文件中涉及密文的配置项配置为密文字符串(需自己加密计算得到);

②在Spring启动中读取密文字符串并解密还原。

二、思路

       对于以上第②步Spring启动时的处理,由于以上配置文件在Spring加载的时机和生命周期不同,有两种处理方式:

A) 普通方式

      由于Spring中的对本地application.properties或者配置中心上的配置(例如nacos上的Data ID)在Spring Bean创建过程中,会有对应的配置Bean(通过注解@Configuration申明的Java类),Spring会自动根据读取解析配置文件并赋值给Bean。

      因此,若需要对密文字符串并解密还原,可以对配置Bean(通过注解@Configuration申明的Java类)进行继承,Override重写对应的set方法,完成解密。

B) 适合bootstrap.properties方式

      对于Spring Cloud,在bootstrap阶段还未创建Bean,所以以上Override重写对应的set方法并不适用。所以对于bootstrap.properties配置文件。可通过实现EnvironmentPostProcessor接口,来捕获Environment配置,解密后将配置新值设置到Environment中。

三、示例

A) 普通方式(连接Redis集群)

      下面以连接Redis集群为例进行说明,连接Redis集群的配置项可以在本地application.properties或者配置中心上的配置(例如nacos上的Data ID),且其中spring.redis.password配置项值已经设置为密文。

      下面代码对配置Bean(通过注解@Configuration申明的Java类RedisProperties)进行继承,Override重写对应的set方法。Java代码如下:

package 包指定忽略,请自定;

import 忽略解密计算工具类SystemSecurityAlgorithm,请自定;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.util.StringUtils;

/**
 * 连接Redis集群的配置类【通过@Configuration覆盖原Bean机制】:
 *   1、连接Redis的连接password不得出现明文,故需在properties配置文件中配置为加密密文(加密算法Java类为:SystemSecurityAlgorithm),然后在启动时通过本类解密
 *   2、贵金属应用服务采用多数据中心DataCenter部署。而每逻辑中心均有独立的Redis集群。 应用服务应连接同逻辑中心内的Redis集群,既北京的应用服务不应该连接合肥Redis集群
 *     既:对于同服务的不同实例,应根据服务实例所在逻辑中心(具体见枚举ServiceConstant.DataCenter定义的逻辑中心)连接相同逻辑中心下的Redis集群。
 *     因此:
 *        a).以Spring标准Redis连接配置为基础,对nodes值中各个IP端口配置,在各IP前增加一个大写字母:该IP所在DataCenter数据中心的英文代码
 *        b).以Spring标准Redis连接配置为基础,对password值改为可配多个密码,以逗号分隔,每个密码前增加一个大写字母,该密码是连接哪个Redis集群的DataCenter数据中心的英文代码
 * 为支持以上,定制化开发本类,实现处理最终还原至Spring标准连接Redis的配置,以供lettuce创建连接池。
 *  -----------------------------------------------------------
 * 机制适用性:
 * 除了通过@Configuration覆盖原Bean机制,还有通过实现EnvironmentPostProcessor接口机制。两种机制适用性说明如下:
 *   bootstrap.properties配置文件(bootstrap阶段,还未创建Bean) →→适合→→ 【实现EnvironmentPostProcessor接口机制】
 *   本地application.properties配置文件(正常SpringBoot启动,通过@Configuration注解的Bean) →→适合→→ 【实现EnvironmentPostProcessor接口机制】和【通过@Configuration覆盖原Bean机制】均可
 *   从Nacos等配置中心获取得到的配置文件 →→适合→→ 【通过@Configuration覆盖原Bean机制】
 *
 */
@Configuration
@Primary // 由于默认RedisProperties作为配置类会自动创建Bean。 为避免存在两个同类型(RedisProperties)Bean,所以本类通过注解Primary,使得只有本类生效。相当于替代默认RedisProperties
public class GjsRedisProperties extends RedisProperties {

    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GjsRedisProperties.class);

    @Override
    public void setPassword(String orginPassword) {
        if(StringUtils.hasText(orginPassword)) {
            // 对密文解密并设置
            if (StringUtils.hasText(orginPassword) && orginPassword.length() >= 32 ) { // 如果满足密码密文的长度及大小写要求,视为密文,解密
                String padStr = SystemSecurityAlgorithm.decryptStr(orginPassword);
                log.debug("连接Redis配置项spring.redis.password: 解密前orginPassword=[{}], 解密后padStr=[{}]", orginPassword, padStr); //为避免密码泄露,仅debug才输出明文
                log.info("连接Redis配置项spring.redis.password: 对密文orginPassword=[{}]已完成解密", orginPassword);
                super.setPassword(padStr);
            } else { // 不满足密码密文的长度及大小写要求(视为明文,不解密),保持不变
                log.warn("连接Redis配置项spring.redis.password的:orginPassword=[{}]不满足密码密文的长度及大小写要求(视为明文,不解密),保持不变", orginPassword);
                super.setPassword(orginPassword);
            }
        }
    }
}

A) 普通方式(连接RocketMQ)

      下面以连接RocketMQ为例进行说明,连接RocketMQ的配置项可以在本地application.properties或者配置中心上的配置(例如nacos上的Data ID),且其中rocketmq.producer.secret-keyrocketmq.consumer.secret-key配置项值已经设置为密文。

      下面代码对配置Bean(通过注解@Configuration申明的Java类RocketMQProperties)进行继承,Override重写对应的set方法。Java代码如下:

package 包指定忽略,请自定;

import 忽略解密计算工具类SystemSecurityAlgorithm,请自定;
import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.Map;

/**
 * 连接RocketMQ的配置类【通过@Configuration覆盖原Bean机制】:
 *   因连接RocketMQ的secret-key不得出现明文,故需在properties配置文件中配置为加密密文(加密算法Java类为:SystemSecurityAlgorithm),然后在启动时通过本类解密
 *  -----------------------------------------------------------
 * 机制适用性:
 * 除了通过@Configuration覆盖原Bean机制,还有通过实现EnvironmentPostProcessor接口机制。两种机制适用性说明如下:
 *   bootstrap.properties配置文件(bootstrap阶段,还未创建Bean) →→适合→→ 【实现EnvironmentPostProcessor接口机制】
 *   本地application.properties配置文件(正常SpringBoot启动,通过@Configuration注解的Bean) →→适合→→ 【实现EnvironmentPostProcessor接口机制】和【通过@Configuration覆盖原Bean机制】均可
 *   从Nacos等配置中心获取得到的配置文件 →→适合→→ 【通过@Configuration覆盖原Bean机制】
 *
 */
@Configuration
@Primary // 由于默认RocketMQProperties作为配置类会自动创建Bean。 为避免存在两个同类型(RocketMQProperties)Bean,所以本类通过注解Primary,使得只有本类生效。相当于替代默认RocketMQProperties
public class GjsRocketMQProperties extends RocketMQProperties {

    final private String KEYNAME_PRODUCER_SECRET = "rocketmq.producer.secret-key";
    final private String KEYNAME_CONSUMER_SECRET = "rocketmq.consumer.secret-key";

    @Autowired
    ConfigurableApplicationContext springContext;

    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GjsRocketMQProperties.class);

    @Override
    public void setProducer(Producer producer) {
        final String orginSecretKey = producer.getSecretKey();
        // 对密文解密并设置
        if (StringUtils.hasText(orginSecretKey) && orginSecretKey.length() >= 32) { // 如果满足密码密文的长度及大小写要求,视为密文,解密
            String padStr = SystemSecurityAlgorithm.decryptStr(orginSecretKey);
            log.debug("连接RocketMQ配置项{}: 解密前orginSecretKey=[{}], 解密后padStr=[{}]", KEYNAME_PRODUCER_SECRET, orginSecretKey, padStr); //为避免密码泄露,仅debug才输出明文
            log.info("连接RocketMQ配置项{}: 对密文orginSecretKey=[{}]已完成解密", KEYNAME_PRODUCER_SECRET, orginSecretKey);
            producer.setSecretKey(padStr);

            // 由于RocketMQ在构建DefaultRocketMQListenerContainer过程中,会从Spring的Environment中获取配置。
            // 附调用关系简要说明如下:
            //     org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer.afterPropertiesSet()
            //       org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer.initRocketMQPushConsumer()
            //         org.apache.rocketmq.spring.support.RocketMQUtil.getRPCHookByAkSk()
            //           org.springframework.core.env.AbstractEnvironment.resolveRequiredPlaceholders()
            //             ......
            //               org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertyResolver.findPropertyValue()
            // 因此一并修改环境中的值,使其能取得新值
            modifyEnvironmentValue(springContext.getEnvironment(), KEYNAME_PRODUCER_SECRET, padStr);

        } else { // 不满足密码密文的长度及大小写要求(视为明文,不解密),保持不变
            log.warn("连接RocketMQ配置项rocketmq.producer.secret-key值=[{}]不满足密码密文的长度及大小写要求(视为明文,不解密),保持不变", orginSecretKey);
        }

        super.setProducer(producer);
    }

    @Override
    public void setConsumer(PushConsumer pushConsumer) {
        final String orginSecretKey = pushConsumer.getSecretKey();
        // 对密文解密并设置
        if (StringUtils.hasText(orginSecretKey) && orginSecretKey.length() >= 32 ) { // 如果满足密码密文的长度及大小写要求,视为密文,解密
            String padStr = SystemSecurityAlgorithm.decryptStr(orginSecretKey);
            log.debug("连接RocketMQ配置项{}: 解密前orginSecretKey=[{}], 解密后padStr=[{}]", KEYNAME_CONSUMER_SECRET, orginSecretKey, padStr); //为避免密码泄露,仅debug才输出明文
            log.info("连接RocketMQ配置项{}: 对密文orginSecretKey=[{}]已完成解密", KEYNAME_CONSUMER_SECRET, orginSecretKey);
            pushConsumer.setSecretKey(padStr);

            // 由于RocketMQ在构建DefaultRocketMQListenerContainer过程中,会从Spring的Environment中获取配置。
            // 附调用关系简要说明如下:
            //     org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer.afterPropertiesSet()
            //       org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer.initRocketMQPushConsumer()
            //         org.apache.rocketmq.spring.support.RocketMQUtil.getRPCHookByAkSk()
            //           org.springframework.core.env.AbstractEnvironment.resolveRequiredPlaceholders()
            //             ......
            //               org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertyResolver.findPropertyValue()
            // 因此一并修改环境中的值,使其能取得新值
            modifyEnvironmentValue(springContext.getEnvironment(), KEYNAME_CONSUMER_SECRET, padStr);

        } else { // 不满足密码密文的长度及大小写要求(视为明文,不解密),保持不变
            log.warn("连接RocketMQ配置项{}的值=[{}]不满足密码密文的长度及大小写要求(视为明文,不解密),保持不变", KEYNAME_CONSUMER_SECRET, orginSecretKey);
        }

        super.setConsumer(pushConsumer);
    }

    @Override
    public void setPullConsumer(PullConsumer pullConsumer) {
        final String orginSecretKey = pullConsumer.getSecretKey();
        // 对密文解密并设置
        if (StringUtils.hasText(orginSecretKey) && orginSecretKey.length() >= 32 ) { // 如果满足密码密文的长度及大小写要求,视为密文,解密
            String padStr = SystemSecurityAlgorithm.decryptStr(orginSecretKey);
            log.debug("连接RocketMQ配置项{}: 解密前orginSecretKey=[{}], 解密后padStr=[{}]", KEYNAME_CONSUMER_SECRET, orginSecretKey, padStr); //为避免密码泄露,仅debug才输出明文
            log.info("连接RocketMQ配置项{}: 对密文orginSecretKey=[{}]已完成解密", KEYNAME_CONSUMER_SECRET, orginSecretKey);
            pullConsumer.setSecretKey(padStr);

            // 由于RocketMQ在构建DefaultRocketMQListenerContainer过程中,会从Spring的Environment中获取配置。
            // 附调用关系简要说明如下:
            //     org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer.afterPropertiesSet()
            //       org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer.initRocketMQPushConsumer()
            //         org.apache.rocketmq.spring.support.RocketMQUtil.getRPCHookByAkSk()
            //           org.springframework.core.env.AbstractEnvironment.resolveRequiredPlaceholders()
            //             ......
            //               org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertyResolver.findPropertyValue()
            // 因此一并修改环境中的值,使其能取得新值
            modifyEnvironmentValue(springContext.getEnvironment(), KEYNAME_CONSUMER_SECRET, padStr);

        } else { // 不满足密码密文的长度及大小写要求(视为明文,不解密),保持不变
            log.warn("连接RocketMQ配置项{}的值=[{}]不满足密码密文的长度及大小写要求(视为明文,不解密),保持不变", KEYNAME_CONSUMER_SECRET, orginSecretKey);
        }

        super.setPullConsumer(pullConsumer);
    }


    /**
     * 对Spring的Environment的配置项的值修改为新值
     * @param environment Spring的Environment对象
     * @param keyName 配置项名
     * @param newValue 新值
     */
    private void modifyEnvironmentValue(ConfigurableEnvironment environment, final String keyName, String newValue) {
        if(!environment.containsProperty(keyName)) {
            log.warn("当前Spring的environment中不存在名为{}的配置项", keyName);
            return;
        }
        if(environment.getProperty(keyName, "").equals(newValue)) {
            log.debug("当前Spring的environment中配置项{}的值已与新值相同,无需修改", keyName);
            return;
        }
        Map<String, Object> map = new HashMap<>(); //用于存放新值
        map.put(keyName, newValue);
        // 若有map有值,则把该map作为PropertySource加入列表中,以实现:把environment中对应key的value覆盖为新值
        // 必须加到First并且不能存在两个相同的Name的MapPropertySource,值覆盖才能生效
        environment.getPropertySources().addFirst(new MapPropertySource("modifyEnvironmentValue-"+keyName, map));
        log.info("已对Spring的Environment的配置项{}的值修改为新值", keyName);
    }
}

B) 适合bootstrap.properties方式

      下面以连接Nacos配置中心为例进行说明,需要在本地bootstrap.properties配置文件中指定连接Nacos配置中心的Nacos用户名、密码、服务端地址、Data ID等信息。bootstrap.properties配置文件有关连接Nacos配置中心类似如下:

#Nacos配置中心及注册中心的authenticate鉴权用户名和密码(需Nacos服务端开启auth鉴权)
spring.cloud.nacos.username=nacos
spring.cloud.nacos.password=760dee29f9fc82af0cc1d6074879dc39
#Nacos配置中心服务端的地址和端口(形式ip:port,ip:port,...) 。注:nacos-client1.x会按顺序选其中地址进行连接(前个连接失败则自动选后一个)。nacos-client2.x会随机选其中地址进行连接(若连接失败则自动另选)
spring.cloud.nacos.config.server-addr=ip1:8848,ip2:8848,ip3:8848,ip4:8848

#Data ID的前缀(如果不设置,则默认取 ${spring.application.name})
#spring.cloud.nacos.config.prefix=
#默认指定为开发环境
#spring.profiles.active=
#Nacos命名空间,此处不设置,保持默认
#spring.cloud.nacos.config.namespace=
#配置组(如果不设置,则默认为DEFAULT_GROUP)
spring.cloud.nacos.config.group=G_CONFIG_GJS_SERVICE
#指定文件后缀(如果不设置,则默认为properties)
spring.cloud.nacos.config.file-extension=properties

#以下为全局Data ID
spring.cloud.nacos.config.shared-configs[0].data-id=NacosRegDiscoveryInfo.properties
spring.cloud.nacos.config.shared-configs[0].group=G_CONFIG_GJS_GLOBALSHARED
spring.cloud.nacos.config.shared-configs[0].refresh=true

spring.cloud.nacos.config.shared-configs[1].data-id=XXXXX.properties
spring.cloud.nacos.config.shared-configs[1].group=G_CONFIG_GJS_GLOBALSHARED
spring.cloud.nacos.config.shared-configs[1].refresh=true

spring.cloud.nacos.config.shared-configs[2].data-id=YYYYY.properties
spring.cloud.nacos.config.shared-configs[2].group=G_CONFIG_GJS_GLOBALSHARED
spring.cloud.nacos.config.shared-configs[2].refresh=true

其中spring.cloud.nacos.password配置项值已经设置为密文。

      下面的代码通过实现EnvironmentPostProcessor接口,来捕获配置,并将配置新值设置到Environment中。Java代码如下:

package 包指定忽略,请自定;

import 忽略解密计算工具类SystemSecurityAlgorithm,请自定;
import org.apache.commons.logging.Log;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.Map;

/**
 * 本类通过实现EnvironmentPostProcessor接口,实现在Spring启动过程中从environment中读取指定的key值,处理后,然后把environment中对应key的value覆盖为新值。
 * 通过本类已经实现对bootstrap阶段的配置文件处理:
 *   因连接Nacos的password不得出现明文,故bootstrap配置文件中为加密密文(加密算法Java类为:SystemSecurityAlgorithm),然后在启动时通过本类解密
 * -----------------------------------------------------------
 * 注意:
 *   a) 需要在META-INF下的spring.factories文件中配置本类后,本类才会生效(才被Spring扫描识别到)
 *   b) 因为本类是通过实现EnvironmentPostProcessor接口方式,所以本类在SpringCloud启动过程中会被调用两次:
 *         首先是在bootstrap配置文件加载后(SpringCloud为支持配置中心的bootstrap阶段)
 *         其次是在application配置文件加载后(SpringBoot的正常启动时加载配置文件阶段)
 * 机制适用性:
 * 除了通过实现EnvironmentPostProcessor接口机制,还有通过@Configuration覆盖原Bean机制。两种机制适用性说明如下:
 *   bootstrap.properties配置文件(bootstrap阶段,还未创建Bean) →→适合→→ 【实现EnvironmentPostProcessor接口机制】
 *   本地application.properties配置文件(正常SpringBoot启动,通过@Configuration注解的Bean) →→适合→→ 【实现EnvironmentPostProcessor接口机制】和【通过@Configuration覆盖原Bean机制】均可
 *   从Nacos等配置中心获取得到的配置文件 →→适合→→ 【通过@Configuration覆盖原Bean机制】
 *
 */
public class GjsEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {

    /**
     * The default order for the processor.  值越小,优先级越高
     * 因bootstrap配置文件是通过{@link org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor}完成加载处理
     * 由于本EnvironmentPostProcessor类需等待SpringCloud对bootstrap配置文件后才能执行,所以本EnvironmentPostProcessor类优先级需更低
     */
    public static final int ORDER = Ordered.HIGHEST_PRECEDENCE + 50;

    private final DeferredLogFactory logFactory;

    private final Log logger;

    public GjsEnvironmentPostProcessor(DeferredLogFactory logFactory,
                                       ConfigurableBootstrapContext bootstrapContext) {
        this.logFactory = logFactory;
        this.logger = logFactory.getLog(getClass());
    }


    @Override
    public int getOrder() {
        return ORDER;
    }


    /**
     * 从environment中读取指定的key,并进行解密,解密后的结果放入map对象中
     * @param environment 已经有的Spring环境
     * @param keyName 指定的key名
     * @param map 若完成解密,则将解密后的结果放入map对象
     */
    private void decodePwd(ConfigurableEnvironment environment, String keyName, Map<String, Object> map ) {
        if(!environment.containsProperty(keyName)) {
            this.logger.debug("EnvironmentPostProcessor 当前Spring的environment中不存在名为"+keyName+"的配置项");
            return;
        }

        final String origalValue = environment.getProperty(keyName);
        // 对密文解密并设置
        if (StringUtils.hasText(origalValue) && origalValue.length() >= 32) { // 如果满足密码密文的长度及大小写要求,视为密文,解密
            String padStr = SystemSecurityAlgorithm.decryptStr(origalValue);
            this.logger.debug("EnvironmentPostProcessor 配置项"+keyName+"原值=["+origalValue+"], 解密后值=["+padStr+"]"); //为避免在日志中密码泄露,仅debug才输出明文
            this.logger.info("EnvironmentPostProcessor 配置项"+keyName+"原值=["+origalValue+"]已完成解密");
            map.put(keyName, padStr);
        }else {
            this.logger.warn("EnvironmentPostProcessor 配置项"+keyName+"值=["+origalValue+"]不满足密码密文的长度及大小写要求(视为明文,不解密),保持不变");
        }
    }

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        this.logger.debug("EnvironmentPostProcessor before PropertySources size=" + environment.getPropertySources().size());
        this.logger.debug("EnvironmentPostProcessor before PropertySources : " + environment.getPropertySources());
        Map<String, Object> map = new HashMap<>(); //用于存放新值

        decodePwd(environment, "spring.cloud.nacos.password", map);

        if(!map.isEmpty()) {
            // 若有map有值,则把该map作为PropertySource加入列表中,以实现:把environment中对应key的value覆盖为新值
            // 必须加到First并且不能存在两个相同的Name的MapPropertySource,值覆盖才能生效
            environment.getPropertySources().addFirst(new MapPropertySource("afterDecodePassword", map));
        }
        this.logger.debug("EnvironmentPostProcessor after PropertySources size=" + environment.getPropertySources().size());
        this.logger.debug("EnvironmentPostProcessor after PropertySources : " + environment.getPropertySources());
    }

}

四、总结

      通过以上两种方式,可解决Spring各类配置文件对配置密文的适配和处理。

      同时不仅仅用于密文,凡是需对配置文件的内容在启动时进行改变情况都可以按以上方式进行处理。例如启动时对配置项值中多个IP进行动态使用等情形。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2274092.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Golang笔记:使用net包进行TCP监听回环测试

文章目录 前言TCP监听回环代码演示 附&#xff1a;UDP监听回环 前言 TCP是比较基础常用的网络通讯方式&#xff0c;这篇文章将使用Go语言实现TCP监听回环测试。 本文中使用 Packet Sender 工具进行测试&#xff0c;其官网地址如下&#xff1a; https://packetsender.com/ TC…

SSL 证书格式和证书文件扩展名:完整指南

SSL 证书是什么以及它如何工作相当容易理解。但当涉及到在服务器上安装它时&#xff0c;有时&#xff0c;你可能觉得这是在处理火箭科学。 由于有如此多的SSL 证书格式与特定服务器要求相关&#xff0c;您更有可能感到困惑和沮丧&#xff0c;而不是从一开始就正确配置证书。但…

【源码+文档+调试讲解】项目申报小程序

摘 要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#xff0c;各行各业相继进入信息管理时代…

echarts横向柱状图胶囊

echarts配置项 tooltip: {trigger: axis, // 触发tooltip提示类型 axis:坐标轴触发axisPointer: {type: cross, // 指示器类型 cross: 十字准星指示器crossStyle: {color: #999 // 线颜色}} }, grid: { left: 0%, //离容器左侧的距离top:5%,bottom: 3%,containLabel: true…

人工智能的发展领域之GPU加速计算的应用概述、架构介绍与教学过程

文章目录 一、架构介绍GPU算力平台概述优势与特点 二、注册与登录账号注册流程GPU服务器类型配置选择指南内存和存储容量网络带宽CPU配置 三、创建实例实例创建步骤镜像选择与设置 四、连接实例SSH连接方法远程桌面配置 一、架构介绍 GPU算力平台概述 一个专注于GPU加速计算的…

Redis Exporter 安装与配置指南(v1.67.0)

&#x1f680; 1. 下载 Redis Exporter 首先&#xff0c;登录到目标服务器&#xff0c;下载 Redis Exporter v1.67.0 安装包。 wget https://github.com/oliver006/redis_exporter/releases/download/v1.67.0/redis_exporter-v1.67.0.linux-amd64.tar.gz&#x1f4e6; 2. 解压…

WD5105同步降压转换器:9.2V-95V宽电压输入,4.5A大电流输出,95%高效率,多重保护功能

概述 • WD5105同步降压转换器 • 封装形式&#xff1a;QFN-20封装 • 应用场景&#xff1a;适用于车载充电器、电动车仪表、电信基站电源、电源适配器等 性能特点 • 输入电压范围&#xff1a;9.2V至95V • 输出电流&#xff1a;可提供4.5A连续负载电流 • 效率&#xff1a;高…

Laravel 新 WebSocket 服务 Reverb 使用指南

旧篇 > Laravel/Lumen 中使用 Echo Socket.IO-Client 实现网页即时通讯广播 https://blog.csdn.net/maxsky/article/details/130394420 已过时 与时俱进&#xff0c;Laravel 官方在 2024 年 7 月发布了 laravel/reverb 包的正式版&#xff0c;因为之前使用的 laravel-echo-…

什么是Kafka?有什么主要用途?

大家好&#xff0c;我是锋哥。今天分享关于【什么是Kafka&#xff1f;有什么主要用途&#xff1f;】面试题。希望对大家有帮助&#xff1b; 什么是Kafka&#xff1f;有什么主要用途&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Kafka 是一个分布式流…

Python爬虫基础——认识网页结构(各种标签的使用)

1、添加<div>标签的代码定义了两个区块的宽度和高度均为100px&#xff0c;边框的格式也相同&#xff0c;只是区块中显示的内容不同&#xff1b; 2、添加<ul>和<ol>标签分别用于定义无序列表和有序列表。<il>标签位于<ul>标签或<ol>标签之…

数据结构:LinkedList与链表—面试题(三)

目录 1、移除链表元素 2、反转链表 3、链表的中间结点 4、返回倒数第k个结点 5、合并两个有序链表 1、移除链表元素 习题链接https://leetcode.cn/problems/remove-linked-list-elements/description/ 描述&#xff1a;给你一个链表的头节点 head 和一个整数 val &#xff…

QT实现 端口扫描暂停和继续功能 3

上篇QT给端口扫描工程增加线程2-CSDN博客 为按钮pushButton_Stop添加clicked事件&#xff0c;功能为暂停扫描&#xff0c;并在暂停后显示继续按钮&#xff0c;点击继续按钮之后继续扫描 1.更新UI 添加继续按钮 点击转到槽则会自动声明 2. 更新 MainWindow.h 需要新增的部分…

LabVIEW瞬变电磁接收系统

利用LabVIEW软件与USB4432采集卡开发瞬变电磁接收系统。系统通过改进硬件配置与软件编程&#xff0c;解决了传统仪器在信噪比低和抗干扰能力差的问题&#xff0c;实现了高精度的数据采集和处理&#xff0c;特别适用于地质勘探等领域。 ​ 项目背景&#xff1a; 瞬变电磁法是探…

左神算法基础巩固--3

文章目录 二叉树二叉树的遍历先序遍历中序遍历后序遍历 解答二叉树的宽度优先遍历 在这里插入图片描述 一颗完全二叉树具有以下特征&#xff1a;1.不存在任何一个节点具有右子树但不存在左子树.2.不存在任何一个节点在满足1的情况下左右子树不全且其后续节点不为叶子节点 根据以…

高山旅游景区有效降低成本,无人机山下到山上物资吊运技术详解

在高山旅游景区&#xff0c;传统的物资运输方式往往面临人力成本高昂、效率低下等问题&#xff0c;而无人机技术的引入为这一难题提供了新的解决方案。以下是对无人机从山下到山上进行物资吊运技术的详细解析&#xff1a; 一、无人机物资吊运技术的优势 1. 降低人力成本&#…

APP上架之Android 证书 MD5 指纹

Android 证书 MD5 指纹 1. 什么是 Android 证书 MD5 指纹&#xff1f; Android 证书 MD5 指纹是对证书数据进行 MD5 哈希运算后得到的 128 位字符串。在 Android 开发中&#xff0c;每个证书在理论上都有一个唯一的 MD5 指纹&#xff0c;用于识别和验证证书的有效性。证书指纹…

用户界面的UML建模11

然而&#xff0c;在用户界面方面&#xff0c;重要的是要了解《boundary》类是如何与这个异常分层结构进行关联的。 《exception》类的对象可以作为《control》类的对象。因此&#xff0c;《exception》类能够聚合《boundary》类。 参见图12&#xff0c;《exception》Database…

网络安全-XSS跨站脚本攻击(基础篇)

漏洞扫描的原理 1.跨站脚本攻击介绍 xss跨站脚本攻击&#xff1a; xSS 全称&#xff08;Cross site Scripting &#xff09;跨站脚本攻击&#xff0c;是最常见的Web应用程序安全漏洞之一&#xff0c;位于OWASP top 10 2013/2017年度分别为第三名和第七名&#xff0c;XSS是指攻…

CODESYS MODBUS TCP通信(禾川Q1 PLC作为MODBUS TCP从站)

禾川Q1 PLC MODBUS TCP 通信(PLC作为MODBUS TCP通信主站) 禾川Q1 PLC MODBUS TCP通信(CODESYS平台完整配置+代码)-CSDN博客文章浏览阅读28次。MATLAB和S7-1200PLC水箱液位高度PID控制联合仿真(MODBUSTCP通信)_将matlab仿真导入plc-CSDN博客文章浏览阅读722次。本文详细介绍了如…

golang OpcUaClient

实现功能 package mainimport ("fmt""log""opcuaclient/util/plugin/client/opcclient""os""os/signal""syscall" )func main() {OPCUATest()// 监听操作系统信号&#xff0c;阻塞直到接收到信号quit : make(chan…