1. 程式人生 > 其它 >淺談https中的雙向認證

淺談https中的雙向認證

總述

https簡單來說就是在http協議的基礎上增加了一層安全協議。通常為TLS或者SSL(一般現在都採用TLS,更加安全)。這一層安全協議的最主要的作用有兩個:
1. 驗證服務端或客戶端的合法性
2. 商量出最終用來http通訊的對稱加密祕鑰
本次僅僅講第1點

單向認證與雙向認證

所謂的認證既確認對方身份,單向認證一般是指客戶端確認服務端身份,雙向認證則是指在客戶端需要確認服務端身份的同時,服務端也需要確認客戶端的身份。
具體可以通過下面兩幅圖來看下區別:

單向認證

雙向認證


show me the code

這裡給出在使用httpClient的時候如何初始化連線池。
public class HttpClientInitConfig {
    
    /**
     * ssl雙向認證證書 預設客戶端不驗證服務端返回
     */
    private TrustStrategy trustStrategy = TrustAllStrategy.INSTANCE;
    
    private String sslProtocol = "TLSV1.2";
    
    /**
     * ssl雙向認證客戶端的keystore
     */
    private String keyStorePath;
    
    /**
     * ssl雙向認證客戶端keystore的祕鑰
     */
    private String storePwd;
    
    /**
     * ssl雙向認證客戶端私鑰證書密碼
     */
    private String keyPwd;
    
    /**
     * 祕鑰庫證書型別
     */
    private String keyStoreType = "PKCS12";
    
    public String getKeyStoreType() {
        return keyStoreType;
    }

    public void setKeyStoreType(String keyStoreType) {
        this.keyStoreType = keyStoreType;
    }

    private int maxPerRoute = 200;
    
    private int maxTotal = 200;
    
    private int validateAfterInactivity = 60000;

    public int getValidateAfterInactivity() {
        return validateAfterInactivity;
    }

    public void setValidateAfterInactivity(int validateAfterInactivity) {
        this.validateAfterInactivity = validateAfterInactivity;
    }

    public TrustStrategy getTrustStrategy() {
        return trustStrategy;
    }

    public void setTrustStrategy(TrustStrategy trustStrategy) {
        this.trustStrategy = trustStrategy;
    }

    public String getSslProtocol() {
        return sslProtocol;
    }

    public void setSslProtocol(String sslProtocol) {
        this.sslProtocol = sslProtocol;
    }

    public String getKeyStorePath() {
        return keyStorePath;
    }

    public void setKeyStorePath(String keyStorePath) {
        this.keyStorePath = keyStorePath;
    }

    public String getStorePwd() {
        return storePwd;
    }

    public void setStorePwd(String storePwd) {
        this.storePwd = storePwd;
    }

    public String getKeyPwd() {
        return keyPwd;
    }

    public void setKeyPwd(String keyPwd) {
        this.keyPwd = keyPwd;
    }

    public int getMaxPerRoute() {
        return maxPerRoute;
    }

    public void setMaxPerRoute(int maxPerRoute) {
        this.maxPerRoute = maxPerRoute;
    }

    public int getMaxTotal() {
        return maxTotal;
    }

    public void setMaxTotal(int maxTotal) {
        this.maxTotal = maxTotal;
    }
}
public class CacheablePooledHttpClient {
    private static final Logger LOG = LoggerFactory.getLogger(CacheablePooledHttpClient.class);
    
    private Map<String, HttpClient> httpClientMap = new ConcurrentHashMap<>();

    private Map<String, HttpClientConnectionManager> connectionManagerMap = new ConcurrentHashMap<>();
    
    CacheablePooledHttpClient() {
    }
    
    private static final class InstanceHolder {
        static final CacheablePooledHttpClient instance = new CacheablePooledHttpClient();
    }
    
    public static CacheablePooledHttpClient getInstance() {
        return InstanceHolder.instance;
    }
    
    public HttpClient getHttpClient(String clientKey) {
        return this.httpClientMap.get(clientKey);
    }

	public HttpClient initHttpClient(String clientKey, HttpClientInitConfig initConfig) {
        if (this.httpClientMap.containsKey(clientKey)) {
            return this.httpClientMap.get(clientKey);
        }
        
        synchronized (httpClientMap) {
            if (this.httpClientMap.containsKey(clientKey)) {
                return this.httpClientMap.get(clientKey);
            }
            try {
                SSLContextBuilder sslContextBuilder = SSLContexts.custom().setProtocol(initConfig.getSslProtocol()).loadTrustMaterial(initConfig.getTrustStrategy());
                // ssl雙向認證的時候,客戶端需要載入的證書
                if (StringUtils.isNotBlank(initConfig.getKeyStorePath()) && StringUtils.isNotBlank(initConfig.getStorePwd())) {
                    final KeyStore ks = CryptUtils.loadKeyStore(initConfig.getKeyStorePath(), initConfig.getStorePwd().toCharArray(), initConfig.getKeyStoreType());
                    sslContextBuilder.loadKeyMaterial(ks, initConfig.getKeyPwd().toCharArray(), (a, b) -> {
                            try {
                                return CryptUtils.getFirstAliase(ks);
                            } catch (KeyStoreException e) {
                                throw new HttpClientInitException(e);
                            }
                        });
                    LOG.info("使用客戶端證書【{}】初始化http連線池", initConfig.getKeyStorePath());
                }
                SSLContext sslContext = sslContextBuilder.build();
                Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory> create()
                        .register("http", PlainConnectionSocketFactory.getSocketFactory())
                        .register("https", new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE)).build();

                @SuppressWarnings("resource")
                PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
                connectionManager.setValidateAfterInactivity(initConfig.getValidateAfterInactivity());    // 半連線狀態關閉時間
                connectionManager.setDefaultMaxPerRoute(initConfig.getMaxPerRoute());
                connectionManager.setMaxTotal(initConfig.getMaxTotal());
                connectionManagerMap.put(clientKey, connectionManager);
                httpClientMap.put(clientKey, new HttpClient(initHttpClient(connectionManager)));
                LOG.info("初始化key為【{}】的httpClient連線池成功!", clientKey);
            } catch (Exception ex) {
                throw new HttpClientInitException(ex);
            }

            try {
                Runtime.getRuntime().addShutdownHook(new ContainerShutdownHook(this));
            } catch (Exception e) {
                LOG.error("新增關閉鉤子異常!", e);
            }
        }
        
        
        return httpClientMap.get(clientKey);
    }
	
	private CloseableHttpClient initHttpClient(PoolingHttpClientConnectionManager connectionManager) {
        RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(5000).setSocketTimeout(300000).setConnectionRequestTimeout(30000)
                .build();
        return HttpClients.custom().setDefaultRequestConfig(requestConfig).setConnectionManager(connectionManager)
                .setConnectionManagerShared(false).setRetryHandler(new DefaultHttpRequestRetryHandler(0, false)).evictExpiredConnections()
                .evictIdleConnections(1800L, TimeUnit.SECONDS).build();
    }
    
    public void close() {
        try {
            for (HttpClient client : this.httpClientMap.values()) {
                if (client != null) {
                    client.getCloseableHttpClient().close();
                }
            }

            for (HttpClientConnectionManager connectionManager : this.connectionManagerMap.values()) {
                if (connectionManager != null) {
                    connectionManager.shutdown();
                }
            }

            LOG.info("Close httpClient completed");
        } catch (Exception e) {
            LOG.error("shutdown httpcliet exception: ", e);
        }
    }
}
這裡其實本來是雙向認證的,但是因為時間原因所以偷了個懶,略過了客戶端對伺服器端證書的校驗,而直接使用`TrustAllStrategy.INSTANCE`。其實如果客戶端需要對伺服器端證書進行校驗的話可以參考如下程式碼設定`trustStrategy`:
KeyStore trustKeyStore = KeyStore.getInstance("jks");
trustKeyStore.load(new FileInputStream("D:\\trustkeystore.jks"), "123456".toCharArray());
sslContextBuilder.loadTrustMaterial(trustKeyStore, new TrustSelfSignedStrategy());

小結

  • 證書是存在證書鏈的,根證書能對所有子證書進行驗證,在進行雙向認證的時候服務端和客戶端需要初始化的證書都是從根證書生成的
  • 在TLS協議過程中傳送的客戶端和服務端證書(.crt)其實都是公鑰證書,外加一些版本號、身份、簽名等資訊
  • 客戶端可以通過使用TrustAllStrategy來忽略對伺服器證書中的身份校驗,而僅僅是去拿到證書裡面的公鑰
  • 如果服務端對客戶端證書有校驗,而客戶端在使用HttpClient請求的時候未loadKeyMaterial傳送客戶端證書,則會報類似如下錯誤:
javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake
Caused by: javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake
        at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367) ~[?:1.8.0_192]
        at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395) ~[?:1.8.0_192]
        at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379) ~[?:1.8.0_192]
        at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367) ~[?:1.8.0_192]
        at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395) ~[?:1.8.0_192]
        at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379) ~[?:1.8.0_192]
  • 如果客戶端未使用TrustAllStrategy初始化HttpClient且指定對服務端的域名校驗器不是NoopHostnameVerifier.INSTANCE, 那麼如果服務端生成證書時指定的域名/ip不是服務端實際域名/ip。那麼會報類似如下錯誤:
Certificate for <xxxx> doesn't match any of the subject alternative name 
黎明前最黑暗,成功前最絕望!