Spring Cloud integrates etcd and sets up SSL

1. Introduction to etcd

etcd is implemented based on the go language. It is mainly a component for configuration sharing and service discovery. It was originally used to solve problems such as distributed concurrency control, storage and division of configuration files during OS upgrades in cluster management systems. Therefore, etcd is a component that provides strong consistency and high availability in a distributed system and is used to store a small amount of important data.

2. Practice

The base versions used by the project are:

Spring Cloud Alibaba version 2021.0.1.0

Spring Boot version 2.6.13

Spring Cloud version 2021.0.1

1.POM dependency
<dependency>
    <groupId>io.etcd</groupId>
    <artifactId>jetcd-core</artifactId>
    <version>0.5.0</version>
</dependency>
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-tcnative-boringssl-static</artifactId>
    <version>2.0.54.Final</version>
</dependency>
2.etcd tool class
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.kv.GetResponse;
import io.etcd.jetcd.kv.PutResponse;
import io.etcd.jetcd.options.DeleteOption;
import io.etcd.jetcd.options.GetOption;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import lombok. SneakyThrows;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ResourceUtils;

import javax.net.ssl.SSLException;
import java.io.File;
import java.io.FileNotFoundException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

/**
 * etcd configuration
 *
 * @author jiangxiaoyi
 * @create 2023/03/09 10:56
 */
@Configuration
public class EtcdUtil {

    private static Client client;
    private static long TIME_OUT = 10000L;
    private static TimeUnit TIME_UNIT = TimeUnit. MILLISECONDS;

    public static void setClient(Client client) {
        EtcdUtil.client = client;
    }

    /**
     * milliseconds
     *
     * @param timeout expiration time
     */
    public static void setTimeOut(long timeout) {
        EtcdUtil.TIME_OUT = timeout;
    }


    /**
     * Convert String to ByteSequence type object
     *
     * @param val The value to be converted: can be Key or Value
     * @return ByteSequence object
     */
    public static ByteSequence bytesOf(String val) {
        return ByteSequence.from(val.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * Convert ByteSequence type object to String
     *
     * @param byteSequence ByteSequence object
     * @return String type
     */
    public static String toString(ByteSequence byteSequence) {
        return byteSequence.toString(StandardCharsets.UTF_8);
    }

    /**
     * Determine whether the current Key exists
     *
     * @param key key
     * @return true: yes false: no
     */
    @SneakyThrows
    public static Boolean hvKey(String key) {
        if (null == key || "".equals(key)) {
            return false;
        }
        ByteSequence byteKey = bytesOf(key);
        GetResponse response = client.getKVClient().get(byteKey).get(TIME_OUT, TIME_UNIT);
        return response. getCount() > 0;
    }

    /**
     * Set specified K-V
     *
     * @param key key
     * @param value value
     * @return true: success false: failure
     */
    @SneakyThrows
    public static Boolean put(String key, String value) {
        if (null == key || "".equals(key)) {
            throw new NullPointerException();
        }
        CompletableFuture<PutResponse> future = client.getKVClient().put(bytesOf(key), bytesOf(value));
        PutResponse response = future. get(TIME_OUT, TIME_UNIT);
        return null != response;
    }

    /**
     * Get the value of the specified Key
     *
     * @param key key
     * @return get the value
     */
    @SneakyThrows
    public static String getSingle(String key) {
        if (null == key || "".equals(key)) {
            throw new NullPointerException();
        }
        ByteSequence byteKey = bytesOf(key);
        GetResponse response = client.getKVClient().get(byteKey).get(TIME_OUT, TIME_UNIT);
        if (null != response & amp; & amp; response. getCount() > 0) {
            return response.getKvs().get(0).getValue().toString(StandardCharsets.UTF_8);
        } else {
            return null;
        }
    }

    /**
     * Get the KV mapping table of the specified Key prefix
     *
     * @param prefix
     * @return
     */
    @SneakyThrows
    public static Map<String, String> getWithPrefix(String prefix) {
        if (null == prefix || "".equals(prefix)) {
            throw new NullPointerException();
        }

        ByteSequence prefixByte = bytesOf(prefix);
        GetOption getOption = GetOption.newBuilder().withPrefix(prefixByte).build();
        GetResponse response = client.getKVClient().get(prefixByte, getOption).get(TIME_OUT, TIME_UNIT);

        Map<String, String> kvMap = new HashMap<>();
        if (null != response & amp; & amp; response. getCount() > 0) {
            response.getKvs().forEach(item -> kvMap.put(toString(item.getKey()), toString(item.getValue())));
        }
        return kvMap;
    }

    /**
     * Delete the specified Key
     */
    @SneakyThrows
    public static Boolean delSingle(String key) {
        if (null == key || "".equals(key)) {
            throw new NullPointerException();
        }
        long deleted = client.getKVClient().delete(bytesOf(key)).get(TIME_OUT, TIME_UNIT).getDeleted();
        return deleted > 0;
    }

    /**
     * Delete the Key with the specified prefix, and return the number of deletions
     */
    @SneakyThrows
    public static long delWithPrefix(String prefix) {
        if (null == prefix || "".equals(prefix)) {
            throw new NullPointerException();
        }
        ByteSequence prefixByte = bytesOf(prefix);
        DeleteOption deleteOption = DeleteOption.newBuilder().withPrefix(prefixByte).build();
        long deleted = client.getKVClient().delete(prefixByte, deleteOption).get(TIME_OUT, TIME_UNIT).getDeleted();
        return deleted;
    }
}
3. Connection instance (username + password)
public void pushDataToEtcd(Object pushData, String requestId) {
        var endpoints = applicationConfig.getEtcdUrl().split(",");
        String user = "user";
        String passwd = "password";
        String key = "/etcd/key";
        String value = JSONUtil.toJsonStr(pushData);
        log.info("Push etcd data key: " + key + " value: " + value);
        Client etcdClient =
            Client.builder().endpoints(endpoints).user(EtcdUtil.bytesOf(user)).password(EtcdUtil.bytesOf(passwd)).build();
        EtcdUtil.setClient(etcdClient);
        if (!EtcdUtil. put(key, value)) {
            // Push failure handling
        }
    }
4. Connection instance (username + password + SSL authentication)

Set up SSL (prepare CA certificate, client certificate, private key), the required private key file must be a .key file in pkcs#8 format to be read by netty (the default generated is **-key.pem private key information, its file format is pkcs#1 format)

The format of the secret key generated directly by RSA without conversion is: —–BEGIN RSA PRIVATE KEY—–

The key format of the .key file in pkcs#8 format is —-BEGIN PRIVATE KEY—–, the private key in java is generally in this format, but the public key does not need to be converted.

Conversion command: openssl pkcs8 -topk8 -nocrypt -in client-key.pem -out client.key

public void pushDataToEtcdBySSL(Object pushData) throws FileNotFoundException, SSLException {
        var endpoints = applicationConfig.getEtcdUrl().split(",");
        String user = "user";
        String passwd = "password";
        String key = "/etcd/key";
        String value = JSONUtil.toJsonStr(pushData);
        log.info("Push etcd data key: " + key + " value: " + value);
        SslContext sslContext = EtcdUtil. openSslContext();
        Client etcdClient =
            Client.builder().endpoints(endpoints).user(EtcdUtil.bytesOf(user)).password(EtcdUtil.bytesOf(passwd))
                .sslContext(sslContext).authority(applicationConfig.getEtcdIp()).build();
        EtcdUtil.setClient(etcdClient);
        if (!EtcdUtil. put(key, value)) {
            // Push failure handling
        }
    }

public static SslContext openSslContext() throws SSLException, FileNotFoundException {
        // certificate, client certificate, client private key
        File trustManagerFile = ResourceUtils.getFile("classpath:ca.crt");
        File keyCertChainFile = ResourceUtils.getFile("classpath:client.crt");
        File KeyFile = ResourceUtils.getFile("classpath:client.key");
        // Alpn must be set here, otherwise it will prompt ALPN must be enabled and list HTTP/2 as a supported protocol. error; here mainly set the transmission protocol and the error solution during the transmission process
        ApplicationProtocolConfig alpn = new ApplicationProtocolConfig(ApplicationProtocolConfig. Protocol. ALPN,
            ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
            ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, ApplicationProtocolNames.HTTP_2);
        SslContext context = SslContextBuilder.forClient()
            // set alpn
            .applicationProtocolConfig(alpn)
            // Set the ssl implementation used
            .sslProvider(SslProvider. OPENSSL)
            // set ca certificate
            .trustManager(trustManagerFile)
            // set client certificate
            .keyManager(keyCertChainFile, KeyFile).
            build();
        return context;
    }

Note: If an error is reported: No matching name found: No name matching “etcd” found. The corresponding IP address cannot be found, which can be solved by .authority(“Defined server-side CA address DNS/IP”), the same It can also be solved by setting the host on the server side. This is because the DNS name set by default for Jetcd is etcd.

The knowledge points of the article match the official knowledge files, and you can further learn relevant knowledge. Network skill treeHomepageOverview 28749 people are studying systematically