SSL层协议

SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。SSL协议可分为两层: SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。 SSL握手协议(SSL Handshake Protocol):它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。

SSL加密通道上的数据加解密使用对称密钥算法,目前主要支持的算法有DES、3DES、AES等,这些算法都可以有效地防止交互数据被破解。对称密钥算法要求解密密钥和加密密钥完全一致。因此,利用对称密钥算法加密传输数据之前,需要在通信两端部署相同的密钥。

例如AES就是在解密和加密信息时利用同一个Key,去解密加密,且aes8 、aes16、aes32,aes8代表以8个字节为单位进行加密和解密,具体看不同的算法实现。

SSL利用基于MD5或SHA的MAC算法来保证消息的完整性。MAC算法是在密钥参与下的数据摘要算法,能将密钥和任意长度的数据转换为固定长度的数据。利用MAC算法验证消息完整性的过程如下图所示。发送者在密钥的参与下,利用MAC算法计算出消息的MAC值,并将其加在消息之后发送给接收者。接收者利用同样的密钥和MAC算法计算出消息的MAC值,并与接收到的MAC值比较。如果二者相同,则报文没有改变;否则,报文在传输过程中被修改,接收者将丢弃该报文。MAC算法要求通信双方具有相同的密钥,否则MAC值验证将会失败。因此,利用MAC算法验证消息完整性之前,需要在通信两端部署相同的密钥

非对称性加密的好处,防止第三方获取密钥

SSL客户端(如Web浏览器)利用SSL服务器(如Web服务器)的公钥加密密钥,将加密后的密钥发送给SSL服务器,只有拥有对应私钥的SSL服务器才能从密文中获取原始的密钥。SSL通常采用RSA算法加密传输密钥。(Server端公钥加密密钥,私钥解密密钥)

实际上,SSL客户端发送给SSL服务器的密钥不能直接用来加密数据或计算MAC值,该密钥是用来计算对称密钥和MAC密钥的信息,称为premaster secret。

权威机构验证公钥的身份

数字证书(简称证书)是一个包含用户的公钥及其身份信息的文件,证明了用户与公钥的关联。数字证书由权威机构——CA签发,并由CA保证数字证书的真实性。

验证SSL服务器/SSL客户端的身份之前,SSL服务器/SSL客户端需要将从CA获取的证书发送给对端,对端通过PKI判断该证书的真实性。如果该证书确实属于SSL服务器/SSL客户端,则对端利用该证书中的公钥验证SSL服务器/SSL客户端的身份。

(1)保密性:利用握手协议所定义的共享密钥对SSL净荷(Payload)加密。

(2)完整性:利用握手协议所定义的共享的MAC密钥来生成报文的鉴别码(MAC)。

SSL的工作过程如下。

(1)发送方的工作过程为:

从上层接受要发送的数据(包括各种消息和数据);

对信息进行分段,分成若干记录;

使用指定的压缩算法进行数据压缩(可选);

使用指定的MAC算法生成MAC;

使用指定的加密算法进行数据加密;

添加SSL记录协议的头,发送数据。

(2)接收方的工作过程为:

接收数据,从SSL记录协议的头中获取相关信息;

使用指定的解密算法解密数据

使用指定的MAC算法校验MAC;

使用压缩算法对数据解压缩(在需要进行);

将记录进行数据重组;

将数据发送给高层。

双向验证过程(服务端和客户端)

  在看完上边单双向认证过程的区别之后,会发现在认证过程中,有公钥和私钥的概念,而何时谁应该持有谁的公钥或者私钥呢?

1、如果客户端想验证服务端证书,客户端需要安装服务端的公钥文件(cer)(或者服务端证书是官方CA颁发的,客户端可以直接联网认证),因为服务端会将自己的随机数等信息使用自己的私钥加密之后发给客户端,而客户端要想解开这些数据,必须持有服务端的公钥才可以,之后服务端验证通过。

2、服务端想验证客户端证书,则需要将客户端的证书的公钥文件放到服务端trustStore信任库中,当客户端请求访问服务端时,会使用自己的私钥加密随机数、ssl版本等信息发送给服务端,服务端只有持有客户端的公钥才能解开这些数据,验证才能通过。

springboot SSL配置

配置application.properties

#端口号
server.port: 8443
#你生成的证书名字
server.ssl.key-store: E:\work\rave\tomcat.keystore
#密钥库密码
server.ssl.key-store-password: duan123
server.ssl.keyStoreType: JKS
server.ssl.keyAlias: tomcat

证书由可以使自签名或者从SSL证书授权中心获得的。有对应的插件。JDK中keytool是一个证书管理工具,可以生成自签名证书。

keystore和truststore从其文件格式来看其实是一个东西,只是为了方便管理将其分开
keystore中一般保存的是我们的私钥,用来加解密或者为别人做签名

key就是公钥,私钥,数字签名等组成的一个信息。用来存放服务端证书,

truststore和keystore的性质是一样的,都是存放key的一个仓库,区别在于,truststore里存放的是只包含公钥的数字证书(放服务端信任的客户端证书),代表了可以信任的证书,

双向ssl验证

java SSL证书双向认证 双向ssl证书 原理_java SSL证书双向认证

   keyStore密钥库,存放了客户端信任的服务端证书的私钥、公钥和证书。

        server.port=8080

        server.ssl.key-store=classpath:a.pfx

        server.ssl.key-store-password=aaa123

        server.ssl.key-alias=1

        server.ssl.keyStoreType=JKS

   trustStore信任库,存放了服务端信任的客户端证书的公钥文件

        server.ssl.trust-store=classpath:b.pfx

        server.ssl.trust-store-password=aaa123

        server.ssl.client-auth=need

        server.ssl.trust-store-type=JKS

        server.ssl.trust-store-provider=SUN

 

 
truststore中保存的是一些可信任的证书,主要是java在代码中访问某个https的时候对被访问者进行认证的,以确保其实可信任的。

@SpringBootApplication
public class SpringbootmyApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootmyApplication.class, args);
    }

    /**
     * it's for set http url auto change to https
     */
    @Bean
    public EmbeddedServletContainerFactory servletContainer(){
        TomcatEmbeddedServletContainerFactory tomcat=new TomcatEmbeddedServletContainerFactory(){
            @Override
            protected void postProcessContext(Context context) {
                SecurityConstraint securityConstraint=new SecurityConstraint();
                securityConstraint.setUserConstraint("CONFIDENTIAL");//confidential
                SecurityCollection collection=new SecurityCollection();
                collection.addPattern("/*");
                securityConstraint.addCollection(collection);
                context.addConstraint(securityConstraint);
            }
        };
        tomcat.addAdditionalTomcatConnectors(httpConnector());
        return tomcat;
    }

    @Bean
    public Connector httpConnector(){
        Connector connector=new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("http");
        connector.setPort(8080);
        connector.setSecure(false);
        connector.setRedirectPort(server.port);
        return connector;
    }

}

此时运行http://localhost:8080,会自动跳转到https://localhost:8443

 

spring 利用web.xml配置的写法

<!-- Define an SSL Coyote HTTP/1.1 Connector on port 8443 -->
<Connector
           protocol="org.apache.coyote.http11.Http11AprProtocol"
           port="8443" maxThreads="200"
           scheme="https" secure="true" SSLEnabled="true"
           SSLCertificateFile="/usr/local/ssl/server.crt"
           SSLCertificateKeyFile="/usr/local/ssl/server.pem"
           SSLVerifyClient="optional" SSLProtocol="TLSv1+TLSv1.1+TLSv1.2"/>

 

springboot加载sslkeystore和ssltruststore源码

application.yml配置对应的是ServerProperties,这是个配置类,对应的就是服务器的相关配置。

@ConfigurationProperties(
    prefix = "server",  //读取application.yml配置类
    ignoreUnknownFields = true
)
public class ServerProperties {
    private Integer port;
    private InetAddress address;
    @NestedConfigurationProperty
    private final ErrorProperties error = new ErrorProperties();
    private Boolean useForwardHeaders;
    private String serverHeader;
    private DataSize maxHttpHeaderSize = DataSize.ofKilobytes(8L);
    private Duration connectionTimeout;
    @NestedConfigurationProperty
    private Ssl ssl; //读完配置之后写入ssl的相关keystore url字段
    @NestedConfigurationProperty
    private final Compression compression = new Compression();
    @NestedConfigurationProperty
    private final Http2 http2 = new Http2();
    private final ServerProperties.Servlet servlet = new ServerProperties.Servlet();
    private final ServerProperties.Tomcat tomcat = new ServerProperties.Tomcat();
    private final ServerProperties.Jetty jetty = new ServerProperties.Jetty();
    private final ServerProperties.Netty netty = new ServerProperties.Netty();
    private final ServerProperties.Undertow undertow = new ServerProperties.Undertow();
.......
//在TomcatServletWebServerFactory里有一个SslConnectorCustomizer类对ssl进行初始化

  if(sslStoreProvider != null) {
//自定义的Keystore
            this.configureSslStoreProvider(protocol, sslStoreProvider);
        } else {
//默认Keysote
            this.configureSslKeyStore(protocol, ssl);
            this.configureSslTrustStore(protocol, ssl);
        }

    }

    private void configureSslClientAuth(AbstractHttp11JsseProtocol<?> protocol, Ssl ssl) {
        if(ssl.getClientAuth() == ClientAuth.NEED) {
            protocol.setClientAuth(Boolean.TRUE.toString());
        } else if(ssl.getClientAuth() == ClientAuth.WANT) {
            protocol.setClientAuth("want");
        }

    }

    protected void configureSslStoreProvider(AbstractHttp11JsseProtocol<?> protocol, SslStoreProvider sslStoreProvider) {
        Assert.isInstanceOf(Http11NioProtocol.class, protocol, "SslStoreProvider can only be used with Http11NioProtocol");
        TomcatURLStreamHandlerFactory instance = TomcatURLStreamHandlerFactory.getInstance();
        instance.addUserFactory(new SslStoreProviderUrlStreamHandlerFactory(sslStoreProvider));

        try {
            if(sslStoreProvider.getKeyStore() != null) {
                protocol.setKeystorePass("");
                protocol.setKeystoreFile("springbootssl:keyStore");
            }

            if(sslStoreProvider.getTrustStore() != null) {
                protocol.setTruststorePass("");
                protocol.setTruststoreFile("springbootssl:trustStore");
            }

手动载入ssl相关配置文件

Connector类,(参考tomacat)tomcat初始化时Connector组件时,Connector会关联和初始化protocolHandler.connnector类会启动一个socket监听请求,例如8080端口。它具有许多可配置的参数,例如port、redirectport、protocol(处理请求的类有一个参数会默认的connector: useAprConnector)

 

public class Connector extends LifecycleMBeanBase {
    private static final Log log = LogFactory.getLog(Connector.class);
    public static final boolean RECYCLE_FACADES = Boolean.parseBoolean(System.getProperty("org.apache.catalina.connector.RECYCLE_FACADES", "false"));
    public static final String INTERNAL_EXECUTOR_NAME = "Internal";
    protected Service service;
    protected boolean allowTrace;
    protected long asyncTimeout;
    protected boolean enableLookups;
    protected boolean xpoweredBy;
    protected String proxyName;
    protected int proxyPort;
    protected boolean discardFacades;
//重定向,若不为http请求
    protected int redirectPort;
    protected String scheme;
    protected boolean secure;
    protected static final StringManager sm = StringManager.getManager(Connector.class);
    private int maxCookieCount;
    protected int maxParameterCount;
    protected int maxPostSize;
    protected int maxSavePostSize;
    protected String parseBodyMethods;
....

可以讲connector理解成一个支持Http1.1等协议的请求处理组件,该组件具有start\stop\init方法等,控制该组件启动、停止,该组件会为每一个请求启动一个线程去处理,若超过最大的线程池,则会扩大线程,若还是超过则会引发错误请求。

initInternal方法主要做了以下几件事:

  • 创建一个CoyoteAdapter并关联到此Connector上;(Adapter连接了Tomcat连接器Connector和容器Container.它的实现类是CoyoteAdapter主要负责的是对请求进行封装,构造Request和Response对象.并将请求转发给Container也就是Servlet容器.)
  • 初始化此Connector的protocolHandler。
@Override
protected void initInternal() throws LifecycleException {
    super.initInternal();
    // Initialize adapter
    adapter = new CoyoteAdapter(this);
    protocolHandler.setAdapter(adapter);
    // Make sure parseBodyMethodsSet has a default
    if (null == parseBodyMethodsSet) {
        setParseBodyMethods(getParseBodyMethods());
    }
    if (protocolHandler.isAprRequired() && !AprLifecycleListener.isAprAvailable()) {
        throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerNoApr",
                getProtocolHandlerClassName()));
    }
    if (AprLifecycleListener.isAprAvailable() && AprLifecycleListener.getUseOpenSSL() &&
            protocolHandler instanceof AbstractHttp11JsseProtocol) {
        AbstractHttp11JsseProtocol<?> jsseProtocolHandler =
                (AbstractHttp11JsseProtocol<?>) protocolHandler;
        if (jsseProtocolHandler.isSSLEnabled() &&
                jsseProtocolHandler.getSslImplementationName() == null) {
            // OpenSSL is compatible with the JSSE configuration, so use it if APR is available
            jsseProtocolHandler.setSslImplementationName(OpenSSLImplementation.class.getName());
        }
    }

    try {
        protocolHandler.init();
    } catch (Exception e) {
        throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
    }
}

Connector类的startInternal方法启动了关联的protocolHandler

为了使用具体的protocol而不是使用机制自动选择的默认useAprConnector,在Connector初始化时需要传入下列Protocol实现类.
org.apache.coyote.http11.Http11NioProtocol - non blocking Java NIO connector
org.apache.coyote.http11.Http11Nio2Protocol - non blocking Java NIO2 connector
org.apache.coyote.http11.Http11AprProtocol - the APR/native connector.(三者的区别)

java SSL证书双向认证 双向ssl证书 原理_spring boot_02

Http11NioProtocol分析ProtocolHandler的初始化和启动过程,Http11NioProtocol的类层次结构如下图所示。

java SSL证书双向认证 双向ssl证书 原理_spring boot_03

Http11NioProtocol对象在被构造时,为其自己关联了一个NioEndpoint,NioEndpoint负责处理tomcat的客户端的连接及数据处理的模型,Http11NioProtocol的init和start方法都在其父类AbstractProtocol中定义。

端点名称与ProtocolHandler实现有关,AbstractProtocol类中的getName、getNameInternal和getNamePrefix是用于获取ProtocolHandler名称的函数

@Override
protected String getNamePrefix() {
    if (isSSLEnabled()) {
        return ("https-" + getSslImplementationShortName()+ "-nio");
    } else {
        return ("http-nio");
    }
}

端点类的许多属性都是从abstracEndPoint中继承的,关于端点的属性如下所示,有许多线程池的东西。

public abstract class AbstractEndpoint<S, U> {
    protected static final StringManager sm = StringManager.getManager(AbstractEndpoint.class);
    protected volatile boolean running = false;
    protected volatile boolean paused = false;
    protected volatile boolean internalExecutor = true;
    private volatile LimitLatch connectionLimitLatch = null;
    protected final SocketProperties socketProperties = new SocketProperties();
    protected Acceptor<U> acceptor;
    protected SynchronizedStack<SocketProcessorBase<S>> processorCache;
    private ObjectName oname = null;
    protected Map<U, SocketWrapperBase<S>> connections = new ConcurrentHashMap();
//我们要找的sslconfig属性在这里
    private String defaultSSLHostConfigName = "_default_";
    protected ConcurrentMap<String, SSLHostConfig> sslHostConfigs = new ConcurrentHashMap();
    private boolean useSendfile = true;
    private long executorTerminationTimeoutMillis = 5000L;
    protected int acceptorThreadCount = 1;
    protected int acceptorThreadPriority = 5;
......

那ssl属性是什么时候赋值的呢?

以Spring为例,在解析server.xml时为Server/Service/Connector创建了一个ConnectorCreateRule和一个SetAllPropertiesRule
ConnectorCreateRule创建了Connector实例,并调用ProtocolHandler如AbstractProtocol的setExecutor方法将executor属性值引用的外部工作线程池设置到与AbstractProtocol关联的AbstractEndpoint上,sslImplementationName同理:

@Override
public void begin(String namespace, String name, Attributes attributes)
        throws Exception {
    Service svc = (Service)digester.peek();
    Executor ex = null;
    if ( attributes.getValue("executor")!=null ) {
        ex = svc.getExecutor(attributes.getValue("executor"));
    }
    Connector con = new Connector(attributes.getValue("protocol"));
    if (ex != null) {
        setExecutor(con, ex);
    }
    String sslImplementationName = attributes.getValue("sslImplementationName");
    if (sslImplementationName != null) {
        setSSLImplementationName(con, sslImplementationName);
    }
    digester.push(con);
}

private static void setExecutor(Connector con, Executor ex) throws Exception {
    Method m = IntrospectionUtils.findMethod(con.getProtocolHandler().getClass(),"setExecutor",new Class[] {java.util.concurrent.Executor.class});
    if (m!=null) {
        m.invoke(con.getProtocolHandler(), new Object[] {ex});
    }else {
        log.warn(sm.getString("connector.noSetExecutor", con));
    }
}

SetAllPropertiesRule这个规则只排除了executorsslImplementationName两个属性的赋值,并使用IntrospectionUtils.setProperty为属性赋值。Connector元素上可配置的属性列表可以参见官方文档,可以分成三种类型:

  • 只属于Connector,如scheme等;
  • 只属于EndPoint,如bindOnInit、acceptCount等;
  • 共存于Connector和EndPoint,如port、redirectPort等。

 

对于第一种属性,IntrospectionUtils.setProperty会调用恰当的setter方法;

TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
        connector.setScheme("http");
        connector.setPort(httpPort);
        connector.setSecure(false);
        connector.setRedirectPort(httpsPort);

对于第二种属性,IntrospectionUtils.setProperty会调用Connector的setProperty方法

connector.setProperty("relaxedPathChars", "xxxx");
            connector.setProperty("relaxedQueryChars", "xxx");
            connector.setProperty("connectionTimeout", "xx");
该方法会接着在ProtocolHandler上赋值,AbstractProtocol的setProperty方法如下:
public boolean setProperty(String name, String value) {
    return endpoint.setProperty(name, value);
}

接着调用AbstractEndPoint的setProperty方法,如果属性名以socket.开头那么将值设置socketProperties的对应属性上,否则设置到AbstractEndPoint的自身成员变量上

public boolean setProperty(String name, String value) {
    setAttribute(name, value);
    final String socketName = "socket.";
    try {
        if (name.startsWith(socketName)) {
            return IntrospectionUtils.setProperty(socketProperties, name.substring(socketName.length()), value);
        } else {
            return IntrospectionUtils.setProperty(this,name,value,false);
        }
    }catch ( Exception x ) {
        getLog().error("Unable to set attribute \""+name+"\" to \""+value+"\"",x);
        return false;
    }
}

对于第三种属性,上述两个赋值过程都会执行。

看了看虽然sslConfig实在endpoint,但是这三种都没给sslconfig类赋值,说明sslconfig类是endpoint自己默认初始并读取了springboot中application.yml中的 server的值,调用里面的接口进行了初始化。

但是connect提供了一个addsslconfig函数来传递自定义的sslconfig

SSLHostConfig sslConfig = new SSLHostConfig();keyStorePassword);
        sslConfig.setCertificateKeystoreFile(keyStoreFile);
        sslConfig.setCertificateKeyAlias(keyAlias);
        sslConfig.setCertificateKeystoreType(keyStoreType);
        sslConfig.setCiphers(ciphers);
        sslConfig.setEnabledProtocols(enabledProtocols);
        sslConfig.setProtocols(protocol);


        connector.addSslHostConfig(sslConfig);

因此sslconfig里的属性可以从这里初始化。初始化sslconfig并绑定给connector时你得需要讲sslEnabled参数设置为true

想要自己再更改ssl配置的可以在这里调用Http11NioProtocol的接口修改ssl相关属性

Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
            String pass = new BaseEncrypt().dbAesEncryptBase(keyStorePassword, -1);
            protocol.setKeystorePass(pass);