欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SSL Tomcat 双向认证

程序员文章站 2022-05-01 09:48:05
...

基本逻辑:
1、生成服务端**库并导出证书;
2、生成客户端**库并导出证书;
3、根据服务端**库生成客户端信任的证书;
4、将客户端证书导入服务端**库;
5、将服务端证书导入浏览器。

基本思路解析:通信双方在建立连接时,服务端会下发服务端证书,如果服务端证书所颁发的CA根证书在客户端的信任颁发列表中,客户端认证服务器通过,然后客户端发送自己的证书到服务端,如果该客户端证书在服务端证书信任列表中,通过。

免费做法:自己制作CA根证书,同时生成服务端和客户端证书,将服务端证书加入CA根证书信任列表,将客户端证书加入服务端信任列表,将CA根证书安装到客户端系统(win7 Mac 浏览器)证书信任列表中,即可通讯,此种做法不会有颁发机构的绿色安全提示。

付费做法:在付费CA证书机构中,上传自有服务端证书或购买下载机构颁发的服务端证书。


构建演示系统
演示环境:
JDK:1.6.0_32
Tomcat:apache-tomcat-7.0.27
开发工具:MyEclipse 10
浏览器:Internet Explorer 9

 

生成**库和证书
可参考以下**生成脚本,根据实际情况做必要的修改,其中需要注意的是:服务端的**库参数“CN”必须与服务端的IP地址相同,否则会报错,客户端的任意。

此间要注意,提醒的域名可以随意写一个泛域名,在本机测试可以修改host文件。

1、生成服务器证书库

keytool -validity 365 -genkey -v -alias server -keyalg RSA -keystore E:\ssl\server.keystore -dname "CN=127.0.0.1,OU=icesoft,O=icesoft,L=Haidian,ST=Beijing,c=cn" -storepass 123456 -keypass 123456


2、生成客户端证书库

keytool -validity 365 -genkeypair -v -alias client -keyalg RSA -storetype PKCS12 -keystore E:\ssl\client.p12 -dname "CN=client,OU=icesoft,O=icesoft,L=Haidian,ST=Beijing,c=cn" -storepass 123456 -keypass 123456


3、从客户端证书库中导出客户端证书

keytool -export -v -alias client -keystore E:\ssl\client.p12 -storetype PKCS12 -storepass 123456 -rfc -file E:\ssl\client.cer


4、从服务器证书库中导出服务器证书

keytool -export -v -alias server -keystore E:\ssl\server.keystore -storepass 123456 -rfc -file E:\ssl\server.cer


5、生成客户端信任证书库(由服务端证书生成的证书库)

keytool -import -v -alias server -file E:\ssl\server.cer -keystore E:\ssl\client.truststore -storepass 123456


6、将客户端证书导入到服务器证书库(使得服务器信任客户端证书)

keytool -import -v -alias client -file E:\ssl\client.cer -keystore E:\ssl\server.keystore -storepass 123456


7、查看证书库中的全部证书

keytool -list -keystore E:\ssl\server.keystore -storepass 123456

 

Tomat 配置双向认真

使用文本编辑器编辑${catalina.base}/conf/server.xml
找到Connector port="8443"的标签,取消注释,并修改成如下:

<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" SSLEnabled="true"
               maxThreads="150" scheme="https" secure="true"
               clientAuth="true" sslProtocol="TLS"
               keystoreFile="${catalina.base}/key/server.keystore" keystorePass="123456"
               truststoreFile="${catalina.base}/key/server.keystore" truststorePass="123456"/>

备注:
keystoreFile:指定服务器**库,可以配置成绝对路径,如“D:/key/server.keystore”,本例中是在Tomcat目录中创建了一个名称为key的文件夹,仅供参考。
keystorePass:**库生成时的密码
truststoreFile:受信任**库,和**库相同即可
truststorePass:受信任**库密码

注意:clientAuth="true"即代表双向认证,true必填truststoreFile客户端证书信任。

 

建立演示项目
项目结构图:
项目名称:SSL(随意)
SSL Tomcat 双向认证

SSLServlet.java

package com.icesoft.servlet;

import java.io.IOException;
import java.io.PrintWriter;
import java.security.cert.X509Certificate;

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

/**
 * <p>
 * SSL Servlet
 * </p>
 * 
 * @author IceWee
 * @date 2012-6-4
 * @version 1.0
 */
public class SSLServlet extends HttpServlet {

    private static final long serialVersionUID = 1601507150278487538L;
    private static final String ATTR_CER = "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";

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType(CONTENT_TYPE);
        response.setCharacterEncoding(DEFAULT_ENCODING);
        PrintWriter out = response.getWriter();
        X509Certificate[] certs = (X509Certificate[]) request.getAttribute(ATTR_CER);
        if (certs != null) {
            int count = certs.length;
            out.println("共检测到[" + count + "]个客户端证书");
            for (int i = 0; i < count; i++) {
                out.println("客户端证书 [" + (++i) + "]: ");
                out.println("校验结果:" + verifyCertificate(certs[--i]));
                out.println("证书详细:\r" + certs[i].toString());
            }
        } else {
            if (SCHEME_HTTPS.equalsIgnoreCase(request.getScheme())) {
                out.println("这是一个HTTPS请求,但是没有可用的客户端证书");
            } else {
                out.println("这不是一个HTTPS请求,因此无法获得客户端证书列表 ");
            }
        }
        out.close();
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
    
    /**
     * <p>
     * 校验证书是否过期
     * </p>
     * 
     * @param certificate
     * @return
     */
    private boolean verifyCertificate(X509Certificate certificate) {
        boolean valid = true;
        try {
            certificate.checkValidity();
        } catch (Exception e) {
            e.printStackTrace();
            valid = false;
        }
        return valid;
    }

}


web.xml
说明:该演示项目强制使用了SSL,即普通的HTTP请求也会强制重定向为HTTPS请求,配置在最下面,可以去除,这样HTTP和HTTPS都可以访问。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" 
    xmlns="http://java.sun.com/xml/ns/javaee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
      <display-name>Secure Sockets Layer</display-name>    
    
    <servlet>
        <servlet-name>SSLServlet</servlet-name>
        <servlet-class>com.icesoft.servlet.SSLServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>SSLServlet</servlet-name>
        <url-pattern>/sslServlet</url-pattern>
    </servlet-mapping>
    
    <welcome-file-list>
      <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>

    <!-- 强制SSL配置,即普通的请求也会重定向为SSL请求 -->  
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>SSL</web-resource-name>
            <url-pattern>/*</url-pattern><!-- 全站使用SSL -->
        </web-resource-collection>
        <user-data-constraint>
            <description>SSL required</description>
            <!-- CONFIDENTIAL: 要保证服务器和客户端之间传输的数据不能够被修改,且不能被第三方查看到 -->
            <!-- INTEGRAL: 要保证服务器和client之间传输的数据不能够被修改 -->
            <!-- NONE: 指示容器必须能够在任一的连接上提供数据。(即用HTTP或HTTPS,由客户端来决定)-->
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>
</web-app>


index.jsp 访问页面:

<%@ page language="java" pageEncoding="UTF-8"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>客户端证书上传</title>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">    
</head>
<body>
<form action="${pageContext.request.contextPath}/sslServlet" method="post">
    <input type="submit"  value="提交证书"/>
</form>
</body>
</html>



演示及配置
发布演示项目,通过浏览器访问:http://127.0.0.1:8080/SSLhttps://127.0.0.1:8443/SSL,得到相同的结果,如图:

SSL Tomcat 双向认证

SSL Tomcat 双向认证


得到如上结果的原始是因为客户端没有通过服务端的安全认证,说白了,系统就没有加载这个证书来请问服务器,根本没有发送客户端证书,接下来将服务端给客户端颁发的证书导入到浏览器中:

双击“client.p12”
SSL Tomcat 双向认证

弹出窗口,下一步
SSL Tomcat 双向认证

默认,下一步
SSL Tomcat 双向认证

输入生成**时的密码“123456”,下一步
SSL Tomcat 双向认证

下一步
SSL Tomcat 双向认证

完成
SSL Tomcat 双向认证

成功
SSL Tomcat 双向认证


再次访问http://127.0.0.1:8080/SSLhttps://127.0.0.1:8443/SSL,弹出提示框:
SSL Tomcat 双向认证


点击确定后,IE浏览器自动阻止了继续访问,并给予警告提示,原因是浏览器中未导入该网站的可信证书
SSL Tomcat 双向认证

SSL Tomcat 双向认证


点击“继续浏览此网站”,弹出提示,点击确定
SSL Tomcat 双向认证


哇!鲜红的地址栏,够醒目吧!你访问的网站不安全那,亲!
SSL Tomcat 双向认证


点击“提交证书”按钮,返回正确结果!
SSL Tomcat 双向认证


可以看出,客户端并没有服务端那么严格,只要未通过验证就甭想访问,下面将服务端生成的信任证书导入到浏览器的根证书中,这样红色的地址栏就会消失了!
开始导入服务端信任证书,不能双击“server.cer”,需要手动导入到受信任的根证书机构中去。
SSL Tomcat 双向认证

浏览器Internet选项-内容-证书
SSL Tomcat 双向认证


点击“受信任的根证书颁发机构”
SSL Tomcat 双向认证


点击“导入”
SSL Tomcat 双向认证


下一步
SSL Tomcat 双向认证


手动选择“server.cer”,下一步

SSL Tomcat 双向认证


下一步
SSL Tomcat 双向认证


完成
SSL Tomcat 双向认证

点“是”

SSL Tomcat 双向认证


成功
SSL Tomcat 双向认证


可以看到我们刚刚导入的根证书
SSL Tomcat 双向认证


把所有浏览器窗口都关掉,再次访问网站,发现鲜红色已经逝去
SSL Tomcat 双向认证


点击“提交证书”按钮,一切正常了,双向认证的DEMO结束了!
SSL Tomcat 双向认证

 

总结疑点:

1、确信已经明白单向和双向认证的区别,在双向认证中,客户端请求必须带有客户端证书,这也就解释了很多网站是单向认证,使用HttpClient等第三方库,可以不携带证书而直接请求https链接的原因所在;

2、请勿钻于各种证书的格式,在前一片文章中,罗列了很多证书的格式,说白了,他们都是存储钥匙的文件而已,只不过不同的服务器所需要的证书格式不同,这也是不同厂商解析不同的原因而已;

3、要明白ca证书为什么要付费,因为在系统中存在着默认信任的机构,如果不付费,我们还需要用户将我们的ca根证书安装在系统中;

4、CA证书中,有不同等级划分,例如OV,EV等,不同的区别自行查阅即可;

5、再次给出两个程序,分别为服务端开启验证和客户端携带证书请求的Java实例:

package org.fanmi.JavaDemo;

import java.io.FileInputStream;
import java.io.*;
import java.net.Socket;
import java.security.KeyStore;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;

public class KeystoreTest {

    /**
     * name:KeystoreTest
     * author:suju
     */
    public static void main(String[] args) throws Exception {
        String key = "E:\\ssl\\server.keystore";
        KeyStore keystore = KeyStore.getInstance("JKS");
        // keystore的类型,默认是jks
        keystore.load(new FileInputStream(key), "123456".toCharArray());
        // 创建jkd**访问库 123456是keystore密码。
        KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
        kmf.init(keystore, "123456".toCharArray());
        // asdfgh是key密码。
        // 创建管理jks**库的x509**管理器,用来管理**,需要key的密码
        SSLContext sslc = SSLContext.getInstance("SSLv3");
        // 构造SSL环境,指定SSL版本为3.0,也可以使用TLSv1,但是SSLv3更加常用。
        sslc.init(kmf.getKeyManagers(), null, null);
        // 第二个参数TrustManager[] 是认证管理器,在需要双向认证时使用,
        // 构造ssl环境

        SSLServerSocketFactory sslfactory = sslc.getServerSocketFactory();
        SSLServerSocket serversocket = (SSLServerSocket) sslfactory
                .createServerSocket(9999);
        // 创建serversocket,监听,并传输数据来验证授权
        for (int i = 0; i < 15; i++) {
            final Socket socket = serversocket.accept();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        InputStream is = socket.getInputStream();
                        OutputStream os = socket.getOutputStream();
                        byte[] buf = new byte[1024];
                        int len = is.read(buf);
                        System.out.println(new String(buf));
                        os.write("ssl test".getBytes());
                        os.close();
                        is.close();
                    } catch (Exception e) {
                    }
                }
            }).start();
        }
        serversocket.close();
    }
}
package org.fanmi.JavaDemo;

import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.KeyStore;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

public class KeystoreTestClient {

    public static void main(String[] args) throws Exception {
        String key = "E:\\ssl\\client.truststore";
        KeyStore keystore = KeyStore.getInstance("JKS"); // 创建一个keystore来管理**库
        keystore.load(new FileInputStream(key), "123456".toCharArray());
        // 创建jkd**访问库
        TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
        tmf.init(keystore); // 验证数据,可以不传入key密码
        // 创建TrustManagerFactory,管理授权证书
        SSLContext sslc = SSLContext.getInstance("SSLv3");
        // 构造SSL环境,指定SSL版本为3.0,也可以使用TLSv1,但是SSLv3更加常用。
        sslc.init(null, tmf.getTrustManagers(), null);
        // KeyManager[] 第一个参数是授权的**管理器,用来授权验证。第二个是被授权的证书管理器,
        // 用来验证服务器端的证书。只验证服务器数据,第一个管理器可以为null
        // 构造ssl环境

        SSLSocketFactory sslfactory = sslc.getSocketFactory();
        SSLSocket socket = (SSLSocket) sslfactory.createSocket("127.0.0.1", 9999);
        // 创建serversocket通过传输数据来验证授权

        InputStream is = socket.getInputStream();
        OutputStream os = socket.getOutputStream();
        os.write("client".getBytes());
        byte[] buf = new byte[1024];
        int len = is.read(buf);
        System.out.println(new String(buf));
        os.close();
        is.close();
    }

}

注意:KeyStore keystore=KeyStore.getInstance("JKS") 这句程序JKS参数代表着加载证书的方式,跟生成证书的格式一致即可。

完结

相关标签: SSL