java支持https_ssl双向证书访问

爱⌒轻易说出口 提交于 2020-07-29 10:31:01
HTTPS简介
超文本传输安全协议(英语:Hypertext Transfer Protocol Secure,缩写:HTTPS,常称为HTTP over TLS,HTTP over SSL或HTTP Secure)是一种网络安全传输协议。具体介绍以前先来介绍一下以前常见的HTTP,HTTP就是我们平时浏览网页时候使用的一种协议。HTTP协议传输的数据都是未加密的,也就是明文,因此使用HTTP协议传输隐私信息非常不安全。HTTP使用80端口通讯,而HTTPS占用443端口通讯。在计算机网络上,HTTPS经由超文本传输协议(HTTP)进行通信,但利用SSL/TLS来加密数据包。HTTPS开发的主要目的,是提供对网络服务器的身份认证,保护交换数据的隐私与完整性。这个协议由网景公司(Netscape)在1994年首次提出,随后扩展到互联网上。
 
HTTPS和HTTP的区别
https协议需要到ca申请证书,一般免费证书很少,需要交费。
http是超文本传输协议,信息是明文传输;https 则是具有安全性的ssl加密传输协议。
http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
 
SSL 证书
前面我们可以了解到HTTPS核心的一个部分是数据传输之前的握手,握手过程中确定了数据加密的密码。在握手过程中,网站会向浏览器发送SSL证书,SSL证书和我们日常用的身份证类似,是一个支持HTTPS网站的身份证明,SSL证书里面包含了网站的域名,证书有效期,证书的颁发机构以及用于加密传输密码的公钥等信息,由于公钥加密的密码只能被在申请证书时生成的私钥解密,因此浏览器在生成密码之前需要先核对当前访问的域名与证书上绑定的域名是否一致,同时还要对证书的颁发机构进行验证,如果验证失败浏览器会给出证书错误的提示。在这一部分我将对SSL证书的验证过程以及个人用户在访问HTTPS网站时,对SSL证书的使用需要注意哪些安全方面的问题进行描述。
 
单向认证
常见的SSL验证常见以单向认证居多,在双方建立连接后,由服务器从信作库中取出证书,生成随机算法数值,发送给客户端,客户端收到后检查证书是否合法(如:是否过期,是否吊销,证书状态等);因此该方案以面向用户为主,因用户众多,服务端不验证客户端身份不做任何限制,用户请求后由服务端分布即可;
 
双向认证
双向SSL认证,通常是企业之间或服务之间调用,有较高的安全鉴权要求,限制访问者身份,开启双方服务端验证,从而避免未认证用户的非法访问;需要确定一点的是,使用单向验证还是双向验证,是服务器决定的。
双向验证基本过程与单向验证相同,不同在于:1.服务器第一次回应客户端的SeverHello消息中,会要求客户端提供“客户端的证书”。2.在双向验证中,客户端需要用到密钥库,保存自己的私钥和证书,并且证书需要提前发给服务器,由服务器放到它的信任库中;
 
模拟场景:
服务端与客户端采用SSL加密通信,需要基于双向证书+私有密钥,进行授权和身份的验证,客户端只能接受证书认证通过的服务端消息,同样,服务端只接受证书认证通过的客户端消息。
 
证书库
不采用keytool工具导入到jdk的证书信任库,而在D:\test\cacerts目录下存放以证书与密钥文件,通过代码加载与使用
ca.crt //服务端证书,CA证书 (SSL),通常是服务端创建的证书,由于客户端做双向验证,确认是证书目标服务的响应,防止非法篡改
client.crt //客户端证书文件和密码 (SSL),在常用企业开发场景中,通常由服务端创建后,由客户端保存,当然也可以客户端创建后向服务端申请上传到信任库
openssl.key //私钥文件名 (SSL),通常会有私钥和公钥两种密钥文件,将访问数据通过私钥加密,公钥解密,也可相反,公钥加密,私钥解密

 

代码实现
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.asn1.pkcs.RSAPrivateKey;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;

import javax.net.ssl.*;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.util.Map;

/**
 * @version V1.0
 * @Description 用java.net.HttpURLConnection进行https操作工具类,支持双向证书(ca证书,crt证书,密钥)验证
 */
public class HttpsSslUtils {
    /**
        1.pom.xml依赖第三方包做openssl私钥算法解析,默认jdk库无法解析超长128byte以上的密钥
        <dependency>
         <groupId>org.bouncycastle</groupId>
         <artifactId>bcprov-jdk15on</artifactId>
         <version>1.60</version>
      </dependency>
        2.在双向验证中,客户端需要用到密钥库,保存自己的私钥和证书,并且证书需要提前发给服务器,由服务器放到它的信任库中。
        3.可通过命令方式调试对比:curl -v -i --cacert ca.crt --cert client.crt --key openssl.key https://ssl.com/token -X POST -d '{"user_id":"1"}'
    */
    private final static Logger logger = LoggerFactory.getLogger(HttpsSslUtils.class);

    static final int CONNECT_TIMEOUT_MILLES = 3000;
    static final Charset ENCODING;
    public static final String HTTP_GET = "GET";
    public static final String HTTP_POST = "POST";
    static final String [] METHODS;

    static {
        ENCODING = Charset.forName("UTF-8");
        METHODS = new String[]{HTTP_GET, HTTP_POST};
    }

    public static String doSslGet(String url, Map<String, String> headers, Map<String,String> params, String content, SSLKeyStore ssl) throws IOException {
        return doOutput(url, HTTP_GET, headers, params, content, true, ssl);
    }

    public static String doSslPost(String url, Map<String, String> headers, Map<String,String> params, String content, SSLKeyStore ssl) throws IOException {
        return doOutput(url, HTTP_POST, headers, params, content, true, ssl);
    }

    public static String doOutput(final String url,final String method,final Map<String, String> headers,final Map<String,String> params,final String content,boolean isSsl, SSLKeyStore ssl) throws IOException {
        HttpURLConnection conn = null;
        try{
            conn = createConnection(setParams(url, params), isSsl, ssl);
            setMethod(conn, method);
            setHeaders(conn, headers);
            conn.connect();
            output(conn, content);
            return input(conn);
        }catch(IOException ioe){
            ioe.printStackTrace();
            throw ioe;
        } finally{
            connClose(conn);
        }
    }

    private static void connClose(HttpURLConnection conn){
        if (conn != null){
            conn.disconnect();
        }
    }

    private static String input(HttpURLConnection conn) throws IOException{
        int len ;
        char[] cbuf = new char[1024 * 8];
        StringBuilder buf = new StringBuilder();
        int status = conn.getResponseCode();
        InputStream in = null;
        BufferedReader reader = null;
        try{
            in = conn.getErrorStream();
            if (in == null && status < 400) { //400或以上表示:访问的页面域名不存在或者请求错误、服务端异常
                in = conn.getInputStream();
            }
            if (in != null){
                reader = new BufferedReader(new InputStreamReader(in, ENCODING));
                while ((len = reader.read(cbuf)) > 0){
                    buf.append(cbuf, 0 , len);
                }
            }
        }finally{
            if (reader != null){
                reader.close();
            }
            if (in != null){
                in.close();
            }
        }
        return buf.toString();
    }

    private static void output(HttpURLConnection conn, String content) throws IOException {
        if (StringUtils.isBlank(content))
            return ;
        OutputStream out = conn.getOutputStream();
        try{
            out.write(content.getBytes(ENCODING));
        } finally{
            if (out != null){
                out.flush();
                out.close();
            }
        }
    }

    private static HttpURLConnection createConnection(String url, boolean isSsl, SSLKeyStore ssl) throws IOException {
        logger.info("http connection url:"+ url);
        HttpURLConnection conn = null;
        if (isSsl) {
            try {
                conn = (HttpsURLConnection)(new URL(url)).openConnection();
                if (ssl != null){
                    //ssl协议,证书验证
                    SSLSocketFactory sslSocketFactory = getSSLSocketFactory(ssl.getCaAlias(),ssl.getCaPath(),ssl.getCrtAlias(),ssl.getCrtPath(),ssl.getKeyPath(), ssl.getPassword());
                    ((HttpsURLConnection) conn).setSSLSocketFactory(sslSocketFactory);
                }else {
                    //ssl协议,跳过证书验证
                    httpssl(conn);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }else {
            conn = (HttpURLConnection)(new URL(url)).openConnection();
        }
        setConfig(conn);
        return conn;
    }

    private static void setMethod(HttpURLConnection conn, String method) throws IOException{
        Assert.isTrue(StringUtils.containsAny(method,METHODS),"只支持GET、POST、PUT、DELETE操作");
        conn.setRequestMethod(method);
    }

    private static void setConfig(HttpURLConnection conn){
        conn.setConnectTimeout(CONNECT_TIMEOUT_MILLES);
        conn.setUseCaches(false);
        conn.setInstanceFollowRedirects(true);
        conn.setRequestProperty("Connection", "close");
        //conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");//这个根据需求,自已加,也可以放到headersc参数内
        conn.setDoOutput(true);//是否启用输出流,method=get,请求参数是拼接在地址档,默认为false
        conn.setDoInput(true);//是否启用输入流
    }

    private static void setHeaders(HttpURLConnection conn, Map<String,String> headers){
        if (headers == null || headers.size() <= 0) return ;
        headers.forEach((k,v) -> conn.setRequestProperty(k,v));//设置自定义header参数
    }

    private static String setParams(String url, Map<String, String> params){
        if (params == null || params.size() <= 0)
            return url;
        StringBuilder sb = new StringBuilder(url);
        sb.append("?");
        params.forEach((k,v)->sb.append(k).append("=").append(v).append("&"));
        return sb.substring(0, sb.length() - 1);
    }

    /**
     * 创建ssl连接(此版本跳过证书较验)
     * SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持,提供如下支持:
     * 1)认证用户和服务器,确保数据发送到正确的客户机和服务器;
     * 2)加密数据以防止数据中途被窃取;
     * 3)维护数据的完整性,确保数据在传输过程中不被改变。
     * @throws Exception
     */
    public static void httpssl(HttpURLConnection conn) throws Exception {
        TrustManager[] trustAllCerts = new TrustManager[1];
        TrustManager tm = new SslManager();
        trustAllCerts[0] = tm;
        javax.net.ssl.SSLContext sc = javax.net.ssl.SSLContext.getInstance("SSL");
//        javax.net.ssl.SSLContext sc = javax.net.ssl.SSLContext.getInstance("TLS");
        sc.init(null, trustAllCerts, new SecureRandom());
        ((HttpsURLConnection)conn).setSSLSocketFactory(sc.getSocketFactory());
        ((HttpsURLConnection)conn).setHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String str, SSLSession session) {
                return true;
            }
        });
    }
    
    /**
     *  SSL双向认证,需要服务端和客户端均加载ca证书和客户端证书,并且支持私钥加密解密(双向解析签名)
     * @param caAlias  服务端证书别名
     * @param caPath    服务端证书绝对路径
     * @param crtAlias  客户端证书别名
     * @param crtPath   客户端证书绝对路径
     * @param keyPath   密钥证书绝对路径
     * @param password  本地证书信任库密码,可以为空
     * @return
     * @throws Exception
     */
    public static SSLSocketFactory getSSLSocketFactory(String caAlias, String caPath, String crtAlias, String crtPath, String keyPath, String password) throws Exception{
        //CA证书是用来认证服务端的,CA就是一个公认的认证证书
        CertificateFactory cacf = CertificateFactory.getInstance("X.509");
        InputStream caStream = new FileInputStream(new File(caPath));
        Certificate ca = cacf.generateCertificate(caStream);
        KeyStore caks = KeyStore.getInstance(KeyStore.getDefaultType());
        caks.load(null, password.toCharArray());
        caks.setCertificateEntry(caAlias, ca);
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(caks);
        //关闭文件流
        caStream.close();

        //crt客户端证书,发送给服务端做双向验证
        CertificateFactory crtcf = CertificateFactory.getInstance("X.509");
        InputStream crtStream = new FileInputStream(new File(crtPath));
        Certificate crt = crtcf.generateCertificate(crtStream);
        KeyStore crtks = KeyStore.getInstance(KeyStore.getDefaultType());
        crtks.load(null, password.toCharArray());
        crtks.setCertificateEntry(crtAlias, crt);
        //客户端私钥,处理双向SSL验证中服务端用客户端证书加密的数据的解密(解析签名)工具
        //加载openssl私钥文件并返回解析对象
        PrivateKey privateKey = getPrivateKey(keyPath);
        crtks.setKeyEntry(crtAlias + ".private.key",  privateKey, password.toCharArray(), new Certificate[]{crt});

        //初始化秘钥管理器
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(crtks, password.toCharArray());
        //关闭文件流0
        crtStream.close();

        //注意,此处用TLSv1.2,需要服务端与客户端采用相同协议
        SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
        // 第一个参数是授权的密钥管理器,用来授权验证。TrustManager[]第二个是被授权的证书管理器,用来验证服务器端的证书。第三个参数是一个随机数值,可以填写null
        sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
        return sslContext.getSocketFactory();
    }

    /**
     * 解析openssl私钥文件
     * @param keyPath
     * @return
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(String keyPath) throws Exception{
        PrivateKey privKey = null;
        PemReader pemReader = null;
        File file = new File(keyPath);
        try {
            if (!file.exists()){
                throw new FileNotFoundException("未找到私钥文件:" + keyPath);
            }
            pemReader = new PemReader(new FileReader(file));
            PemObject pemObject = pemReader.readPemObject();
            byte[] pemContent = pemObject.getContent();
            //支持从PKCS#1或PKCS#8 格式的私钥文件中提取私钥
            if (pemObject.getType().endsWith("RSA PRIVATE KEY")) {
                //取得私钥  for PKCS#1 , openssl genrsa 默认生成的私钥就是PKCS1的编码
                RSAPrivateKey asn1PrivKey = RSAPrivateKey.getInstance(pemContent);
                RSAPrivateKeySpec rsaPrivKeySpec = new RSAPrivateKeySpec(asn1PrivKey.getModulus(), asn1PrivKey.getPrivateExponent());
                KeyFactory keyFactory= KeyFactory.getInstance("rsa");
                privKey= keyFactory.generatePrivate(rsaPrivKeySpec);
            } else if (pemObject.getType().endsWith("PRIVATE KEY")) {
                //通过openssl pkcs8 -topk8转换为pkcs8,例如(-nocrypt不做额外加密操作):
                //openssl pkcs8 -topk8 -in pri.key -out pri8.key -nocrypt
                //取得私钥 for PKCS#8
                PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(pemContent);
                KeyFactory kf = KeyFactory.getInstance("rsa");
                privKey = kf.generatePrivate(privKeySpec);
            }
        } catch (FileNotFoundException e) {
            throw e;
        } catch (IOException e) {
            throw e;
        } catch (NoSuchAlgorithmException e) {
            throw e;
        } catch (InvalidKeySpecException e) {
            throw e;
        }  finally {
            try {
                if (pemReader != null) {
                    pemReader.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return privKey;
    }
    //TrustManager是JSSE 信任管理器的基接口,管理和接受提供的证书,通过JSSE可以很容易地编程实现对HTTPS站点的访问
    //X509TrustManager此接口的实例管理使用哪一个 X509 证书来验证远端的安全套接字
    public static class SslManager implements TrustManager, X509TrustManager {
        @Override
        public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
        }
        @Override
        public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
        }
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }
    }

    /**
     *  此实体用于封装ssl请求,证书验证相关的keystroe信息
     */
    public static class SSLKeyStore{
        //ca证书别名
        private String caAlias;
        //ca存放绝对路径
        private String caPath;
        //客户端证书别名
        private String crtAlias;
        //客户端证书存放绝对路径
        private String crtPath;
        //客户端密钥存放绝对路径
        private String keyPath;
        //keystroey证书信任库访问密码,可以默认为null
        private String password;

        public String getCaAlias() {
            return caAlias;
        }

        public void setCaAlias(String caAlias) {
            this.caAlias = caAlias;
        }

        public String getCaPath() {
            return caPath;
        }

        public void setCaPath(String caPath) {
            this.caPath = caPath;
        }

        public String getCrtAlias() {
            return crtAlias;
        }

        public void setCrtAlias(String crtAlias) {
            this.crtAlias = crtAlias;
        }

        public String getCrtPath() {
            return crtPath;
        }

        public void setCrtPath(String crtPath) {
            this.crtPath = crtPath;
        }

        public String getKeyPath() {
            return keyPath;
        }

        public void setKeyPath(String keyPath) {
            this.keyPath = keyPath;
        }

        public String getPassword() {
            return password;
        }

        public void setPassword(String password) {
            this.password = password;
        }
    }

    public static void main(String[] args) throws IOException {
        //https 测试
        String httpsUrl = "https://openapi.com/token";
        String content = "{\"user_id\":\"1\"}";
        SSLKeyStore ssl = new SSLKeyStore();
        ssl.setCaAlias("ca");
        ssl.setCaPath("D:\\test\\cacerts\\ca.crt");
        ssl.setCrtAlias("client");
        ssl.setCrtPath("D:\\test\\cacerts\\client.crt");
        ssl.setKeyPath("D:\\test\\cacerts\\openssl.key");
        ssl.setPassword("changeit");
        String html = doSslGet(httpsUrl,null, null, content, ssl);
        System.out.println(html);
    }
}
常见问题
javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
原因:与服务端,握手失败
解决方案:
1.协议问题
指向请求协议:在调用创建http连接之前,在代码里指定系统属性,System.setProperty("https.protocols", "TLSv1.2,TLSv1.1,SSLv3");
或者在jvm启动命令中添加参数-Dhttps.protocols=TLSv1.2,TLSv1.1,TLSv1.0,SSLv3,SSLv2Hello,具体协议需要视服务端而定;
如果未知协议,可以上 https://myssl.com/ 测试查看,也可以jvm启动参数添加-Djavax.net.debug=all,查看debug日志,打印的服务端响应中保含协议信息,
 
2.网上有部份人因jdk中的jce安全机制问题,需更新jdk中相应的jce包
jce所在jdk的路径: %JAVA_HOME%\jre\lib\security目录下local_policy.jar,US_export_policy.jar
JDK7版本:http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
JDK8版本:http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
 
参考:
https://blog.csdn.net/liuquan0071/article/details/50318405 (JAVA SSL HTTPS连接详解生成证书)
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!