Springboot Web应用中Tomcat SSL是如何工作的

本文详细介绍了SpringBoot如何加载SSL配置,并将其应用于servlet容器。从ServerProperties的加载到Tomcat容器的具体配置,再到SSL握手的具体实现,涵盖了整个流程。

Spring boot 加载SSL配置参数到servlet容器

SSL配置参数属于配置文件中以server开头的项(从字面可直观理解成这是Web服务器的配置参数),相应地,这类服务器配置参数的加载过程,就包含了对SSL配置参数,如keystore文件路径以及keystore的类型等配置参数的加载。

ServerProperties是用来加载服务器配置参数的工具类,它在容器启动时会被作为一个bean定义注册到容器,在容器启动过程中应用EmbeddedServletContainerCustomizerBeanPostProcessor的阶段,ServerProperties bean会被实例化,此时配置文件会被该bean读取,并被设置到目标TomcatEmbeddedServletContainerFactory 容器实例上去。

具体的调用入口点如下 :

SpringApplication.run()
 => refreshContext()
  => EmbeddedWebApplicationContext.refresh()
  => onRefresh()  
  => createEmbeddedServletContainer()
  => getEmbeddedServletContainerFactory()
    => AbstractBeanFactory.getBean("tomcatEmbeddedServletContainerFactory")
    => doGetBean()
	=> DefaultSingletonBeanRegistry.getSingleton()    
    => AbstractAutowireCapableBeanFactory.doCreateBean()
    => initializeBean() 
    => applyBeanPostProcessorsBeforeInitialization()

关于服务器配置文件加载更多的信息可以参考 :
Springboot Web应用中服务器配置参数ServerProperties的加载

Servlet容器应用SSL配置参数

应用入口点

SpringApplication.run()
 => refreshContext()
  => EmbeddedWebApplicationContext.refresh()
  => onRefresh()  
  => createEmbeddedServletContainer()   
   => TomcatEmbeddedServletContainerFactory.getEmbeddedServletContainer()
   => customizeConnector(connector)
   => customizeSsl(connector)

应用SSL配置到Connector和Http11NioProtocol实例

	//TomcatEmbeddedServletContainerFactory 方法
	private void customizeSsl(Connector connector) {
		// 这里handler缺省情况下是一个Http11NioProtocol实例
		ProtocolHandler handler = connector.getProtocolHandler();
		Assert.state(handler instanceof AbstractHttp11JsseProtocol,
				"To use SSL, the connector's protocol handler must be an "
						+ "AbstractHttp11JsseProtocol subclass");
		// 将从外部设置进来的SSL配置全部设置到handler上,也就是Http11NioProtocol实例上						
		configureSsl((AbstractHttp11JsseProtocol<?>) handler, getSsl());
		connector.setScheme("https");
		connector.setSecure(true);
	}
	// 配置Tomcat的AbstractHttp11JsseProtocol实例支持SSL
	// ssl 是从配置文件中加载得到的SSL配置参数信息
	protected void configureSsl(AbstractHttp11JsseProtocol<?> protocol, Ssl ssl) {
		protocol.setSSLEnabled(true);
		protocol.setSslProtocol(ssl.getProtocol());
		configureSslClientAuth(protocol, ssl);
		protocol.setKeystorePass(ssl.getKeyStorePassword());
		protocol.setKeyPass(ssl.getKeyPassword());
		protocol.setKeyAlias(ssl.getKeyAlias());
		String ciphers = StringUtils.arrayToCommaDelimitedString(ssl.getCiphers());
		protocol.setCiphers(StringUtils.hasText(ciphers) ? ciphers : null);
		if (ssl.getEnabledProtocols() != null) {
			try {
				for (SSLHostConfig sslHostConfig : protocol.findSslHostConfigs()) {
					sslHostConfig.setProtocols(StringUtils
							.arrayToCommaDelimitedString(ssl.getEnabledProtocols()));
				}
			}
			catch (NoSuchMethodError ex) {
				// Tomcat 8.0.x or earlier
				Assert.isTrue(
						protocol.setProperty("sslEnabledProtocols",
								StringUtils.arrayToCommaDelimitedString(
										ssl.getEnabledProtocols())),
						"Failed to set sslEnabledProtocols");
			}
		}
		if (getSslStoreProvider() != null) {
			TomcatURLStreamHandlerFactory instance = TomcatURLStreamHandlerFactory
					.getInstance();
			instance.addUserFactory(
					new SslStoreProviderUrlStreamHandlerFactory(getSslStoreProvider()));
			protocol.setKeystoreFile(
					SslStoreProviderUrlStreamHandlerFactory.KEY_STORE_URL);
			protocol.setTruststoreFile(
					SslStoreProviderUrlStreamHandlerFactory.TRUST_STORE_URL);
		}
		else {
			configureSslKeyStore(protocol, ssl);
			configureSslTrustStore(protocol, ssl);
		}
	}	

初始化SSL

SSL初始化入口点

SpringApplication.run()
 => refreshContext()
  => EmbeddedWebApplicationContext.refresh()
  => finishRefresh()  
  => startEmbeddedServletContainer()   
   => TomcatEmbeddedServletContainer.start()
   => addPreviouslyRemovedConnectors()
    => StandartService.addConnector()
     => Connector.start()
     => startInternal()
     => Http11NioProtocol protocolHandler.start()
      => NioEndpoint  endpoint.start()
      => bind()
      => initialiseSsl()

在类 org.apache.tomcat.util.net.NioEndpoint 中 :

  protected void initialiseSsl() throws Exception {
        if (isSSLEnabled()) {
            sslImplementation = SSLImplementation.getInstance(getSslImplementationName());

            for (SSLHostConfig sslHostConfig : sslHostConfigs.values()) {
                sslHostConfig.setConfigType(getSslConfigType());
                createSSLContext(sslHostConfig);
            }

            // Validate default SSLHostConfigName
            if (sslHostConfigs.get(getDefaultSSLHostConfigName()) == null) {
                throw new IllegalArgumentException(sm.getString("endpoint.noSslHostConfig",
                        getDefaultSSLHostConfigName(), getName()));
            }

        }
    }
    @Override
    protected void createSSLContext(SSLHostConfig sslHostConfig) throws IllegalArgumentException {
        boolean firstCertificate = true;
        for (SSLHostConfigCertificate certificate : sslHostConfig.getCertificates(true)) {
            SSLUtil sslUtil = sslImplementation.getSSLUtil(certificate);
            if (firstCertificate) {
                firstCertificate = false;
                sslHostConfig.setEnabledProtocols(sslUtil.getEnabledProtocols());
                sslHostConfig.setEnabledCiphers(sslUtil.getEnabledCiphers());
            }

            SSLContext sslContext;
            try {
                sslContext = sslUtil.createSSLContext(negotiableProtocols);
                sslContext.init(sslUtil.getKeyManagers(), sslUtil.getTrustManagers(), null);
            } catch (Exception e) {
                throw new IllegalArgumentException(e);
            }

            SSLSessionContext sessionContext = sslContext.getServerSessionContext();
            if (sessionContext != null) {
                sslUtil.configureSessionContext(sessionContext);
            }
            certificate.setSslContext(sslContext);
        }
    }    

Tomcat被客户端请求触发SSL握手handshake

请求到达时Tomcat接收器线程Acceptor创建SecureNioChannel

Acceptor 是 Tomcat NioEndpoint类的内部类,它工作在Tomcat的接收器线程,当它经监听到外来连接时,调用setSocketOptions(),设置相应的参数,构建相应的工作组件,然后把实际的处理任务委托给合适的SocketProcessor 来处理。

    protected class Acceptor extends AbstractEndpoint.Acceptor {

        @Override
        public void run() {

            int errorDelay = 0;

            // Loop until we receive a shutdown command
            while (running) {

                // Loop if endpoint is paused
                while (paused && running) {
                    state = AcceptorState.PAUSED;
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        // Ignore
                    }
                }

                if (!running) {
                    break;
                }
                state = AcceptorState.RUNNING;

                try {
                    //if we have reached max connections, wait
                    countUpOrAwaitConnection();

                    SocketChannel socket = null;
                    try {
                        // Accept the next incoming connection from the server
                        // socket
                        socket = serverSock.accept();
                    } catch (IOException ioe) {
                        // We didn't get a socket
                        countDownConnection();
                        if (running) {
                            // Introduce delay if necessary
                            errorDelay = handleExceptionWithDelay(errorDelay);
                            // re-throw
                            throw ioe;
                        } else {
                            break;
                        }
                    }
                    // Successful accept, reset the error delay
                    errorDelay = 0;

                    // Configure the socket
                    if (running && !paused) {
                        // setSocketOptions() will hand the socket off to
                        // an appropriate processor if successful
                        if (!setSocketOptions(socket)) {
                            closeSocket(socket);
                        }
                    } else {
                        closeSocket(socket);
                    }
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    log.error(sm.getString("endpoint.accept.fail"), t);
                }
            }
            state = AcceptorState.ENDED;
        }


        private void closeSocket(SocketChannel socket) {
            countDownConnection();
            try {
                socket.socket().close();
            } catch (IOException ioe)  {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("endpoint.err.close"), ioe);
                }
            }
            try {
                socket.close();
            } catch (IOException ioe) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("endpoint.err.close"), ioe);
                }
            }
        }
    }

setSocketOptions()是Tomcat NioEndpoint类的方法,用来处理接收线程中接收器接收到的某个特定的连接socket , 从下面代码可以看出,setSocketOptions() 自身的逻辑工作在接收线程中,但是该方法准备好相应的参数传递和组件准备之后,具体的处理任务注册到了Poller上,而不是直接由当前接收器线程执行,这是Tomcat的架构设计决定的,接收器线程仅负责请求连接的接收和转发,具体的处理由Poller交给Tomcat worker线程来处理 :

    protected boolean setSocketOptions(SocketChannel socket) {
        // Process the connection
        try {
            //disable blocking, APR style, we are gonna be polling it
            socket.configureBlocking(false);
            Socket sock = socket.socket();
            socketProperties.setProperties(sock);

            NioChannel channel = nioChannels.pop();
            if (channel == null) {
                SocketBufferHandler bufhandler = new SocketBufferHandler(
                        socketProperties.getAppReadBufSize(),
                        socketProperties.getAppWriteBufSize(),
                        socketProperties.getDirectBuffer());
                if (isSSLEnabled()) {
	                // 如果SSL被启动,创建的channel对象是一个SecureNioChannel
                    channel = new SecureNioChannel(socket, bufhandler, selectorPool, this);
                } else {
	                // 如果SSL没有被启动,创建的channel对象是一个NioChannel
                    channel = new NioChannel(socket, bufhandler);
                }
            } else {
                channel.setIOChannel(socket);
                channel.reset();
            }
            getPoller0().register(channel);
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            try {
                log.error("",t);
            } catch (Throwable tt) {
                ExceptionUtils.handleThrowable(tt);
            }
            // Tell to close the socket
            return false;
        }
        return true;
    }

NioChannel和SecureNioChannel是Tomcat提供的类,都位于以下包 :

org.apache.tomcat.util.net

SecureNioChannel继承自NioChannel,NioChannel用于处理HTTP,而SecureNioChannel用于处理HTTPS(内部基于SSL)。

NioChannel是Tomcat对Java NIO SocketChannel的封装类,也是Tomcat endpoint用于操作Java NIO SocketChannel的基础类。虽然NioChannel不提供处理SSL的实际业务逻辑,但是它也定义了处理SSL的公开方法,而SecureNioChannel提供了具体的实现逻辑。通过这种方式,不管是SSL还是非SSL的情况,Tomcat endpoint都可以基于基类NioChannel采用同样的逻辑来处理。

NioChannel实现了 Java NIO 接口 ByteChannel,拿到一个NioChannel实例的使用者可以将其作为一个ByteChannel操作,但实际上NioChannel将这些操作都委托到了所封装的Java NIO SocketChannel实例上面。

Tomcat worker线程执行握手逻辑

	// Tomcat worker 线程的执行逻辑由 Tomcat NioEndpoint内部类SocketProcessor提供,如下
    protected class SocketProcessor extends SocketProcessorBase<NioChannel> {

        public SocketProcessor(SocketWrapperBase<NioChannel> socketWrapper, SocketEvent event) {
            super(socketWrapper, event);
        }

        @Override
        protected void doRun() {
            NioChannel socket = socketWrapper.getSocket();
            SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());

            try {
	            // 用于记录SSL handshake是否完成的局部变量,初始化为-1
	            // handshake逻辑成功或者不需要handshake逻辑时,该变量设置为0,
	            // handshake逻辑失败时,该变量设置为-1
                int handshake = -1;

                try {
                    if (key != null) {
	                    // 针对非SSL,该方法总是返回true,
	                    // 针对SSL,handshake完成后该方法返回true
                        if (socket.isHandshakeComplete()) {
                            // No TLS handshaking required. Let the handler
                            // process this socket / event combination.
                            handshake = 0;
                        } else if (event == SocketEvent.STOP || event == SocketEvent.DISCONNECT ||
                                event == SocketEvent.ERROR) {
                            // Unable to complete the TLS handshake. Treat it as
                            // if the handshake failed.
                            handshake = -1;
                        } else {
	                        // 如果是SSL,并且handshake尚未完成,则先做handshake
                            handshake = socket.handshake(key.isReadable(), key.isWritable());
                            // The handshake process reads/writes from/to the
                            // socket. status may therefore be OPEN_WRITE once
                            // the handshake completes. However, the handshake
                            // happens when the socket is opened so the status
                            // must always be OPEN_READ after it completes. It
                            // is OK to always set this as it is only used if
                            // the handshake completes.
                            event = SocketEvent.OPEN_READ;
                        }
                    }
                } catch (IOException x) {
                    handshake = -1;// 因为IOException异常标记SSL握手失败
                    if (log.isDebugEnabled()) log.debug("Error during SSL handshake",x);
                } catch (CancelledKeyException ckx) {
                    handshake = -1;//因为CancelledKeyException异常标记SSL握手失败
                }
                if (handshake == 0) {
	                // 1. 不需要握手(非SSL情况)
	                // 2. 或者握手成功(SSL情况并且握手逻辑执行成功)
                    SocketState state = SocketState.OPEN;
                    // Process the request from this socket
                    // 1.如果是SSL的情况,SSL握手已经做完,现在可以开始处理客户数据了;
                    // 2.如果是非SSL的情况,可以直接开始处理客户数据了;
                    // 下面的getHandler()返回一个AbstractProtocol.ConnectionHandler
                    // 实例,对业务数据的处理都交由其process()方法继续完成
                    if (event == null) {
                        state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
                    } else {
                        state = getHandler().process(socketWrapper, event);
                    }
                    if (state == SocketState.CLOSED) {
                        close(socket, key);
                    }
                } else if (handshake == -1 ) {
                    close(socket, key);
                } else if (handshake == SelectionKey.OP_READ){
                    socketWrapper.registerReadInterest();
                } else if (handshake == SelectionKey.OP_WRITE){
                    socketWrapper.registerWriteInterest();
                }
            } catch (CancelledKeyException cx) {
                socket.getPoller().cancelledKey(key);
            } catch (VirtualMachineError vme) {
                ExceptionUtils.handleThrowable(vme);
            } catch (Throwable t) {
                log.error("", t);
                socket.getPoller().cancelledKey(key);
            } finally {
                socketWrapper = null;
                event = null;
                //return to cache
                if (running && !paused) {
                    processorCache.push(this);
                }
            }
        }
    }

上面Tomcat worker线程主逻辑首先检测是否需要做SSL handshake,如果需要做的话,是交给 SecureNioChannel 的handshake方法来做的 :

    /**
     * Performs SSL handshake, non blocking, but performs NEED_TASK on the same
     * thread. Hence, you should never call this method using your Acceptor
     * thread, as you would slow down your system significantly. If the return
     * value from this method is positive, the selection key should be
     * registered interestOps given by the return value.
     *
     * @param read boolean - true if the underlying channel is readable
     * @param write boolean - true if the underlying channel is writable
     *
     * @return 0 if hand shake is complete, -1 if an error (other than an
     *         IOException) occurred, otherwise it returns a SelectionKey
     *         interestOps value
     *
     * @throws IOException If an I/O error occurs during the handshake or if the
     *                     handshake fails during wrapping or unwrapping
     */
    @Override
    public int handshake(boolean read, boolean write) throws IOException {
        if (handshakeComplete) {
            return 0; //we have done our initial handshake
        }

        if (!sniComplete) {
            int sniResult = processSNI();
            if (sniResult == 0) {
                sniComplete = true;
            } else {
                return sniResult;
            }
        }

        if (!flush(netOutBuffer)) return SelectionKey.OP_WRITE; //we still have data to write

        SSLEngineResult handshake = null;

        while (!handshakeComplete) {
            switch ( handshakeStatus ) {
                case NOT_HANDSHAKING: {
                    //should never happen
                    throw new IOException(sm.getString("channel.nio.ssl.notHandshaking"));
                }
                case FINISHED: {
                    if (endpoint.hasNegotiableProtocols() && sslEngine instanceof SSLUtil.ProtocolInfo) {
                        socketWrapper.setNegotiatedProtocol(
                                ((SSLUtil.ProtocolInfo) sslEngine).getNegotiatedProtocol());
                    }
                    //we are complete if we have delivered the last package
                    handshakeComplete = !netOutBuffer.hasRemaining();
                    //return 0 if we are complete, otherwise we still have data to write
                    return handshakeComplete?0:SelectionKey.OP_WRITE;
                }
                case NEED_WRAP: {
                    //perform the wrap function
                    try {
                        handshake = handshakeWrap(write);
                    } catch (SSLException e) {
                        if (log.isDebugEnabled()) {
                            log.debug(sm.getString("channel.nio.ssl.wrapException"), e);
                        }
                        handshake = handshakeWrap(write);
                    }
                    if (handshake.getStatus() == Status.OK) {
                        if (handshakeStatus == HandshakeStatus.NEED_TASK)
                            handshakeStatus = tasks();
                    } else if (handshake.getStatus() == Status.CLOSED) {
                        flush(netOutBuffer);
                        return -1;
                    } else {
                        //wrap should always work with our buffers
                        throw new IOException(sm.getString("channel.nio.ssl.unexpectedStatusDuringWrap", handshake.getStatus()));
                    }
                    if ( handshakeStatus != HandshakeStatus.NEED_UNWRAP || (!flush(netOutBuffer)) ) {
                        //should actually return OP_READ if we have NEED_UNWRAP
                        return SelectionKey.OP_WRITE;
                    }
                    //fall down to NEED_UNWRAP on the same call, will result in a
                    //BUFFER_UNDERFLOW if it needs data
                }
                //$FALL-THROUGH$
                case NEED_UNWRAP: {
                    //perform the unwrap function
                    handshake = handshakeUnwrap(read);
                    if ( handshake.getStatus() == Status.OK ) {
                        if (handshakeStatus == HandshakeStatus.NEED_TASK)
                            handshakeStatus = tasks();
                    } else if ( handshake.getStatus() == Status.BUFFER_UNDERFLOW ){
                        //read more data, reregister for OP_READ
                        return SelectionKey.OP_READ;
                    } else if (handshake.getStatus() == Status.BUFFER_OVERFLOW) {
                        getBufHandler().configureReadBufferForWrite();
                    } else {
                        throw new IOException(sm.getString("channel.nio.ssl.unexpectedStatusDuringWrap", handshakeStatus));
                    }//switch
                    break;
                }
                case NEED_TASK: {
                    handshakeStatus = tasks();
                    break;
                }
                default: throw new IllegalStateException(sm.getString("channel.nio.ssl.invalidStatus", handshakeStatus));
            }
        }
        // Handshake is complete if this point is reached
        return 0;
    }

关于整个握手过程的JSSE定义,请参考 :
Java Secure Socket Extension (JSSE) Reference Guide中小节Generating and Processing SSL/TLS Data

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值