Jetcd客戶端集成與SSL TLS認證設置

前情提要

使用JETCD-Java客戶端沒法直接使用cfssl生成的.pem受權信息(只對於私鑰信息,公鑰所需格式都是一致的).所需的KeyFile文件必須是pkcs#8格式的.key文件纔可以被netty讀取到(默認生成的是**-key.pem的私鑰信息,其文件格式是pkcs#1的格式)html

使用以下命令進行轉換

netty所需私鑰須要將pkcs#1的.pem私鑰轉換爲pkcs#8的.key格式的私鑰.java

openssl pkcs8 -topk8 -nocrypt -in client-key.pem -out client.keygit

JETCD設置SSL(準備好CA證書、客戶端證書、私鑰)

根據客戶端 ClientBuilder的方法能夠知道,咱們須要爲客戶端Client建立設置SslContext來啓動SSLgithub

import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.ClientBuilder;
import io.netty.handler.ssl.*;

public SslContext openSslContext() throws SSLException, FileNotFoundException {
    // 證書、客戶端證書、客戶端私鑰
    File trustManagerFile = ResourceUtils.getFile("classpath:ca/ca.pem");
    File keyCertChainFile = ResourceUtils.getFile("classpath:ca/reader.pem");
    File KeyFile = ResourceUtils.getFile("classpath:ca/reader.key");
    // 這裏必需要設置alpn,不然會提示ALPN must be enabled and list HTTP/2 as a supported protocol.錯誤; 這裏主要設置了傳輸協議以及傳輸過程當中的錯誤解決方式
    ApplicationProtocolConfig alpn = new ApplicationProtocolConfig(ApplicationProtocolConfig.Protocol.ALPN,
                                ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
                                ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
                                ApplicationProtocolNames.HTTP_2);
    SslContext context = SslContextBuilder
            .forClient()
            // 設置alpn
            .applicationProtocolConfig(alpn)
            // 設置使用的那種ssl實現方式
            .sslProvider(SslProvider.OPENSSL)
            // 設置ca證書
            .trustManager(trustManagerFile)
            // 設置客戶端證書
            .keyManager(keyCertChainFile, KeyFile)
            .build();
    return context;
}

public Client etcdClient() throws SSLException, FileNotFoundException {
    ClientBuilder builder = Client.builder();
    // 設置服務器地址,這裏是列表
    builder.endpoints(etcdProps.getServerAddr().split(StringPool.COMMA));
    // 當服務器端開啓ssl認證時則該地方的設置就沒有意義了.etcd會使用客戶端ca證書中的CN頭做爲用戶名進行權限認證
    if (etcdProps.getAuthority()) {
        ByteSequence user = ByteSequence.from("username");
        ByteSequence pwd = ByteSequence.from("password");

        builder.user(user);
        builder.password(pwd);
    }
    // 這個authority必填.是服務器端CA設置的可受權訪問的host域名之一.
    // https訪問網站的時候,最重要的一環就是驗證服務器方的證書的域名是否與我想要訪問的域名一致(可查看ETCD概念入門文章瞭解CA證書生成)
    builder.sslContext(openSslContext())
            .authority("etcdcluster.com");
    return builder.build();
}
複製代碼

POM依賴

<dependency>
    <groupId>io.etcd</groupId>
    <artifactId>jetcd-core</artifactId>
    <version>0.5.0</version>
    <exclusions>
        <exclusion>
            <artifactId>netty-handler</artifactId>
            <groupId>io.netty</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-tcnative-boringssl-static</artifactId>
    <version>2.0.26.Final</version> <!-- See table for correct version -->
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-handler</artifactId>
    <version>4.1.42.Final</version>
</dependency>
複製代碼

ETCDTOOL

import com.baomidou.mybatisplus.core.toolkit.StringPool;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.Txn;
import io.etcd.jetcd.kv.GetResponse;
import io.etcd.jetcd.kv.PutResponse;
import io.etcd.jetcd.kv.TxnResponse;
import io.etcd.jetcd.op.Op;
import io.etcd.jetcd.options.DeleteOption;
import io.etcd.jetcd.options.GetOption;
import io.etcd.jetcd.options.PutOption;
import lombok.SneakyThrows;
import org.springframework.util.CollectionUtils;

import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

/** * ETCD操做工具類 * <p> * String key = "/myapp/database/user"; * String value = "Reuben"; * if (EtcdTool.put(key, value)) { * System.out.println(EtcdTool.getSingle(key)); * } * </p> */
public class EtcdTool {

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

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

    /** * 毫秒 * * @param timeout */
    public static void setTimeOut(long timeout) {
        EtcdTool.TIME_OUT = timeout;
    }


    /** * ETCD設置值傳遞給客戶端須要ByteSequence類型對象才能夠 * * @param val 欲轉換的值 : 能夠爲Key或者Value * @return */
    public static ByteSequence bytesOf(String val) {
        return ByteSequence.from(val.getBytes(StandardCharsets.UTF_8));
    }

    public static String toString(ByteSequence byteSequence) {
        return byteSequence.toString(StandardCharsets.UTF_8);
    }

    /** * 判斷當前Key是否存在 * * @param key * @return */
    @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;
    }

    /** * 設置指定K-V * * @param key * @param value * @return */
    @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;
    }

    /** * 獲取指定Key的值 * * @param key * @return */
    @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 && response.getCount() > 0) {
            return response.getKvs().get(0).getValue().toString(StandardCharsets.UTF_8);
        } else {
            return null;
        }
    }

    /** * 獲取指定Key前綴的KV映射表 * * @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 && response.getCount() > 0) {
            response.getKvs().forEach(item -> kvMap.put(toString(item.getKey()), toString(item.getValue())));
        }
        return kvMap;
    }

    /** * 刪除指定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;
    }

    /** * 刪除指定前綴的Key,返回刪除的數量 */
    @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;
    }

    /** * 開啓事務進行批量增刪改操做(發佈/回滾操做必定要開啓事務執行批量操做) */
    @SneakyThrows
    public static boolean operationWithTxn(List<String> delKeys, Map<String, String> addOrUpdateKV, String keyPrefix) {
        Txn txn = client.getKVClient().txn();
        if (!CollectionUtils.isEmpty(delKeys)) {
            List<Op.DeleteOp> delOps = new ArrayList<>();
            delKeys.forEach(item -> {

                ByteSequence bsKey = bytesOf(keyPrefix.concat(StringPool.SLASH).concat(item));
                Op.DeleteOp delOp = Op.delete(bsKey, DeleteOption.DEFAULT);

                delOps.add(delOp);
            });
            txn.Then(delOps.toArray(new Op.DeleteOp[0]));
        }
        if (!CollectionUtils.isEmpty(addOrUpdateKV)) {
            Set<Map.Entry<String, String>> entries = addOrUpdateKV.entrySet();
            List<Op.PutOp> addOrUpdateOps = new ArrayList<>();
            for (Map.Entry<String, String> item : entries) {
                ByteSequence bsKey = bytesOf(keyPrefix.concat(StringPool.SLASH).concat(item.getKey()));
                ByteSequence bsVal = bytesOf(item.getValue());
                Op.PutOp putOp = Op.put(bsKey, bsVal, PutOption.DEFAULT);

                addOrUpdateOps.add(putOp);
            }

            txn.Then(addOrUpdateOps.toArray(new Op.PutOp[0]));
        }
        TxnResponse txnResponse = txn.commit().get(TIME_OUT, TIME_UNIT);

        return txnResponse.isSucceeded();

    }

}
複製代碼

錯誤提示

  1. 未設置ApplicationProtocolConfig會提示ALPN must be enabled and list HTTP/2 as a supported protocol錯誤
  2. 未處理好grpc-netty、netty-handler、netty-tcnative-boringssl-static依賴版本兼容性(使用jetcd-core依賴時),會提示各類錯誤例如java.lang.NoSuchFieldError: SSL_SESS_CACHE CLIENTjava.lang.ClassNotFoundException: io.netty.internal.tcnative.SSLContext等.能夠拉到最下面可查看版本兼容信息
  3. 提示未找到匹配名稱: No name matching "etcd" found. Jetcd默認設置的DNS名稱時etcd,可是咱們須要在服務器端設置該host才能夠,不然沒法找到對應的IP地址,若是咱們能夠設置builder.authority("定義的服務器端CA地址DNS/IP")來解決這個問題
  4. 別忘記設置SslContext的keyManager中的客戶端證書和私鑰.否則請求回來的信息沒法轉換讀取,也會報錯javax.net.ssl.SSLHandshakeException: error:10000412:SSL routines:OPENSSL_internal:SSLV3_ALERT_BAD_CERTIFICATEio.grpc.StatusRuntimeException: UNAVAILABLE: io exception Channel Pipeline: [SslHandler#0, ProtocolNegotiators$ClientTlsHandler#0, WriteBufferingAndExceptionHandler#0, DefaultChannelPipeline$TailContext#0]

參考文檔:

官方DemoSSL和TLS介紹etcd-TLS攻略常見的PKI標準(X.50九、PKCS)及證書相關介紹X.50九、PKCS文件格式介紹官方SSL示例etcd配置支持SSL+ACLspring

相關文章
相關標籤/搜索