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

被折叠的 条评论
为什么被折叠?



