diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index d1af4329b76..40004ab7ec6 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -33,6 +33,7 @@ import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletCont import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizerBeanPostProcessor; import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; +import org.springframework.boot.context.embedded.Ssl; import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer; import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer; import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; @@ -59,6 +60,8 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer { private String contextPath; + private Ssl ssl; + @NotNull private String servletPath = "/"; @@ -132,6 +135,14 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer { this.sessionTimeout = sessionTimeout; } + public Ssl getSsl() { + return this.ssl; + } + + public void setSsl(Ssl ssl) { + this.ssl = ssl; + } + public void setLoader(String value) { // no op to support Tomcat running as a traditional container (not embedded) } @@ -150,12 +161,41 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer { if (getSessionTimeout() != null) { container.setSessionTimeout(getSessionTimeout()); } + if (getSsl() != null) { + container.setSsl(getSsl()); + } if (container instanceof TomcatEmbeddedServletContainerFactory) { getTomcat() .customizeTomcat((TomcatEmbeddedServletContainerFactory) container); } } + public String[] getPathsArray(Collection paths) { + String[] result = new String[paths.size()]; + int i = 0; + for (String path : paths) { + result[i++] = getPath(path); + } + return result; + } + + public String[] getPathsArray(String[] paths) { + String[] result = new String[paths.length]; + int i = 0; + for (String path : paths) { + result[i++] = getPath(path); + } + return result; + } + + public String getPath(String path) { + String prefix = getServletPrefix(); + if (!path.startsWith("/")) { + path = "/" + path; + } + return prefix + path; + } + public static class Tomcat { private String accessLogPattern; @@ -330,31 +370,4 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer { } } - - public String[] getPathsArray(Collection paths) { - String[] result = new String[paths.size()]; - int i = 0; - for (String path : paths) { - result[i++] = getPath(path); - } - return result; - } - - public String[] getPathsArray(String[] paths) { - String[] result = new String[paths.length]; - int i = 0; - for (String path : paths) { - result[i++] = getPath(path); - } - return result; - } - - public String getPath(String path) { - String prefix = getServletPrefix(); - if (!path.startsWith("/")) { - path = "/" + path; - } - return prefix + path; - } - } diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 0452cf7aeb6..a14029430b3 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -56,6 +56,18 @@ content into your application; rather pick only the properties that you need. server.session-timeout= # session timeout in seconds server.context-path= # the context path, defaults to '/' server.servlet-path= # the servlet path, defaults to '/' + server.ssl.client-auth= # want or need + server.ssl.key-alias= + server.ssl.key-password= + server.ssl.key-store= + server.ssl.key-store-password= + server.ssl.key-store-provider= + server.ssl.key-store-type= + server.ssl.protocol=TLS + server.ssl.trust-store= + server.ssl.trust-store-password= + server.ssl.trust-store-provider= + server.ssl.trust-store-type= server.tomcat.access-log-pattern= # log pattern of the access log server.tomcat.access-log-enabled=false # is access logging enabled server.tomcat.internal-proxies=10\.\d{1,3}\.\d{1,3}\.\d{1,3}|\ diff --git a/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-docs/src/main/asciidoc/howto.adoc index ca815236b3d..71aac600dc3 100644 --- a/spring-boot-docs/src/main/asciidoc/howto.adoc +++ b/spring-boot-docs/src/main/asciidoc/howto.adoc @@ -389,6 +389,27 @@ and then inject the actual (``local'') port as a `@Value`. For example: +[[howto-configure-ssl]] +=== Configure SSL +SSL can be configured declaratively by setting the various `server.ssl.*` properties, +typically in `application.properties` or `application.yml`. For example: + +[source,properties,indent=0,subs="verbatim,quotes,attributes"] +---- + server.port = 8443 + server.ssl.key-store = classpath:keystore.jks + server.ssl.key-store-password = secret + server.ssl.key-password = another-secret +---- + +See {sc-spring-boot}/context/embedded/Ssl.{sc-ext}[`Ssl`] for details of all of the +supported properties. + +NOTE: Tomcat requires the key store (and trust store if you're using one) to be directly +accessible on the filesystem, i.e. it cannot be read from within a jar file. + + + [[howto-configure-tomcat]] === Configure Tomcat Generally you can follow the advice from @@ -401,56 +422,6 @@ nuclear option is to add your own `TomcatEmbeddedServletContainerFactory`. -[[howto-terminate-ssl-in-tomcat]] -=== Terminate SSL in Tomcat -Use an `EmbeddedServletContainerCustomizer` and in that add a `TomcatConnectorCustomizer` -that sets up the connector to be secure: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @Bean - public EmbeddedServletContainerCustomizer containerCustomizer(){ - return new MyCustomizer(); - } - - // ... - - private static class MyCustomizer implements EmbeddedServletContainerCustomizer { - - @Override - public void customize(ConfigurableEmbeddedServletContainer factory) { - if(factory instanceof TomcatEmbeddedServletContainerFactory) { - customizeTomcat((TomcatEmbeddedServletContainerFactory) factory); - } - } - - public void customizeTomcat(TomcatEmbeddedServletContainerFactory factory) { - factory.addConnectorCustomizers(new TomcatConnectorCustomizer() { - @Override - public void customize(Connector connector) { - connector.setPort(serverPort); - connector.setSecure(true); - connector.setScheme("https"); - connector.setAttribute("keyAlias", "tomcat"); - connector.setAttribute("keystorePass", "password"); - try { - connector.setAttribute("keystoreFile", - ResourceUtils.getFile("src/ssl/tomcat.keystore").getAbsolutePath()); - } catch (FileNotFoundException e) { - throw new IllegalStateException("Cannot load keystore", e); - } - connector.setAttribute("clientAuth", "false"); - connector.setAttribute("sslProtocol", "TLS"); - connector.setAttribute("SSLEnabled", true); - } - }); - } - - } ----- - - - [[howto-enable-multiple-connectors-in-tomcat]] === Enable Multiple Connectors Tomcat Add a `org.apache.catalina.connector.Connector` to the diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml index 062bfd6f371..2f6bdaee696 100644 --- a/spring-boot-samples/pom.xml +++ b/spring-boot-samples/pom.xml @@ -44,6 +44,7 @@ spring-boot-sample-secure spring-boot-sample-servlet spring-boot-sample-simple + spring-boot-sample-tomcat-ssl spring-boot-sample-tomcat spring-boot-sample-tomcat-multi-connectors spring-boot-sample-tomcat8-jsp diff --git a/spring-boot-samples/spring-boot-sample-tomcat-ssl/pom.xml b/spring-boot-samples/spring-boot-sample-tomcat-ssl/pom.xml new file mode 100644 index 00000000000..2497c82769b --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-tomcat-ssl/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + 1.1.7.BUILD-SNAPSHOT + + spring-boot-sample-tomcat-ssl + Spring Boot Tomcat Sample + Spring Boot Tomcat SSL Sample + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-tomcat + + + org.springframework + spring-webmvc + + + org.apache.httpcomponents + httpclient + + + org.springframework.boot + spring-boot-starter-test + test + + + org.yaml + snakeyaml + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-boot-samples/spring-boot-sample-tomcat-ssl/sample.jks b/spring-boot-samples/spring-boot-sample-tomcat-ssl/sample.jks new file mode 100644 index 00000000000..6aa9a28053a Binary files /dev/null and b/spring-boot-samples/spring-boot-sample-tomcat-ssl/sample.jks differ diff --git a/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/java/sample/tomcat/SampleTomcatSslApplication.java b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/java/sample/tomcat/SampleTomcatSslApplication.java new file mode 100644 index 00000000000..d6b6abce3e4 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/java/sample/tomcat/SampleTomcatSslApplication.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.tomcat; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@ComponentScan +@Configuration +@EnableAutoConfiguration +@EnableConfigurationProperties +public class SampleTomcatSslApplication { + + public static void main(String[] args) throws Exception { + SpringApplication.run(SampleTomcatSslApplication.class, args); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/java/sample/tomcat/web/SampleController.java b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/java/sample/tomcat/web/SampleController.java new file mode 100644 index 00000000000..ac00abeb98b --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/java/sample/tomcat/web/SampleController.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.tomcat.web; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +public class SampleController { + + @RequestMapping("/") + @ResponseBody + public String helloWorld() { + return "Hello, world"; + } + +} diff --git a/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/resources/application.properties new file mode 100644 index 00000000000..c8897283947 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/resources/application.properties @@ -0,0 +1,4 @@ +server.port = 8443 +server.ssl.key-store = sample.jks +server.ssl.key-store-password = secret +server.ssl.key-password = password \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/test/java/sample/tomcat/SampleTomcatSslApplicationTests.java b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/test/java/sample/tomcat/SampleTomcatSslApplicationTests.java new file mode 100644 index 00000000000..b6c5fa35455 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/test/java/sample/tomcat/SampleTomcatSslApplicationTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.tomcat; + +import org.apache.http.client.HttpClient; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLContextBuilder; +import org.apache.http.conn.ssl.TrustSelfSignedStrategy; +import org.apache.http.impl.client.HttpClients; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SampleTomcatSslApplication.class) +@WebAppConfiguration +@IntegrationTest("server.port:0") +@DirtiesContext +public class SampleTomcatSslApplicationTests { + + @Value("${local.server.port}") + private int port; + + @Test + public void testHome() throws Exception { + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder().loadTrustMaterial(null, + new TrustSelfSignedStrategy()).build()); + + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory) + .build(); + + TestRestTemplate testRestTemplate = new TestRestTemplate(); + ((HttpComponentsClientHttpRequestFactory) testRestTemplate.getRequestFactory()) + .setHttpClient(httpClient); + ResponseEntity entity = testRestTemplate.getForEntity( + "https://localhost:" + this.port, String.class); + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertEquals("Hello, world", entity.getBody()); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java index 09c7a5335f6..f341ffd2c4e 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractConfigurableEmbeddedServletContainer.java @@ -59,6 +59,8 @@ public abstract class AbstractConfigurableEmbeddedServletContainer implements private int sessionTimeout; + private Ssl ssl; + /** * Create a new {@link AbstractConfigurableEmbeddedServletContainer} instance. */ @@ -247,6 +249,15 @@ public abstract class AbstractConfigurableEmbeddedServletContainer implements this.jspServletClassName = jspServletClassName; } + @Override + public void setSsl(Ssl ssl) { + this.ssl = ssl; + } + + public Ssl getSsl() { + return this.ssl; + } + /** * @return the JSP servlet class name */ diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java index 021173c2010..ff068415792 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/ConfigurableEmbeddedServletContainer.java @@ -139,4 +139,11 @@ public interface ConfigurableEmbeddedServletContainer { */ void addInitializers(ServletContextInitializer... initializers); + /** + * Sets the SSL configuration that will be applied to the container's default + * connector. + * @param ssl the SSL configuration + */ + void setSsl(Ssl ssl); + } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/Ssl.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/Ssl.java new file mode 100644 index 00000000000..1bed8222221 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/Ssl.java @@ -0,0 +1,160 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.embedded; + +/** + * Simple container-independent abstraction for SSL configuration. + * + * @author Andy Wilkinson + * @since 1.2.0 + */ +public class Ssl { + + private ClientAuth clientAuth; + + private String[] ciphers; + + private String keyAlias; + + private String keyPassword; + + private String keyStore; + + private String keyStorePassword; + + private String keyStoreType; + + private String keyStoreProvider; + + private String trustStore; + + private String trustStorePassword; + + private String trustStoreType; + + private String trustStoreProvider; + + private String protocol = "TLS"; + + public ClientAuth getClientAuth() { + return this.clientAuth; + } + + public void setClientAuth(ClientAuth clientAuth) { + this.clientAuth = clientAuth; + } + + public String[] getCiphers() { + return this.ciphers; + } + + public void setCiphers(String[] ciphers) { + this.ciphers = ciphers; + } + + public String getKeyAlias() { + return this.keyAlias; + } + + public void setKeyAlias(String keyAlias) { + this.keyAlias = keyAlias; + } + + public String getKeyPassword() { + return this.keyPassword; + } + + public void setKeyPassword(String keyPassword) { + this.keyPassword = keyPassword; + } + + public String getKeyStore() { + return this.keyStore; + } + + public void setKeyStore(String keyStore) { + this.keyStore = keyStore; + } + + public String getKeyStorePassword() { + return this.keyStorePassword; + } + + public void setKeyStorePassword(String keyStorePassword) { + this.keyStorePassword = keyStorePassword; + } + + public String getKeyStoreType() { + return this.keyStoreType; + } + + public void setKeyStoreType(String keyStoreType) { + this.keyStoreType = keyStoreType; + } + + public String getKeyStoreProvider() { + return this.keyStoreProvider; + } + + public void setKeyStoreProvider(String keyStoreProvider) { + this.keyStoreProvider = keyStoreProvider; + } + + public String getTrustStore() { + return this.trustStore; + } + + public void setTrustStore(String trustStore) { + this.trustStore = trustStore; + } + + public String getTrustStorePassword() { + return this.trustStorePassword; + } + + public void setTrustStorePassword(String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } + + public String getTrustStoreType() { + return this.trustStoreType; + } + + public void setTrustStoreType(String trustStoreType) { + this.trustStoreType = trustStoreType; + } + + public String getTrustStoreProvider() { + return this.trustStoreProvider; + } + + public void setTrustStoreProvider(String trustStoreProvider) { + this.trustStoreProvider = trustStoreProvider; + } + + public String getProtocol() { + return this.protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public enum ClientAuth { + WANT, NEED; + } +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java index b733f69df14..ae96d191860 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java @@ -17,32 +17,41 @@ package org.springframework.boot.context.embedded.jetty; import java.io.File; +import java.io.IOException; import java.net.InetSocketAddress; +import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ErrorHandler; +import org.eclipse.jetty.server.ssl.SslSocketConnector; import org.eclipse.jetty.servlet.ErrorPageErrorHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletMapping; import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.webapp.AbstractConfiguration; import org.eclipse.jetty.webapp.Configuration; import org.eclipse.jetty.webapp.WebAppContext; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.EmbeddedServletContainer; +import org.springframework.boot.context.embedded.EmbeddedServletContainerException; import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.ErrorPage; import org.springframework.boot.context.embedded.MimeMappings; import org.springframework.boot.context.embedded.ServletContextInitializer; +import org.springframework.boot.context.embedded.Ssl; +import org.springframework.boot.context.embedded.Ssl.ClientAuth; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; /** @@ -104,6 +113,16 @@ public class JettyEmbeddedServletContainerFactory extends configureWebAppContext(context, initializers); server.setHandler(context); this.logger.info("Server initialized with port: " + port); + + if (getSsl() != null) { + SslContextFactory sslContextFactory = new SslContextFactory(); + configureSsl(sslContextFactory, getSsl()); + + SslSocketConnector sslConnector = new SslSocketConnector(sslContextFactory); + sslConnector.setPort(port); + server.setConnectors(new Connector[] { sslConnector }); + } + for (JettyServerCustomizer customizer : getServerCustomizers()) { customizer.customize(server); } @@ -111,6 +130,81 @@ public class JettyEmbeddedServletContainerFactory extends return getJettyEmbeddedServletContainer(server); } + /** + * Configure the SSL connection. + * @param factory the Jetty {@link SslContextFactory}. + * @param ssl the ssl details. + */ + protected void configureSsl(SslContextFactory factory, Ssl ssl) { + factory.setProtocol(ssl.getProtocol()); + configureSslClientAuth(factory, ssl); + configureSslPasswords(factory, ssl); + factory.setCertAlias(ssl.getKeyAlias()); + configureSslKeyStore(factory, ssl); + if (ssl.getCiphers() != null) { + factory.setIncludeCipherSuites(ssl.getCiphers()); + } + configureSslTrustStore(factory, ssl); + } + + private void configureSslClientAuth(SslContextFactory factory, Ssl ssl) { + if (ssl.getClientAuth() == ClientAuth.NEED) { + factory.setNeedClientAuth(true); + factory.setWantClientAuth(true); + } + else if (ssl.getClientAuth() == ClientAuth.WANT) { + factory.setWantClientAuth(true); + } + } + + private void configureSslPasswords(SslContextFactory factory, Ssl ssl) { + if (ssl.getKeyStorePassword() != null) { + factory.setKeyStorePassword(ssl.getKeyStorePassword()); + } + if (ssl.getKeyPassword() != null) { + factory.setKeyManagerPassword(ssl.getKeyPassword()); + } + } + + private void configureSslKeyStore(SslContextFactory factory, Ssl ssl) { + try { + URL url = ResourceUtils.getURL(ssl.getKeyStore()); + factory.setKeyStoreResource(Resource.newResource(url)); + } + catch (IOException ex) { + throw new EmbeddedServletContainerException("Could not find key store '" + + ssl.getKeyStore() + "'", ex); + } + if (ssl.getKeyStoreType() != null) { + factory.setKeyStoreType(ssl.getKeyStoreType()); + } + if (ssl.getKeyStoreProvider() != null) { + factory.setKeyStoreProvider(ssl.getKeyStoreProvider()); + } + } + + private void configureSslTrustStore(SslContextFactory factory, Ssl ssl) { + if (ssl.getTrustStorePassword() != null) { + factory.setTrustStorePassword(ssl.getTrustStorePassword()); + } + if (ssl.getTrustStore() != null) { + try { + URL url = ResourceUtils.getURL(ssl.getTrustStore()); + factory.setTrustStoreResource(Resource.newResource(url)); + } + catch (IOException ex) { + throw new EmbeddedServletContainerException( + "Could not find trust store '" + ssl.getTrustStore() + "'", ex); + } + } + if (ssl.getTrustStoreType() != null) { + factory.setTrustStoreType(ssl.getTrustStoreType()); + } + if (ssl.getTrustStoreProvider() != null) { + factory.setTrustStoreProvider(ssl.getTrustStoreProvider()); + } + } + /** * Configure the given Jetty {@link WebAppContext} for use. * @param context the context to configure diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java index f7d8d6a9439..51fa948a82e 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java @@ -17,6 +17,7 @@ package org.springframework.boot.context.embedded.tomcat; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; @@ -42,6 +43,7 @@ import org.apache.catalina.loader.WebappLoader; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.Tomcat.FixContextListener; import org.apache.coyote.AbstractProtocol; +import org.apache.coyote.http11.AbstractHttp11JsseProtocol; import org.springframework.beans.BeanUtils; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.EmbeddedServletContainer; @@ -50,12 +52,16 @@ import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory import org.springframework.boot.context.embedded.ErrorPage; import org.springframework.boot.context.embedded.MimeMappings; import org.springframework.boot.context.embedded.ServletContextInitializer; +import org.springframework.boot.context.embedded.Ssl; +import org.springframework.boot.context.embedded.Ssl.ClientAuth; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; +import org.springframework.util.ResourceUtils; import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; /** * {@link EmbeddedServletContainerFactory} that can be used to create @@ -231,11 +237,87 @@ public class TomcatEmbeddedServletContainerFactory extends // If ApplicationContext is slow to start we want Tomcat not to bind to the socket // prematurely... connector.setProperty("bindOnInit", "false"); + + if (getSsl() != null) { + Assert.state( + connector.getProtocolHandler() instanceof AbstractHttp11JsseProtocol, + "To use SSL, the connector's protocol handler must be an " + + "AbstractHttp11JsseProtocol subclass"); + configureSsl((AbstractHttp11JsseProtocol) connector.getProtocolHandler(), + getSsl()); + connector.setScheme("https"); + connector.setSecure(true); + } + for (TomcatConnectorCustomizer customizer : this.tomcatConnectorCustomizers) { customizer.customize(connector); } } + /** + * Configure Tomcat's {@link AbstractHttp11JsseProtocol} for SSL. + * @param protocol the protocol + * @param ssl the ssl details + */ + 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()); + configureSslKeyStore(protocol, ssl); + String ciphers = StringUtils.arrayToCommaDelimitedString(ssl.getCiphers()); + protocol.setCiphers(ciphers); + 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"); + } + } + + private void configureSslKeyStore(AbstractHttp11JsseProtocol protocol, Ssl ssl) { + try { + File file = ResourceUtils.getFile(ssl.getKeyStore()); + protocol.setKeystoreFile(file.getAbsolutePath()); + } + catch (FileNotFoundException ex) { + throw new EmbeddedServletContainerException("Could not find key store " + + ssl.getKeyStore(), ex); + } + if (ssl.getKeyStoreType() != null) { + protocol.setKeystoreType(ssl.getKeyStoreType()); + } + if (ssl.getKeyStoreProvider() != null) { + protocol.setKeystoreProvider(ssl.getKeyStoreProvider()); + } + } + + private void configureSslTrustStore(AbstractHttp11JsseProtocol protocol, Ssl ssl) { + if (ssl.getTrustStore() != null) { + try { + File file = ResourceUtils.getFile(ssl.getTrustStore()); + protocol.setTruststoreFile(file.getAbsolutePath()); + } + catch (FileNotFoundException ex) { + throw new EmbeddedServletContainerException("Could not find trust store " + + ssl.getTrustStore(), ex); + } + } + protocol.setTruststorePass(ssl.getTrustStorePassword()); + if (ssl.getTrustStoreType() != null) { + protocol.setTruststoreType(ssl.getTrustStoreType()); + } + if (ssl.getTrustStoreProvider() != null) { + protocol.setTruststoreProvider(ssl.getTrustStoreProvider()); + } + } + /** * Configure the Tomcat {@link Context}. * @param context the Tomcat context diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java index c73ee315c32..2850624d4dd 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactoryTests.java @@ -16,11 +16,14 @@ package org.springframework.boot.context.embedded; +import java.io.File; +import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; +import java.security.KeyStore; import java.util.Arrays; import java.util.Date; import java.util.concurrent.TimeUnit; @@ -31,12 +34,18 @@ import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLContextBuilder; +import org.apache.http.conn.ssl.TrustSelfSignedStrategy; +import org.apache.http.impl.client.HttpClients; import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import org.mockito.InOrder; +import org.springframework.boot.context.embedded.Ssl.ClientAuth; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.client.ClientHttpRequest; @@ -62,6 +71,7 @@ import static org.mockito.Mockito.mock; * * @author Phillip Webb * @author Greg Turnquist + * @author Andy Wilkinson */ public abstract class AbstractEmbeddedServletContainerFactoryTests { @@ -300,8 +310,232 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { assertThat(getResponse(getLocalUrl("/bang")), equalTo("Hello World")); } + @Test + public void basicSsl() throws Exception { + FileCopyUtils.copy("test", + new FileWriter(this.temporaryFolder.newFile("test.txt"))); + + AbstractEmbeddedServletContainerFactory factory = getFactory(); + factory.setDocumentRoot(this.temporaryFolder.getRoot()); + + Ssl ssl = new Ssl(); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyPassword("password"); + factory.setSsl(ssl); + + this.container = factory.getEmbeddedServletContainer(); + this.container.start(); + + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder().loadTrustMaterial(null, + new TrustSelfSignedStrategy()).build()); + + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory) + .build(); + + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( + httpClient); + + assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory), + equalTo("test")); + } + + @Test + public void pkcs12KeyStoreAndTrustStore() throws Exception { + FileCopyUtils.copy("test", + new FileWriter(this.temporaryFolder.newFile("test.txt"))); + + AbstractEmbeddedServletContainerFactory factory = getFactory(); + factory.setDocumentRoot(this.temporaryFolder.getRoot()); + + Ssl ssl = new Ssl(); + ssl.setKeyStore("src/test/resources/test.p12"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyStoreType("pkcs12"); + ssl.setTrustStore("src/test/resources/test.p12"); + ssl.setTrustStorePassword("secret"); + ssl.setTrustStoreType("pkcs12"); + ssl.setClientAuth(ClientAuth.NEED); + factory.setSsl(ssl); + + this.container = factory.getEmbeddedServletContainer(); + this.container.start(); + + KeyStore keyStore = KeyStore.getInstance("pkcs12"); + keyStore.load(new FileInputStream(new File("src/test/resources/test.p12")), + "secret".toCharArray()); + + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder() + .loadTrustMaterial(null, new TrustSelfSignedStrategy()) + .loadKeyMaterial(keyStore, "secret".toCharArray()).build()); + + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory) + .build(); + + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( + httpClient); + + assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory), + equalTo("test")); + } + + @Test + public void sslNeedsClientAuthenticationSucceedsWithClientCertificate() + throws Exception { + FileCopyUtils.copy("test", + new FileWriter(this.temporaryFolder.newFile("test.txt"))); + + AbstractEmbeddedServletContainerFactory factory = getFactory(); + factory.setDocumentRoot(this.temporaryFolder.getRoot()); + + Ssl ssl = new Ssl(); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyPassword("password"); + ssl.setClientAuth(ClientAuth.NEED); + ssl.setTrustStore("src/test/resources/test.jks"); + ssl.setTrustStorePassword("secret"); + factory.setSsl(ssl); + + this.container = factory.getEmbeddedServletContainer(); + this.container.start(); + + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(new FileInputStream(new File("src/test/resources/test.jks")), + "secret".toCharArray()); + + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder() + .loadTrustMaterial(null, new TrustSelfSignedStrategy()) + .loadKeyMaterial(keyStore, "password".toCharArray()).build()); + + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory) + .build(); + + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( + httpClient); + + assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory), + equalTo("test")); + } + + @Test(expected = IOException.class) + public void sslNeedsClientAuthenticationFailsWithoutClientCertificate() + throws Exception { + FileCopyUtils.copy("test", + new FileWriter(this.temporaryFolder.newFile("test.txt"))); + + AbstractEmbeddedServletContainerFactory factory = getFactory(); + factory.setDocumentRoot(this.temporaryFolder.getRoot()); + + Ssl ssl = new Ssl(); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyPassword("password"); + ssl.setClientAuth(ClientAuth.NEED); + ssl.setTrustStore("src/test/resources/test.jks"); + ssl.setTrustStorePassword("secret"); + factory.setSsl(ssl); + + this.container = factory.getEmbeddedServletContainer(); + this.container.start(); + + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder().loadTrustMaterial(null, + new TrustSelfSignedStrategy()).build()); + + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory) + .build(); + + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( + httpClient); + + getResponse(getLocalUrl("https", "/test.txt"), requestFactory); + } + + @Test + public void sslWantsClientAuthenticationSucceedsWithClientCertificate() + throws Exception { + FileCopyUtils.copy("test", + new FileWriter(this.temporaryFolder.newFile("test.txt"))); + + AbstractEmbeddedServletContainerFactory factory = getFactory(); + factory.setDocumentRoot(this.temporaryFolder.getRoot()); + + Ssl ssl = new Ssl(); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyPassword("password"); + ssl.setClientAuth(ClientAuth.WANT); + ssl.setTrustStore("src/test/resources/test.jks"); + ssl.setTrustStorePassword("secret"); + factory.setSsl(ssl); + + this.container = factory.getEmbeddedServletContainer(); + this.container.start(); + + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(new FileInputStream(new File("src/test/resources/test.jks")), + "secret".toCharArray()); + + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder() + .loadTrustMaterial(null, new TrustSelfSignedStrategy()) + .loadKeyMaterial(keyStore, "password".toCharArray()).build()); + + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory) + .build(); + + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( + httpClient); + + assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory), + equalTo("test")); + } + + @Test + public void sslWantsClientAuthenticationSucceedsWithoutClientCertificate() + throws Exception { + FileCopyUtils.copy("test", + new FileWriter(this.temporaryFolder.newFile("test.txt"))); + + AbstractEmbeddedServletContainerFactory factory = getFactory(); + factory.setDocumentRoot(this.temporaryFolder.getRoot()); + + Ssl ssl = new Ssl(); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyPassword("password"); + ssl.setClientAuth(ClientAuth.WANT); + ssl.setTrustStore("src/test/resources/test.jks"); + ssl.setTrustStorePassword("secret"); + factory.setSsl(ssl); + + this.container = factory.getEmbeddedServletContainer(); + this.container.start(); + + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder().loadTrustMaterial(null, + new TrustSelfSignedStrategy()).build()); + + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory) + .build(); + + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( + httpClient); + + assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory), + equalTo("test")); + } + protected String getLocalUrl(String resourcePath) { - return "http://localhost:" + this.container.getPort() + resourcePath; + return getLocalUrl("http", resourcePath); + } + + protected String getLocalUrl(String scheme, String resourcePath) { + return scheme + "://localhost:" + this.container.getPort() + resourcePath; } protected String getLocalUrl(int port, String resourcePath) { @@ -318,10 +552,27 @@ public abstract class AbstractEmbeddedServletContainerFactoryTests { } } + protected String getResponse(String url, + HttpComponentsClientHttpRequestFactory requestFactory) throws IOException, + URISyntaxException { + ClientHttpResponse response = getClientResponse(url, requestFactory); + try { + return StreamUtils.copyToString(response.getBody(), Charset.forName("UTF-8")); + } + finally { + response.close(); + } + } + protected ClientHttpResponse getClientResponse(String url) throws IOException, URISyntaxException { - HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(); - ClientHttpRequest request = clientHttpRequestFactory.createRequest(new URI(url), + return getClientResponse(url, new HttpComponentsClientHttpRequestFactory()); + } + + protected ClientHttpResponse getClientResponse(String url, + HttpComponentsClientHttpRequestFactory requestFactory) throws IOException, + URISyntaxException { + ClientHttpRequest request = requestFactory.createRequest(new URI(url), HttpMethod.GET); ClientHttpResponse response = request.execute(); return response; diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java index f9b9d8af31d..929908113a7 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactoryTests.java @@ -21,11 +21,13 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ssl.SslConnector; import org.eclipse.jetty.webapp.Configuration; import org.eclipse.jetty.webapp.WebAppContext; import org.junit.Test; import org.mockito.InOrder; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactoryTests; +import org.springframework.boot.context.embedded.Ssl; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; @@ -39,6 +41,7 @@ import static org.mockito.Mockito.mock; * * @author Phillip Webb * @author Dave Syer + * @author Andy Wilkinson */ public class JettyEmbeddedServletContainerFactoryTests extends AbstractEmbeddedServletContainerFactoryTests { @@ -94,6 +97,27 @@ public class JettyEmbeddedServletContainerFactoryTests extends assertTimeout(factory, 60); } + @Test + public void sslCiphersConfiguration() throws Exception { + Ssl ssl = new Ssl(); + ssl.setKeyStore("src/test/resources/test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyPassword("password"); + ssl.setCiphers(new String[] { "ALPHA", "BRAVO", "CHARLIE" }); + + JettyEmbeddedServletContainerFactory factory = getFactory(); + factory.setSsl(ssl); + + this.container = factory.getEmbeddedServletContainer(); + this.container.start(); + + JettyEmbeddedServletContainer jettyContainer = (JettyEmbeddedServletContainer) this.container; + SslConnector sslConnector = (SslConnector) jettyContainer.getServer() + .getConnectors()[0]; + assertThat(sslConnector.getSslContextFactory().getIncludeCipherSuites(), + equalTo(new String[] { "ALPHA", "BRAVO", "CHARLIE" })); + } + private void assertTimeout(JettyEmbeddedServletContainerFactory factory, int expected) { this.container = factory.getEmbeddedServletContainer(); JettyEmbeddedServletContainer jettyContainer = (JettyEmbeddedServletContainer) this.container; diff --git a/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java b/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java index 1d51a316c16..a5071c61f8f 100644 --- a/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactoryTests.java @@ -28,9 +28,11 @@ import org.apache.catalina.Service; import org.apache.catalina.Valve; import org.apache.catalina.connector.Connector; import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.http11.AbstractHttp11JsseProtocol; import org.junit.Test; import org.mockito.InOrder; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactoryTests; +import org.springframework.boot.context.embedded.Ssl; import org.springframework.util.SocketUtils; import static org.hamcrest.Matchers.equalTo; @@ -228,6 +230,24 @@ public class TomcatEmbeddedServletContainerFactoryTests extends assertEquals("UTF-8", tomcat.getConnector().getURIEncoding()); } + @Test + public void sslCiphersConfiguration() throws Exception { + Ssl ssl = new Ssl(); + ssl.setKeyStore("test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setCiphers(new String[] { "ALPHA", "BRAVO", "CHARLIE" }); + + TomcatEmbeddedServletContainerFactory factory = getFactory(); + factory.setSsl(ssl); + + Tomcat tomcat = getTomcat(factory); + Connector connector = tomcat.getConnector(); + + AbstractHttp11JsseProtocol jsseProtocol = (AbstractHttp11JsseProtocol) connector + .getProtocolHandler(); + assertThat(jsseProtocol.getCiphers(), equalTo("ALPHA,BRAVO,CHARLIE")); + } + private void assertTimeout(TomcatEmbeddedServletContainerFactory factory, int expected) { Tomcat tomcat = getTomcat(factory); Context context = (Context) tomcat.getHost().findChildren()[0]; diff --git a/spring-boot/src/test/resources/test.jks b/spring-boot/src/test/resources/test.jks new file mode 100644 index 00000000000..cc0d7081c2e Binary files /dev/null and b/spring-boot/src/test/resources/test.jks differ diff --git a/spring-boot/src/test/resources/test.p12 b/spring-boot/src/test/resources/test.p12 new file mode 100644 index 00000000000..5726ff02f4b Binary files /dev/null and b/spring-boot/src/test/resources/test.p12 differ