java支持https_ssl双向证书访问

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://www.runoob.com/w3cnote/https-ssl-intro.html (HTTPS 与 SSL 证书概要)

https://blog.csdn.net/liuquan0071/article/details/50318405 (JAVA SSL HTTPS 连接详解生成证书)

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值