zoukankan      html  css  js  c++  java
  • 浅谈https中的双向认证

    总述

    https简单来说就是在http协议的基础上增加了一层安全协议。通常为TLS或者SSL(一般现在都采用TLS,更加安全)。这一层安全协议的最主要的作用有两个:
    1. 验证服务端或客户端的合法性
    2. 商量出最终用来http通信的对称加密秘钥
    本次仅仅讲第1点
    

    单向认证与双向认证

    所谓的认证既确认对方身份,单向认证一般是指客户端确认服务端身份,双向认证则是指在客户端需要确认服务端身份的同时,服务端也需要确认客户端的身份。
    具体可以通过下面两幅图来看下区别:
    

    单向认证

    image

    双向认证

    image


    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 
    
    黎明前最黑暗,成功前最绝望!
  • 相关阅读:
    VSTO不能创建OFFICE 文档项目的原因
    vs2016 创建 vsto excel 文件项目的一个问题
    一个开发原则:永远不要返回NULL
    客户为什么习惯变更需求
    从实际项目中的一个改进细节谈程序的易用性优化
    第三方系统打开EAFC的实现
    功能间(两个form)数据交互的编程方法
    关于行军模式大批量数据的审批的实现
    程序的升级发布管理
    转:从如何判断浮点数是否等于0说起——浮点数的机器级表示 献给依然 if ( double i ==0.00)的菜鸟们
  • 原文地址:https://www.cnblogs.com/Kidezyq/p/15045758.html
Copyright © 2011-2022 走看看