背景
最近在實現(xiàn)一個遠程構建Docker鏡像的功能。用戶在前端頁面觸發(fā)鏡像構建后,后端服務調(diào)用遠程服務執(zhí)行構建腳本雕拼。之后垮卓,后端服務循環(huán)訪問 DockerHub 的REST接口(/v2/{image}/tags/list
),判斷目標鏡像是否已經(jīng)生成裙犹,從而更新前端構建狀態(tài)。
后端服務使用Feign訪問 DockerHub,DockerHub 接口使用的Https協(xié)議鸟蟹。
代碼實現(xiàn)
FeignClient定義
在@FeignClient
注解中指定feign client自定義配置。自定義配置中可以重寫 feign.Client使兔、feign.codec.Decoder建钥、feign.codec.Encoder、feign.Contract 的實現(xiàn)方式虐沥。這些配置僅對當前的FeignClient有效熊经。
@FeignClient(name = "docker-client", configuration = FeignHttpsConfig.class, url = "https://docker2.yidian.com:5000")
public interface DockerClient {
@GetMapping("/v2/{image}/tags/list")
ImageTags getTags(@PathVariable("image") String image);
}
FeignHttpsConfig定義
在FeignHttpsConfig中重寫了Feign.Builder
、feign.Client
欲险、Logger.Level
的配置镐依。其中指定了feign.Client 所使用的SSLSocketFactory
和HostnameVerifier
。
下面對各個類做一下簡單介紹:
-
Feign.Builder
:是feign.Client的建造者天试,用于建造feign.Client實例槐壳; -
feign.Client
:feign客戶端,用于提交Http/Https請求喜每; -
Logger.Level
:當前FeignClient日志輸出級別务唐;文章末尾有詳細描述雳攘; -
SSLSocketFactory
:SSLSocket工廠;SSLSocket擴展了Socket并提供使用SSL或TLS協(xié)議的安全套接字枫笛。這種套接字是正常的流套接字吨灭,但是它們在基礎網(wǎng)絡傳輸協(xié)議(如TCP)上添加了安全保護層。 -
HostnameVerifier
:實現(xiàn)主機名驗證功能刑巧;在握手期間喧兄,如果URL的主機名和服務器的標識主機名不匹配,則驗證機制可以回調(diào)此接口實現(xiàn)程序來確定是否應該允許此連接海诲,如果回調(diào)內(nèi)實現(xiàn)不恰當繁莹,默認接受所有域名,則有安全風險特幔; -
NoopHostnameVerifier
:是HostnameVerifier
的實現(xiàn)類之一咨演,作用是關閉主機名驗證功能,并且永遠不會拋出SSLException蚯斯。
@Configuration
public class FeignHttpsConfig {
@Bean
public Feign.Builder feignBuilder() {
final Client trustSSLSockets = client();
return Feign.builder().client(trustSSLSockets);
}
@Bean
public Client client(){
return new Client.Default(
TrustingSSLSocketFactory.get(), new NoopHostnameVerifier());
}
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
NoopHostnameVerifier定義
位于org.apache.http.conn.ssl包中
public class NoopHostnameVerifier implements HostnameVerifier {
public static final NoopHostnameVerifier INSTANCE = new NoopHostnameVerifier();
@Override
public boolean verify(final String s, final SSLSession sslSession) {
return true;
}
@Override
public final String toString() {
return "NO_OP";
}
}
TrustingSSLSocketFactory定義
public class TrustingSSLSocketFactory extends SSLSocketFactory
implements X509TrustManager, X509KeyManager {
private static final Map<String, SSLSocketFactory> sslSocketFactories =
new LinkedHashMap<String, SSLSocketFactory>();
private static final char[] KEYSTORE_PASSWORD = "password".toCharArray();
private final static String[] ENABLED_CIPHER_SUITES = {"TLS_RSA_WITH_AES_256_CBC_SHA"};
private final SSLSocketFactory delegate;
private final String serverAlias;
private final PrivateKey privateKey;
private final X509Certificate[] certificateChain;
private TrustingSSLSocketFactory(String serverAlias) {
try {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(new KeyManager[] {this}, new TrustManager[] {this}, new SecureRandom());
this.delegate = sc.getSocketFactory();
} catch (Exception e) {
throw new RuntimeException(e);
}
this.serverAlias = serverAlias;
if (serverAlias.isEmpty()) {
this.privateKey = null;
this.certificateChain = null;
} else {
try {
KeyStore keyStore =
loadKeyStore(TrustingSSLSocketFactory.class.getResourceAsStream("/keystore.jks"));
this.privateKey = (PrivateKey) keyStore.getKey(serverAlias, KEYSTORE_PASSWORD);
Certificate[] rawChain = keyStore.getCertificateChain(serverAlias);
this.certificateChain = Arrays.copyOf(rawChain, rawChain.length, X509Certificate[].class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public static SSLSocketFactory get() {
return get("");
}
public synchronized static SSLSocketFactory get(String serverAlias) {
if (!sslSocketFactories.containsKey(serverAlias)) {
sslSocketFactories.put(serverAlias, new TrustingSSLSocketFactory(serverAlias));
}
return sslSocketFactories.get(serverAlias);
}
static Socket setEnabledCipherSuites(Socket socket) {
SSLSocket.class.cast(socket).setEnabledCipherSuites(ENABLED_CIPHER_SUITES);
return socket;
}
private static KeyStore loadKeyStore(InputStream inputStream) throws IOException {
try {
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(inputStream, KEYSTORE_PASSWORD);
return keyStore;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
inputStream.close();
}
}
@Override
public String[] getDefaultCipherSuites() {
return ENABLED_CIPHER_SUITES;
}
@Override
public String[] getSupportedCipherSuites() {
return ENABLED_CIPHER_SUITES;
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose)
throws IOException {
return setEnabledCipherSuites(delegate.createSocket(s, host, port, autoClose));
}
@Override
public Socket createSocket(String host, int port) throws IOException {
return setEnabledCipherSuites(delegate.createSocket(host, port));
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return setEnabledCipherSuites(delegate.createSocket(host, port));
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
throws IOException {
return setEnabledCipherSuites(delegate.createSocket(host, port, localHost, localPort));
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
throws IOException {
return setEnabledCipherSuites(delegate.createSocket(address, port, localAddress, localPort));
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
@Override
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
return null;
}
@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
return null;
}
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
return null;
}
@Override
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
return serverAlias;
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
return certificateChain;
}
@Override
public PrivateKey getPrivateKey(String alias) {
return privateKey;
}
}
日志級別
-
NONE
:不記錄 (DEFAULT)薄风; -
BASIC
,:僅記錄請求方式和URL及響應的狀態(tài)代碼與執(zhí)行時間; -
HEADERS
:日志的基本信息與請求及響應的頭拍嵌; -
FULL
:記錄請求與響應的頭和正文及元數(shù)據(jù)遭赂。