当前位置: 首页 > article >正文

【工作记录】springboot应用中使用Jasypt 加密配置文件@20241216

前言

在现代软件开发中,保护敏感信息(如数据库密码、API 密钥等)是非常重要的。Spring Boot 提供了多种方式来管理配置文件,但默认情况下,这些配置文件是以明文形式存储的。为了提高安全性,我们可以使用 Jasypt(Java Simplified Encryption)来加密配置文件中的敏感信息。

Jasypt 介绍

Jasypt 是一个 Java 库,旨在简化加密操作。它提供了对称加密、非对称加密、数字签名等多种加密功能,并且可以很容易地与 Spring Boot 集成。

Jasypt具有如下特点:

  • 简单易用:Jasypt 提供了简单的 API,使得加密和解密操作变得非常容易。
  • 与 Spring Boot 集成良好:通过 Jasypt 的 Spring Boot Starter,可以轻松地将加密功能集成到 Spring Boot 应用中。
  • 支持多种加密算法:Jasypt 支持多种加密算法,可以根据需求选择合适的算法。

与springboot集成

集成目标

  1. 引入jasypt,实现通过controller加密指定字符串
  2. 通过配置配置文件来验证加密字符串的读取和解密
  3. 引入mysql数据库配置文件,对密码进行加密后进行数据库的链接和操作。

集成步骤

新建项目

新建springboot + maven项目, 引入如下依赖:

<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot-starter</artifactId>
    <version>3.0.5</version>
</dependency>

编写controller

新建controller, 示例如下:

package com.zjtx.tech.sample.samplejasypt.controller;

import com.ulisesbocchio.jasyptspringboot.properties.JasyptEncryptorConfigurationProperties;
import com.ulisesbocchio.jasyptspringboot.util.Singleton;
import jakarta.annotation.Resource;
import org.jasypt.intf.service.JasyptStatelessService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@ResponseBody
public class JasyptController implements InitializingBean {

    @Resource
    private Singleton<JasyptEncryptorConfigurationProperties> configPropsSingleton;

    private static JasyptEncryptorConfigurationProperties config;

    @Value("${example.value:'123'}")
    private String exampleVal;

    @GetMapping("encrypt")
    public String encrypt(String source){
        System.out.println("加密前的数据:" + source);
        String result = doEncrypt(source);
        System.out.println("加密后的结果:" + result);
        return source + "-------加密后---------" + result;
    }

    @GetMapping("getValue")
    public String getDecryptValue() {
        return exampleValue;
    }

    private String doEncrypt(String source) {
        JasyptStatelessService service = new JasyptStatelessService();
        return service.encrypt(
                source,
                config.getPassword(), null, null,
                config.getAlgorithm(), null, null,
                config.getKeyObtentionIterations(), null, null,
                config.getSaltGeneratorClassname(), null, null, null,
                config.getProviderName(), null,
                config.getProviderClassName(), null, null,
                config.getStringOutputType(), null, null,
                config.getIvGeneratorClassname(), null, null);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        config = configPropsSingleton.get();
    }
}

说明如下:

  1. Singleton<JasyptEncryptorConfigurationProperties>是jar包中带的一个默认就存在的Bean,可参考如下源码:

    /**
     * <p>configProps.</p>
     * @param envCopy a {@link com.ulisesbocchio.jasyptspringboot.configuration.EnvCopy} object
     * @return a {@link com.ulisesbocchio.jasyptspringboot.util.Singleton} object
     */
    @Bean(name = CONFIG_SINGLETON)
    public Singleton<JasyptEncryptorConfigurationProperties> configProps(
        final EnvCopy envCopy) {
        return new Singleton<>(() -> JasyptEncryptorConfigurationProperties.bindConfigProps(envCopy.get()));
    }
    

    该源码位于com.ulisesbocchio.jasyptspringboot.configuration.EncryptablePropertyResolverConfiguration类中, bean的名称要求必须是configPropsSingleton(如果通过名称来获取Bean的话),有兴趣可以研究下。

  2. JasyptStatelessService是源码中提供的一个类,可以用于加解密使用。

  3. 加密方法JasyptStatelessService.encrypt有很多入参,仔细观察分类后可以发现关键参数就9个,部分参数提供了多种配置方式(直接传入、环境变量、系统属性, 后两种方式只需要提供变量名称或者属性名称即可),导致参数看起来有很多。

    原始方法定义如下:

    public String encrypt(
                final String input,
                final String password,
                final String passwordEnvName,
                final String passwordSysPropertyName,
                final String algorithm,
                final String algorithmEnvName,
                final String algorithmSysPropertyName,
                final String keyObtentionIterations,
                final String keyObtentionIterationsEnvName,
                final String keyObtentionIterationsSysPropertyName,
                final String saltGeneratorClassName, 
                final String saltGeneratorClassNameEnvName,
                final String saltGeneratorClassNameSysPropertyName,
                final String providerName,
                final String providerNameEnvName,
                final String providerNameSysPropertyName,
                final String providerClassName,
                final String providerClassNameEnvName,
                final String providerClassNameSysPropertyName,
                final String stringOutputType,
                final String stringOutputTypeEnvName,
                final String stringOutputTypeSysPropertyName,
                final String ivGeneratorClassName,
                final String ivGeneratorClassNameEnvName,
                final String ivGeneratorClassNameSysPropertyName) {
        //....省略方法体
    }
    

    关于关键参数说明如下:

    参数名称参数说明备注
    input要加密的原始文本需要提供明文,字符串
    password加密时使用的盐值支持三种方式传入
    algorithm加密算法支持三种方式传入
    keyObtentionIterations密钥生成迭代次数支持三种方式传入
    saltGeneratorClassName盐值生成器全类名支持三种方式传入
    providerName安全服务提供者名称支持三种方式传入
    providerClassName安全服务提供者类的全限定名支持三种方式传入
    stringOutputType输出类型, 可选值为:base64 (default)/hexadecimal支持三种方式传入
    ivGeneratorClassName初始化向量(IV)生成器类的全限定类名支持三种方式传入

    上面配置中有个比较重要的参数就是password, 推荐使用环境变量或系统属性的方式传入

    PS: 下文为方便演示,暂且在配置文件中配置。

    1. 上述文件中的exampleValue是为了演示配置文件读取使用的,配置内容是加密后的字符串, 后文中会用到。

编写配置文件

jasypt:
  encryptor:
    password: qwer@1234

说明:

  1. jasypt提供了比较多的配置项,且基本都带有默认值,默认值可参考JasyptEncryptorConfigurationProperties类。
package com.ulisesbocchio.jasyptspringboot.properties;

import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyFilter;
import com.ulisesbocchio.jasyptspringboot.encryptor.SimpleAsymmetricConfig;
import com.ulisesbocchio.jasyptspringboot.encryptor.SimpleGCMConfig;
import com.ulisesbocchio.jasyptspringboot.util.AsymmetricCryptography.KeyFormat;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver;
import org.springframework.boot.context.properties.bind.handler.IgnoreErrorsBindHandler;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;

import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.List;

import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;

/**
 * Partially used to load {@link com.ulisesbocchio.jasyptspringboot.EncryptablePropertyFilter} config.
 *
 * @author Ulises Bocchio
 * @version $Id: $Id
 */
@SuppressWarnings("ConfigurationProperties")
@ConfigurationProperties(prefix = "jasypt.encryptor", ignoreUnknownFields = true)
@Data
public class JasyptEncryptorConfigurationProperties {

    /**
     * <p>bindConfigProps.</p>
     *
     * @param environment a {@link org.springframework.core.env.ConfigurableEnvironment} object
     * @return a {@link com.ulisesbocchio.jasyptspringboot.properties.JasyptEncryptorConfigurationProperties} object
     */
    public static JasyptEncryptorConfigurationProperties bindConfigProps(ConfigurableEnvironment environment) {
        final BindHandler handler = new IgnoreErrorsBindHandler(BindHandler.DEFAULT);
        final MutablePropertySources propertySources = environment.getPropertySources();
        final Binder binder = new Binder(ConfigurationPropertySources.from(propertySources),
                new PropertySourcesPlaceholdersResolver(propertySources),
                ApplicationConversionService.getSharedInstance());
        final JasyptEncryptorConfigurationProperties config = new JasyptEncryptorConfigurationProperties();

        final ResolvableType type = ResolvableType.forClass(JasyptEncryptorConfigurationProperties.class);
        final Annotation annotation = AnnotationUtils.findAnnotation(JasyptEncryptorConfigurationProperties.class,
                ConfigurationProperties.class);
        final Annotation[] annotations = new Annotation[]{annotation};
        final Bindable<?> target = Bindable.of(type).withExistingValue(config).withAnnotations(annotations);

        binder.bind("jasypt.encryptor", target, handler);
        return config;
    }

    /**
     * Whether to use JDK/Cglib (depending on classpath availability) proxy with an AOP advice as a decorator for
     * existing {@link org.springframework.core.env.PropertySource} or just simply use targeted wrapper Classes. Default
     * Value is {@code false}.
     */
    private Boolean proxyPropertySources = false;

    /**
     * Define a list of {@link org.springframework.core.env.PropertySource} to skip from wrapping/proxying. Properties held
     * in classes on this list will not be eligible for decryption. Default Value is {@code empty list}.
     */
    private List<String> skipPropertySources = Collections.emptyList();

    /**
     * Specify the name of bean to override jasypt-spring-boot's default properties based
     * {@link org.jasypt.encryption.StringEncryptor}. Default Value is {@code jasyptStringEncryptor}.
     */
    private String bean = "jasyptStringEncryptor";

    /**
     * Master Password used for Encryption/Decryption of properties.
     *
     * @see org.jasypt.encryption.pbe.PBEStringEncryptor
     * @see com.ulisesbocchio.jasyptspringboot.encryptor.SimpleGCMStringEncryptor
     * @see org.jasypt.encryption.pbe.config.StringPBEConfig#getPassword()
     * @see SimpleGCMConfig#getSecretKeyPassword()
     */
    private String password;

    /**
     * Encryption/Decryption Algorithm to be used by Jasypt. For more info on how to get available algorithms visit:
     * <a href="http://www.jasypt.org/cli.html"/>Jasypt CLI Tools Page</a>. Default Value is {@code "PBEWITHHMACSHA512ANDAES_256"}.
     *
     * @see org.jasypt.encryption.pbe.PBEStringEncryptor
     * @see com.ulisesbocchio.jasyptspringboot.encryptor.SimpleGCMStringEncryptor
     * @see org.jasypt.encryption.pbe.config.StringPBEConfig#getAlgorithm()
     * @see SimpleGCMConfig#getAlgorithm()
     */
    private String algorithm = "PBEWITHHMACSHA512ANDAES_256";

    /**
     * Number of hashing iterations to obtain the signing key. Default Value is {@code "1000"}.
     *
     * @see org.jasypt.encryption.pbe.PBEStringEncryptor
     * @see com.ulisesbocchio.jasyptspringboot.encryptor.SimpleGCMStringEncryptor
     * @see org.jasypt.encryption.pbe.config.StringPBEConfig#getKeyObtentionIterations()
     * @see SimpleGCMConfig#getSecretKeyIterations()
     */
    private String keyObtentionIterations = "1000";

    /**
     * The size of the pool of encryptors to be created. Default Value is {@code "1"}.
     *
     * @see org.jasypt.encryption.pbe.PBEStringEncryptor
     * @see org.jasypt.encryption.pbe.config.StringPBEConfig#getPoolSize()
     */
    private String poolSize = "1";

    /**
     * The name of the {@link java.security.Provider} implementation to be used by the encryptor for obtaining the
     * encryption algorithm. Default Value is {@code null}.
     *
     * @see org.jasypt.encryption.pbe.PBEStringEncryptor
     * @see org.jasypt.encryption.pbe.config.StringPBEConfig#getProviderName()
     */
    private String providerName = null;

    /**
     * The class name of the {@link java.security.Provider} implementation to be used by the encryptor for obtaining the
     * encryption algorithm. Default Value is {@code null}.
     *
     * @see org.jasypt.encryption.pbe.PBEStringEncryptor
     * @see org.jasypt.encryption.pbe.config.SimpleStringPBEConfig#setProviderClassName(String)
     */
    private String providerClassName = null;

    /**
     * A {@link org.jasypt.salt.SaltGenerator} implementation to be used by the encryptor. Default Value is
     * {@code "org.jasypt.salt.RandomSaltGenerator"}.
     *
     * @see org.jasypt.encryption.pbe.PBEStringEncryptor
     * @see org.jasypt.encryption.pbe.config.StringPBEConfig#getSaltGenerator()
     */
    private String saltGeneratorClassname = "org.jasypt.salt.RandomSaltGenerator";

    /**
     * A {@link org.jasypt.iv.IvGenerator} implementation to be used by the encryptor. Default Value is
     * {@code "org.jasypt.iv.RandomIvGenerator"}.
     *
     * @see org.jasypt.encryption.pbe.PBEStringEncryptor
     * @see org.jasypt.encryption.pbe.config.StringPBEConfig#getIvGenerator()
     */
    private String ivGeneratorClassname = "org.jasypt.iv.RandomIvGenerator";

    /**
     * Specify the form in which String output will be encoded. {@code "base64"} or {@code "hexadecimal"}. Default Value
     * is {@code "base64"}.
     *
     * @see org.jasypt.encryption.pbe.PBEStringEncryptor
     * @see org.jasypt.encryption.pbe.config.StringPBEConfig#getStringOutputType()
     */
    private String stringOutputType = "base64";

    /**
     * Specify a PEM/DER base64 encoded string. PEM encoded keys can simply omit the "BEGIN/END PRIVATE KEY" header/footer
     * and just specify the base64 encoded key. This property takes precedence over {@link #setPrivateKeyLocation(String)}
     *
     * @see com.ulisesbocchio.jasyptspringboot.encryptor.SimpleAsymmetricStringEncryptor
     * @see SimpleAsymmetricConfig#getPrivateKey()
     */
    private String privateKeyString = null;

    /**
     * Specify a PEM/DER private key location, in Spring's resource nomenclature (i.e. classpath:resource/path or file://path/to/file)
     *
     * @see com.ulisesbocchio.jasyptspringboot.encryptor.SimpleAsymmetricStringEncryptor
     * @see SimpleAsymmetricConfig#getPrivateKeyLocation()
     */
    private String privateKeyLocation = null;

    /**
     * Specify the private key format to use: DER (default) or PEM
     *
     * @see com.ulisesbocchio.jasyptspringboot.encryptor.SimpleAsymmetricStringEncryptor
     * @see SimpleAsymmetricConfig#getPrivateKeyFormat()
     */
    private KeyFormat privateKeyFormat = KeyFormat.DER;

    /**
     * Specify a PEM/DER base64 encoded string. PEM encoded keys can simply omit the "BEGIN/END PUBLIC KEY" header/footer
     * and just specify the base64 encoded key. This property takes precedence over {@link #setPrivateKeyLocation(String)}
     *
     * @see com.ulisesbocchio.jasyptspringboot.encryptor.SimpleAsymmetricStringEncryptor
     * @see SimpleAsymmetricConfig#getPublicKey()
     */
    private String publicKeyString = null;

    /**
     * Specify a PEM/DER public key location, in Spring's resource nomenclature (i.e. classpath:resource/path or file://path/to/file)
     *
     * @see com.ulisesbocchio.jasyptspringboot.encryptor.SimpleAsymmetricStringEncryptor
     * @see SimpleAsymmetricConfig#getPublicKeyLocation()
     */
    private String publicKeyLocation = null;

    /**
     * Specify the public key format to use: DER (default) or PEM
     *
     * @see com.ulisesbocchio.jasyptspringboot.encryptor.SimpleAsymmetricStringEncryptor
     * @see SimpleAsymmetricConfig#getPublicKeyFormat()
     */
    private KeyFormat publicKeyFormat = KeyFormat.DER;

    /**
     * Specify a secret key String in base64 for the GCM Algorithm
     *
     * @see com.ulisesbocchio.jasyptspringboot.encryptor.SimpleGCMStringEncryptor
     * @see SimpleGCMConfig#getSecretKey()
     */
    private String gcmSecretKeyString = null;

    /**
     * Specify a secret key resource location in base64 for the GCM Algorithm
     *
     * @see com.ulisesbocchio.jasyptspringboot.encryptor.SimpleGCMStringEncryptor
     * @see SimpleGCMConfig#getSecretKeyLocation()
     */
    private String gcmSecretKeyLocation = null;

    /**
     * Specify a password for the GCM Algorithm
     *
     * @see com.ulisesbocchio.jasyptspringboot.encryptor.SimpleGCMStringEncryptor
     * @see SimpleGCMConfig#getSecretKeyPassword()
     */
    private String gcmSecretKeyPassword = null;

    /**
     * Specify a salt base64 String when using GCM encryption when used with master password
     *
     * @see com.ulisesbocchio.jasyptspringboot.encryptor.SimpleGCMStringEncryptor
     * @see SimpleGCMConfig#getSecretKeySalt()
     */
    private String gcmSecretKeySalt = null;

    /**
     * Specify the class names of extra {@link org.springframework.context.ApplicationEvent} events that should trigger a property cache refresh.
     * @see com.ulisesbocchio.jasyptspringboot.caching.RefreshScopeRefreshedEventListener
     */
    private List<String> refreshedEventClasses = emptyList();

    /**
     * Specify an algorithm for the secret key when used with master password
     *
     * @see com.ulisesbocchio.jasyptspringboot.encryptor.SimpleGCMStringEncryptor
     * @see SimpleGCMConfig#getSecretKeyAlgorithm() ()
     */
    private String gcmSecretKeyAlgorithm = "PBKDF2WithHmacSHA256";

    @NestedConfigurationProperty
    private PropertyConfigurationProperties property = new PropertyConfigurationProperties();

    /**
     * <p>getKeyObtentionIterationsInt.</p>
     *
     * @return a int
     */
    public int getKeyObtentionIterationsInt() {
        return Integer.parseInt(keyObtentionIterations);
    }

    @Data
    public static class PropertyConfigurationProperties {

        /**
         * Specify the name of the bean to be provided for a custom
         * {@link com.ulisesbocchio.jasyptspringboot.EncryptablePropertyDetector}. Default value is
         * {@code "encryptablePropertyDetector"}
         */
        private String detectorBean = "encryptablePropertyDetector";

        /**
         * Specify the name of the bean to be provided for a custom
         * {@link com.ulisesbocchio.jasyptspringboot.EncryptablePropertyResolver}. Default value is
         * {@code "encryptablePropertyResolver"}
         */
        private String resolverBean = "encryptablePropertyResolver";

        /**
         * Specify the name of the bean to be provided for a custom {@link EncryptablePropertyFilter}. Default value is
         * {@code "encryptablePropertyFilter"}
         */
        private String filterBean = "encryptablePropertyFilter";

        /**
         * Specify a custom {@link String} to identify as prefix of encrypted properties. Default value is
         * {@code "ENC("}
         */
        private String prefix = "ENC(";

        /**
         * Specify a custom {@link String} to identify as suffix of encrypted properties. Default value is {@code ")"}
         */
        private String suffix = ")";

        @NestedConfigurationProperty
        private FilterConfigurationProperties filter = new FilterConfigurationProperties();

        @Data
        public static class FilterConfigurationProperties {

            /**
             * Specify the property sources name patterns to be included for decryption
             * by{@link EncryptablePropertyFilter}. Default value is {@code null}
             */
            private List<String> includeSources = null;

            /**
             * Specify the property sources name patterns to be EXCLUDED for decryption
             * by{@link EncryptablePropertyFilter}. Default value is {@code null}
             */
            private List<String> excludeSources = null;

            /**
             * Specify the property name patterns to be included for decryption by{@link EncryptablePropertyFilter}.
             * Default value is {@code null}
             */
            private List<String> includeNames = null;

            /**
             * Specify the property name patterns to be EXCLUDED for decryption by{@link EncryptablePropertyFilter}.
             * Default value is {@code jasypt\\.encryptor\\.*}
             */
            private List<String> excludeNames = singletonList("^jasypt\\.encryptor\\.*");
        }
    }
}

基本和encrypt参数中的参数可以对应上。

  1. 上述参数中比较重要的有keyObtentionIterationssaltGeneratorClassNameivGeneratorClassName,对应的都提供了默认的实现类。

    除了这几个参数以外,还有就是prefixsuffix这两个参数需要注意,也就是前缀和后缀。

  2. password建议配置到环境变量或者系统属性中。

启动项目,初步测试

访问http://localhost:8080/encrypt?source=123456, 查看页面返回,保存加密后的字符串文本。

获取加密数据

修改配置文件

配置文件中修改example.value

example:
  value: ENC(u9tz7memWARMc/CHEdH7o//WoEVaWQDF13nnBSZrw2ipo+p3mUuVku6ttJkofeBN)

这里的ENC对应配置中的prefix,也是该参数的默认值。prefix默认值是ENC(, suffix默认值是)

修改后重启项目,访问http://localhost:8080/getValue, 查看浏览器返回结果。

获取配置文件中的加密数据值

可以看到数据可以正常被访问到。

至此,目标1和目标2已完成。

添加mysql验证

准备mysql数据库

具体步骤略。

添加mysql依赖
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
修改配置文件
spring:
  application:
    name: sample-jasypt
  datasource:
    url: jdbc:mysql://{替换ip}:{替换端口}/{替换数据库名称}?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: ENC(u9tz7memWARMc/CHEdH7o//WoEVaWQDF13nnBSZrw2ipo+p3mUuVku6ttJkofeBN)
    driver-class-name: com.mysql.jdbc.Driver
修改主类
@SpringBootApplication
public class SampleJasyptApplication implements InitializingBean {

    public static void main(String[] args) {
        SpringApplication.run(SampleJasyptApplication.class, args);
    }
    
    @Autowired
    private DataSource dataSource;

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("dataSource = " + dataSource);
    }
}
验证结果

mysql验证结果

可以看到datasource中是可以正常读取到password解密后的值的,连接数据库自然也是没有问题的。

至此,第三个目标也完成了。

小结

本文主要介绍了springboot项目中集成jasypt实现配置文件中敏感信息加密的流程并进行了验证,希望能够帮助到需要的朋友。

针对以上内容有任何疑问或者建议欢迎留言讨论。

创作不易,欢迎一键三连~~~


http://www.kler.cn/a/445902.html

相关文章:

  • WeakAuras NES Script(lua)
  • 多协议视频监控汇聚/视频安防系统Liveweb搭建智慧园区视频管理平台
  • 在Ubuntu下运行QEMU仿真FreeBSD riscv64系统
  • C语言编程1.27汉诺塔
  • 【进程篇】操作系统
  • 【STM32 Modbus编程】-作为主设备写入多个线圈和寄存器
  • 微信小程序:轻应用的未来与无限可能
  • Fortify 24.2.0版本最新版 win/mac/linux
  • 网络和IP地址计算器方案
  • JVM 详解
  • 大数据-252 离线数仓 - Airflow 任务调度 Crontab简介 任务集成部署 入门案例
  • 中间件 redis安装
  • 洛谷 B3644 【模板】拓扑排序 / 家谱树 C语言
  • git部分命令的使用
  • Hmsc包开展群落数据联合物种分布模型分析通用流程(Pipelines)
  • 如何快速构建Jmeter脚本
  • oracle AES CBC,128位密钥加解密方法
  • 【C++ DFS 图论】1519. 子树中标签相同的节点数|1808
  • 解决 Ubuntu 20.04 上因 postmaster.pid 文件残留导致的 PostgreSQL 启动失败问题
  • L24.【LeetCode笔记】 杨辉三角
  • 如何彻底删除电脑数据以防止隐私泄露
  • 【mac 终端美化】oh my zsh
  • GTID详解
  • 【从零开始入门unity游戏开发之——C#篇21】C#面向对象的封装——`this`扩展方法、运算符重载、内部类、`partial` 定义分部类
  • 【Verilog】实验九 存储器设计与IP调用
  • 【论文复现】找出图像中物体的角点