tomcat/apache+https单&双向认证

基础概念介绍:秘钥/证书/https握手/CA相关概念
crt证书: 只含有公钥
p12证书: 是包含证书(含公钥)和私钥
JKS(Java key store): 存放密钥的容器。.jks .keystore .truststore等
KeyStore: 服务器的密钥存储库,存服务器的公钥私钥证书
TrustStore: 服务器的信任密钥存储库,存CA公钥(也就是CA根证书)

  • 单向认证需要文件 :
  • tomcat:
    root.crt: 客户端使用的CA根证书,用来验证服务器传来的证书
    server.keystore 服务端证书存放的容器
  • apache:
    server.crt: 服务器端证书
    server.key: 服务器端私钥
    root.crt: CA根证书
  • **双向认证需要文件 : **
    tomcat:
    root.crt : 客户端使用的CA根证书
    client.p12 : 客户端证书包含私钥
    root.truststore : CA公钥存放到受信赖的容器
    server.keystore : 服务端证书存放的容器
    apache:
    server.crt 服务器端证书
    server.key 服务器端私钥

一.环境说明:

系统:CentOS release 5.5 (Final)
OpenSSL: OpenSSL 1.1.0c
apache: Apache/2.2.23 (Unix)
tomcat: Apache Tomcat/7.0.69
客户端浏览器:Chrome 49.0.2623.110 (64-bit)

二.配置https总体流程

  1. 制作CA根证书 ( 代表第三方权威CA机构 )

  2. 制作服务器端证书 -> 使用CA根证书签名认证服务器端证书

  3. 制作客户端证书 -> 使用CA根证书签名认证客户端证书(双向认证需要

  4. 配置服务器端配置文件, apache(http.conf) tomcat(server.xml),开启ssl

  5. 在客户端浏览器证书管理->授权中信中导入CA根证书(本步骤是自己搭建CA认证需要的,如果证书是第三方认证机构颁发的则不需要配置该步骤,因为各大浏览器厂商已经默认导入了CA权威机构的根证书<包含对应公钥>);

6.在客户端浏览器证书管理->您的证书(个人)中导入客户端证书(双向认证需要),该证书是服务器端配置了双向认证后,服务器端需要验证客户端的证书;

三.配置https详细流程

目录结构:
CA根证书信息目录:/home/lxf/ca
服务证书信息目录:/home/lxf/ca/server
客户端证书信息目录:/home/lxf/ca/client

1.制作CA根证书 ( 代表第三方权威CA机构 )

(1). 创建根证书密钥文件(自己做CA) root.key

cd /home/lxf/ca
openssl genrsa -des3 -out root.key 2048
输出内容为:
Generating RSA private key, 2048 bit long modulus
.....................................................................................................................+++
..........................+++
e is 65537 (0x010001)
Enter pass phrase for root.key: ← 输入一个新密码 
Verifying – Enter pass phrase for root.key: ← 重新输入一遍密码

(2). 创建根证书的申请文件 root.csr

openssl req -new -key root.key -out root.csr
输出内容为:
Enter pass phrase for root.key: ← 输入前面创建的密码 
You are about to be asked to enter information that will be incorporated 
into your certificate request. 
What you are about to enter is what is called a Distinguished Name or a DN. 
There are quite a few fields but you can leave some blank 
For some fields there will be a default value, 
If you enter ‘.’, the field will be left blank. 
—– 
Country Name (2 letter code) [AU]:CN ← 国家代号,中国输入CN 
State or Province Name (full name) [Some-State]:BeiJing ← 省的全名,拼音 
Locality Name (eg, city) []:BeiJing ← 市的全名,拼音 
Organization Name (eg, company) [Internet Widgits Pty Ltd]:MyCompany Corp. ← 公司英文名 
Organizational Unit Name (eg, section) []: ← 可以不输入 
Common Name (eg, YOUR name) []: ← 此时不输入(因为根证书自己验证自己) 
Email Address []:admin@mycompany.com ← 电子邮箱,可随意填

Please enter the following ‘extra’ attributes 
to be sent with your certificate request 
A challenge password []: ← 可以不输入 
An optional company name []: ← 可以不输入

(3). 创建一个自当前日期起为期十年的根证书 root.crt

openssl x509 -req -days 3650 -sha256 -extensions v3_ca -signkey root.key -in root.csr -out root.crt
输出内容为:
Signature ok 
subject=/C=CN/ST=BeiJing/L=BeiJing/O=MyCompany Corp./emailAddress=admin@mycompany.com
Getting Private key 
Enter pass phrase for root.key: ← 输入前面创建的密码

(4).根据CA证书生成truststore JKS文件 root.truststore (存储秘钥库可以放很多个证书), 也就是将CA根证书root.crt导入到root.truststore库中
注意:这一步只针对双向认证,单向不需要,需要配置在双向认证中的tomcat服务器server.xml中的Connector的truststoreFile="/home/lxf/ca/root.truststore" truststorePass="123456

keytool -keystore root.truststore -keypass 123456 -storepass 123456 -alias ca -import -trustcacerts -file root.crt

键入回事后,提示是否信息此证书,输入y, 则生成truststore成功

2. 制作service服务器端证书

(1).创建服务器证书密钥 server.key

openssl genrsa -des3 -out server/server.key 2048
输出内容为:
Generating RSA private key, 2048 bit long modulus
...........................+++
...............+++
e is 65537 (0x010001)
Enter pass phrase for server.key: ← 输入前面创建的密码
Verifying - Enter pass phrase for server.key: ← 重新输入一遍密码
运行时会提示输入密码,此密码用于加密key文件(参数des3便是指加密算法,当然也可以选用其他你认为安全的算法.),以后每当需读取此文件(通过openssl提供的命令或API)都需输入口令.如果觉得不方便,也可以去除这个口令,但一定要采取其他的保护措施! 
去除key文件口令的命令: 
openssl rsa -in server.key -out server.key

(2).创建服务器证书的申请文件 server.csr

openssl req -new -key server/server.key -out server/server.csr
输出内容为:
Enter pass phrase for server.key: ← 输入前面创建的密码
You are about to be asked to enter information that will be incorporated 
into your certificate request. 
What you are about to enter is what is called a Distinguished Name or a DN. 
There are quite a few fields but you can leave some blank 
For some fields there will be a default value, 
If you enter ‘.’, the field will be left blank. 
—– 
Country Name (2 letter code) [AU]:CN ← 国家名称,中国输入CN 
State or Province Name (full name) [Some-State]:BeiJing-Server
Locality Name (eg, city) []:BeiJing-Server ← 市名,拼音 
Organization Name (eg, company) [Internet Widgits Pty Ltd]:MyCompany Corp. ← 公司英文名 
Organizational Unit Name (eg, section) []: ← 可以不输入 
Common Name (eg, YOUR name) []:ljmis.develop ← 域名(或者IP),若填写不正确,浏览器会报告证书无效
Email Address []:admin@mycompany.com ← 电子邮箱,可随便填

Please enter the following ‘extra’ attributes 
to be sent with your certificate request 
A challenge password []: ← 可以不输入 
An optional company name []: ← 可以不输入

(3).创建自当前日期起有效期为期十年的服务器证书 server.crt

openssl x509 -req -days 3650 -sha256  -extensions v3_req -CA root.crt -CAkey root.key -CAcreateserial -in server.csr -out server/server.crt

(4).将server.crt导出.p12文件 server.p12

openssl pkcs12 -export -in /tmp/ca/server.crt -inkey server/server.key -out  server/server.p12 -name "server"

(5).将.p12 文件导入到keystore JKS文件 server.keystore ( tomcat服务器需要配置的 )

keytool -importkeystore -v -srckeystore  server/server.p12 -srcstoretype pkcs12 -srcstorepass 123456 -destkeystore server/server.keystore -deststoretype jks -deststorepass 123456
这里srcstorepass后面的123456为server.p12的密码deststorepass后的123456为keyStore的密码

3. 制作client客户端证书

(1).创建客户端证书密钥文件 client.key

openssl genrsa -des3 -out client/client.key 2048

(2).创建客户端证书的申请文件 client.csr

openssl req -new -key client/client.key -out client.csr
输出内容为:
Enter pass phrase for client.key: ← 输入上一步中创建的密码 
You are about to be asked to enter information that will be incorporated 
into your certificate request. 
What you are about to enter is what is called a Distinguished Name or a DN. 
There are quite a few fields but you can leave some blank 
For some fields there will be a default value, 
If you enter ‘.’, the field will be left blank. 
—– 
Country Name (2 letter code) [AU]:CN ← 国家名称,中国输入CN 
State or Province Name (full name) [Some-State]:BeiJing-client ← 省名称,拼音 
Locality Name (eg, city) []:BeiJing-client ← 市名称,拼音 
Organization Name (eg, company) [Internet Widgits Pty Ltd]:MyCompany Corp. ← 公司英文名 
Organizational Unit Name (eg, section) []: ← 可以不填 
Common Name (eg, YOUR name) []:ljmis.develop ← 域名或自己的英文名
Email Address []:admin@mycompany.com ← 电子邮箱,可以随便填

Please enter the following ‘extra’ attributes 
to be sent with your certificate request 
A challenge password []: ← 可以不填 
An optional company name []: ← 可以不填

(3)创建一个自当前日期起有效期为十年的客户端证书 client.crt

openssl x509 -req -days 3650 -sha256  -extensions v3_req -CA root.crt -CAkey root.key -CAcreateserial -in client/client.csr -out client/client.crt

(4).将client.crt导出.p12文件 client.p12

openssl pkcs12 -export -in /client/client.crt -inkey /client/client.key -out  /client/client.p12 -name "client"

根据命令提示,输入client.key密码,创建p12密码。

4. 配置服务器端配置文件

  • tomcat 单向认证 server.xml
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" SSLEnabled="true"
               maxThreads="150" scheme="https" secure="true"
               clientAuth="false" sslProtocol="TLS" 
               keystoreFile="/home/lxf/ca/server/server.keystore" keystorePass="123456" 
              />
  • tomcat 双向认证 server.xml
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" SSLEnabled="true"
               maxThreads="150" scheme="https" secure="true"
               clientAuth="true" sslProtocol="TLS" 
               keystoreFile="/home/lxf/ca/server/server.keystore" keystorePass="123456" 
               truststoreFile="/hom/lxf/ca/root.truststore" truststorePass="123456"
              />
  • apache 配置
    (1)vim /usr/local/apache/conf/https.conf 开启mod_ssl.so模块
LoadModule ssl_module         modules/mod_ssl.so
Include conf/extra/httpd-ssl.conf
<IfModule ssl_module>
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
</IfModule>

(2)编辑/usr/local/apache/conf/extra/httpd-ssl.conf文件

<VirtualHost 192.168.9.224:443>
        DocumentRoot "/home/ljmis_develop/html"
        ServerAlias ljmis.develop
        #开启ssl
        SSLEngine on
        #单向认证需要的文件
        SSLCertificateFile "/home/lxf/ca/server/server.crt"
        SSLCertificateKeyFile "/home/lxf/ca/server/server.key"
        #双向认证需要的配置
        SSLCACertificateFile "/home/lxf/ca/root.crt"
        SSLVerifyClient require
        SSLVerifyDepth 10
        #日志配置
        ErrorLog "/usr/local/apache/logs/ljmis-ssl-error_log"
        TransferLog "/usr/local/apache/logs/ljmis-ssl-access_log"
</VirtualHost>

如果apache没有mod_ssl.so模块, 两种方式
第一种:重新编译安装apache 添加ssl模块

./configure --prefix=/usr/local/apache --enable-ssl=shared --with-ssl=/usr/local/openssl
make && make install

第二种: 动态添加模块
进入到安装apache源码目录

cd /usr/local/src/httpd-2.4.12/modules/ssl/
 /usr/local/apache2/bin/apxs -a -i -DHAVE_OPENSSL=1 -I/usr/include/openssl -L/usr/lib64/openssl -c *.c -lcrypto -lssl -ldl

查看/usr/local/apache2/modules/ 目录是否动态生成了mod_ssl.so

[root@version test-svn]# cd /usr/local/apache/modules/
[root@version modules]# ll | grep mod_ssl.so 
-rwxr-xr-x 1 root root   713042 May 16 12:03 mod_ssl.so

5.客户端浏览器导入A根证书

在证书管理 -> 授权中信中导入CA根证书root.crt(如果证书经过第三方CA权威颁发,则不需要配置该步骤

ssl-1.jpg

6.客户端浏览器导入客户端证书(双向认证需要的配置

在证书管理 -> 您的证书中导入client.p12客户端证书


ssl-2.jpg

四.访问测试

在制作证书中域名输入的是ljmis.develop,则需要配置hosts

192.168.9.224  ljmis.develop
  • 访问apache:https://ljmis.develop (apache会默认访问443端口)
    浏览器会提示使用导入的client.p12进行访问(双向认证),选择刚刚导入的client.12证书点击确定即可;

    ssl-3.jpg

    出现以下图片情况代表配置成功,即有绿锁图标
    ssl-4.jpg

  • 访问tomcat: https://ljmis.develop:8443 (访问指定8443端口)

  • 因为测试实验在一台服务器做的,apache和tomcat不能同时使用443一个端口,所所为要为tomcat特意指定8443端口;

五.使用java请求测试:

    @Test
    public void httpsAndClientGet() throws Exception
    {
        //加载客户端秘钥库(将客户端证书client.crt导入到该秘钥库)
        KeyStore keyStore  =KeyStore.getInstance(KeyStore.getDefaultType());
        FileInputStream instream =new FileInputStream(new File("/home/lxf/ca/client/client.keystore")); 
        String pass = "123456";
        keyStore.load(instream,pass.toCharArray());
        instream.close();
        
        //加载根证书秘钥库(将CA根证书root.crt导入到该秘钥库)
        KeyStore trustStore  = KeyStore.getInstance(KeyStore.getDefaultType());
        //FileInputStream instream1=new FileInputStream(new File("/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/security/cacerts.bak.lxf"));
        FileInputStream instream1=new FileInputStream(new File("/home/lxfca/root.truststore"));
        trustStore.load(instream1,pass.toCharArray());
        instream1.close();
        
        // Trust own CA and allself-signed certs
        SSLContext sslcontext= SSLContexts.custom()
        .loadKeyMaterial(keyStore,pass.toCharArray())
        .loadTrustMaterial(trustStore,new TrustSelfSignedStrategy())
        .build();
        
     // Allow TLSv1 protocol only
        SSLConnectionSocketFactory sslsf =
        new SSLConnectionSocketFactory(sslcontext,
                                 new String[] {"TLSv1" },
                                        null,
        SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
                 CloseableHttpClient httpclient =HttpClients.custom()
                         .setSSLSocketFactory(sslsf)
                         .build();
         
         String url = "https://localhost:8443/firstServlet/servlet/HelloServlet";
        HttpGet httpget = new HttpGet(url);
        System.out.println("ExecutingRequest:" + httpget.getRequestLine());
        CloseableHttpResponse response = httpclient.execute(httpget);
        
        HttpEntity entity =response.getEntity();
        System.out.println("-------------------------------------");
        System.out.println(response.getStatusLine());
        System.out.println(EntityUtils.toString(entity));
        EntityUtils.consume(entity);
        
        response.close();
        httpclient.close();
    }

请求的servle(https://localhost:8443/firstServlet/servlet/HelloServlet)t打印客户端证书信息

package lxf.servlet;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.SortedMap;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.security.cert.X509Certificate;
import javax.servlet.annotation.WebServlet;

//继承于HttpServlet
public class HelloServlet extends HttpServlet {

    private static final long serialVersionUID = 1601507150278487538L;
    private static final String REQUEST_ATTR_CERT = "javax.servlet.request.X509Certificate";
    private static final String CONTENT_TYPE = "text/plain;charset=UTF-8";
    private static final String DEFAULT_ENCODING = "UTF-8";
    private static final String SCHEME_HTTPS = "https";
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        //super.doGet(request, response);
        request.setCharacterEncoding("utf-8");
        request.setCharacterEncoding("utf-8");
        System.out.println("处理GET()请求");
        //获取浏览器输出对象
        PrintWriter out = response.getWriter();
        //用out对象给浏览器输出hello servlet
        response.setContentType("text/html;charset=utf-8");
        out.println("<strong>I am GET hello servlet</strong>");
        
        
        response.setContentType(CONTENT_TYPE);
        response.setCharacterEncoding(DEFAULT_ENCODING);
        PrintWriter out1 = response.getWriter();
        X509Certificate[] certs = (X509Certificate[]) request.getAttribute(REQUEST_ATTR_CERT);
        if (certs != null) {
            int count = certs.length;
            out1.println("client's cert total = [" + count + "]");
            for (int i = 0; i < count; i++) {
                X509Certificate cert = certs[i];
                out1.println("client cert  [" + cert.getSubjectDN() + "]: ");
                out1.println("client cert's  valid date:" + (verifyCertificate(cert) ? "是" : "否"));
                out1.println("client cert's  detail info:\r" + cert.toString());
            }
        } else {
            if (SCHEME_HTTPS.equalsIgnoreCase(request.getScheme())) {
                //out1.println("this a HTTPS requeest,但是没有可用的客户端证书");
                out1.println("this is a HTTPS requeest,But not usable cert");
            } else {
                out1.println("这不是一个HTTPS请求,因此无法获得客户端证书列表 ");
                out1.println("this  is  not a HTTPS requeest ");
            }
        }
        out1.close();
        
        
        /*
        try {
            String res = CorefireHttpPost.connect("https://localhost:8443", null);
            System.out.println(res.toString());
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        */
    }
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        System.out.println("处理POST()请求");
        //获取浏览器输出对象
        PrintWriter out = response.getWriter();
        //用out对象给浏览器输出hello servlet
        response.setContentType("text/html;charset=utf-8");
        out.println("<strong>I am POST hello servlet</strong>");
    }   
    /**
     * 
     * 校验证书是否过期
     * 
     * 
     * @param certificate
     * @return
     */
    private boolean verifyCertificate(X509Certificate certificate) {
        boolean valid = true;
        try {
            certificate.checkValidity();
        } catch (Exception e) {
            e.printStackTrace();
            valid = false;
        }
        return valid;
    }
    
}

六.使用php请求测试

/************************curl双向认证常量配置start**************************/
//根证书路径
define('HTTPS_CAINFO', '/home/lxf/ca/root.crt');
//client.pem文件路径,可以从crt转换为pem,或使用命令生成pem证书
define('HTTPS_SSLCERT', '/home/lxf/ca/client/client.pem');
//私钥文件路径
define('HTTPS_SSLKEY', '/home/lxf/ca/client/client.key');
//私钥密码
define('HTTPS_SSLKEYPASSWD', '123456');
/************************curl双向认证常量配置end**************************/
/**
 * [方法描述] CURL模拟post请求,执行https双向认证
 * @param [string] $url  请求路径
 * @param [array] $fields  请求参数 array( 'data' => '111' ); 
 * @param [array ] $extraheader [header头部的重写]
 * @param [const] 常量定义 HTTPS_CAINFO 根证书 例:/home/lxf/ca/root.crt
 * @param [const] 常量定义 HTTPS_SSLCERT client.pem client端证书
 * @param [const] 常量定义 HTTPS_SSLCERTPASSWD client证书密码
 * @param [const] 常量定义 HTTPS_SSLKEY 私钥文件路径
 * @param [const] 常量定义 HTTPS_SSLKEYPASSWD 私钥密码
 * @return  接口返回的数据
 */
function do_Post($url, $fields, $extraheader = array()){  
    $fields = http_build_query($fields);    //将数据进行URL-encode转换
    $ch = curl_init();  
    curl_setopt($ch, CURLOPT_URL, $url);  
    curl_setopt($ch, CURLOPT_POST, true);  
    curl_setopt($ch, CURLOPT_PORT, 8443);//指定端口
    curl_setopt($ch, CURLOPT_POSTFIELDS, $fields );  //post参数
    curl_setopt($ch, CURLOPT_HTTPHEADER, $extraheader);  //设置一个header中传输内容的数组。
    curl_setopt($ch, CURLOPT_SSLVERSION, 1);//传递一个包含SSL版本的长参数。默认PHP将被它自己努力的确定,在更多的安全中你必须手工设置
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); //不信任任何证书
    curl_setopt($ch, CURLOPT_CAINFO, HTTPS_CAINFO); //根证书路径
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 1); // 检查证书中是否设置域名,0不验证
    curl_setopt($ch, CURLOPT_VERBOSE, 1); //debug模式
    curl_setopt($ch,CURLOPT_SSLCERTTYPE,'PEM');
    curl_setopt($ch, CURLOPT_SSLCERT, HTTPS_SSLCERT); //client.pem文件路径
    // curl_setopt($ch, CURLOPT_SSLCERTPASSWD, HTTPS_SSLCERTPASSWD); //client证书密码
    curl_setopt($ch, CURLOPT_SSLKEY, HTTPS_SSLKEY);//私钥文件路径
    curl_setopt($ch, CURLOPT_SSLKEYPASSWD, HTTPS_SSLKEYPASSWD);//私钥密码
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 如果成功只将结果返回,不自动输出任何内容。
    $output = curl_exec($ch); 
    if(curl_errno($ch) != 0) $output = 'Curl error: ' . curl_error($ch);//curl错误信息
    curl_close($ch);  
    return $output;  
} 

五.常用命令

  • 查看证书信息
openssl x509 -in cert.pem -noout -text
  • 查看秘钥仓库信息
keytool -list -keystore client.keystore
  • PEM 转为 DER
openssl x509 -in xxx.pem -outform der -out xxx.der
  • DER 转为 PEM
openssl x509 -in xxx.der -inform der -outform pem -out xxx.pem

参考站点:
linux下Tomcat+OpenSSL配置单向&双向认证(自制证书)
Linux下Tomcat配置使用SSL双向认证(使用openssl生成证书)
Java 和 HTTP 的那些事(四) HTTPS 和 证书
Apache 2 配置 SSL

推荐阅读更多精彩内容