使用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.key
git
根據客戶端 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();
}
複製代碼
<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>
複製代碼
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();
}
}
複製代碼
ALPN must be enabled and list HTTP/2 as a supported protocol
錯誤java.lang.NoSuchFieldError: SSL_SESS_CACHE CLIENT
、java.lang.ClassNotFoundException: io.netty.internal.tcnative.SSLContext
等.能夠拉到最下面可查看版本兼容信息No name matching "etcd" found
. Jetcd默認設置的DNS名稱時etcd,可是咱們須要在服務器端設置該host才能夠,不然沒法找到對應的IP地址,若是咱們能夠設置builder.authority("定義的服務器端CA地址DNS/IP")
來解決這個問題javax.net.ssl.SSLHandshakeException: error:10000412:SSL routines:OPENSSL_internal:SSLV3_ALERT_BAD_CERTIFICATE
、io.grpc.StatusRuntimeException: UNAVAILABLE: io exception Channel Pipeline: [SslHandler#0, ProtocolNegotiators$ClientTlsHandler#0, WriteBufferingAndExceptionHandler#0, DefaultChannelPipeline$TailContext#0]
官方Demo、SSL和TLS介紹、etcd-TLS攻略、常見的PKI標準(X.50九、PKCS)及證書相關介紹、X.50九、PKCS文件格式介紹、官方SSL示例、etcd配置支持SSL+ACLspring