前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >浅谈https中的双向认证

浅谈https中的双向认证

作者头像
SecondWorld
发布2021-07-28 11:04:06
2.1K0
发布2021-07-28 11:04:06
举报
文章被收录于专栏:Java开发者杂谈Java开发者杂谈

总述

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

单向认证与双向认证

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

show me the code

代码语言:javascript
复制
这里给出在使用httpClient的时候如何初始化连接池。
代码语言:javascript
复制
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;
    }
}
代码语言:javascript
复制
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);
        }
    }
}
代码语言:javascript
复制
这里其实本来是双向认证的,但是因为时间原因所以偷了个懒,略过了客户端对服务器端证书的校验,而直接使用`TrustAllStrategy.INSTANCE`。其实如果客户端需要对服务器端证书进行校验的话可以参考如下代码设置`trustStrategy`:
代码语言:javascript
复制
KeyStore trustKeyStore = KeyStore.getInstance("jks");
trustKeyStore.load(new FileInputStream("D:\\trustkeystore.jks"), "123456".toCharArray());
sslContextBuilder.loadTrustMaterial(trustKeyStore, new TrustSelfSignedStrategy());

小结

  • 证书是存在证书链的,根证书能对所有子证书进行验证,在进行双向认证的时候服务端和客户端需要初始化的证书都是从根证书生成的
  • 在TLS协议过程中发送的客户端和服务端证书(.crt)其实都是公钥证书,外加一些版本号、身份、签名等信息
  • 客户端可以通过使用TrustAllStrategy来忽略对服务器证书中的身份校验,而仅仅是去拿到证书里面的公钥
  • 如果服务端对客户端证书有校验,而客户端在使用HttpClient请求的时候未loadKeyMaterial发送客户端证书,则会报类似如下错误:
代码语言:javascript
复制
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。那么会报类似如下错误:
代码语言:javascript
复制
Certificate for <xxxx> doesn't match any of the subject alternative name 
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-07-22 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 总述
  • 单向认证与双向认证
    • 单向认证
      • 双向认证
      • show me the code
      • 小结
      相关产品与服务
      SSL 证书
      腾讯云 SSL 证书(SSL Certificates)为您提供 SSL 证书的申请、管理、部署等服务,为您提供一站式 HTTPS 解决方案。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档