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 321ce3cb8f4..930d245d6fa 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; @@ -46,6 +47,7 @@ import org.springframework.util.StringUtils; * * @author Dave Syer * @author Stephane Nicoll + * @author Andy Wilkinson */ @ConfigurationProperties(prefix = "server", ignoreUnknownFields = false) public class ServerProperties implements EmbeddedServletContainerCustomizer { @@ -58,6 +60,8 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer { private String contextPath; + private Ssl ssl; + @NotNull private String servletPath = "/"; @@ -131,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) } @@ -149,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; @@ -313,31 +354,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 51922f9a79e..2c4b27c0760 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,14 @@ 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.protocol=TLS + server.ssl.trust-store= + server.ssl.trust-store-password= server.tomcat.access-log-pattern= # log pattern of the access log server.tomcat.access-log-enabled=false # is access logging enabled server.tomcat.protocol-header=x-forwarded-proto # ssl forward headers diff --git a/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-docs/src/main/asciidoc/howto.adoc index 88afdcc90b3..469f6947a86 100644 --- a/spring-boot-docs/src/main/asciidoc/howto.adoc +++ b/spring-boot-docs/src/main/asciidoc/howto.adoc @@ -387,6 +387,25 @@ 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]] @@ -401,56 +420,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 7d2c5bd1a88..8b4954ac559 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..eb4bbcea430 --- /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.2.0.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..ec8443e36c9 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/main/java/sample/tomcat/SampleTomcatSslApplication.java @@ -0,0 +1,34 @@ +/* + * 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..222e220d75d --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-tomcat-ssl/src/test/java/sample/tomcat/SampleTomcatSslApplicationTests.java @@ -0,0 +1,66 @@ +/* + * 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..957ab5e71b3 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,10 @@ 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..b8726771434 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/Ssl.java @@ -0,0 +1,120 @@ +/* + * 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 trustStore; + + private String trustStorePassword; + + 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 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 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..01cc8c669af 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,6 +17,7 @@ package org.springframework.boot.context.embedded.jetty; import java.io.File; +import java.io.IOException; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; @@ -24,25 +25,32 @@ 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 +112,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(); + configureSslContextFactory(sslContextFactory, getSsl()); + + SslSocketConnector sslConnector = new SslSocketConnector(sslContextFactory); + sslConnector.setPort(port); + server.setConnectors(new Connector[] { sslConnector }); + } + for (JettyServerCustomizer customizer : getServerCustomizers()) { customizer.customize(server); } @@ -111,6 +129,52 @@ public class JettyEmbeddedServletContainerFactory extends return getJettyEmbeddedServletContainer(server); } + protected void configureSslContextFactory(SslContextFactory sslContextFactory, Ssl ssl) { + sslContextFactory.setProtocol(getSsl().getProtocol()); + if (getSsl().getClientAuth() == ClientAuth.NEED) { + sslContextFactory.setNeedClientAuth(true); + sslContextFactory.setWantClientAuth(true); + } + else if (getSsl().getClientAuth() == ClientAuth.WANT) { + sslContextFactory.setWantClientAuth(true); + } + if (getSsl().getKeyStorePassword() != null) { + sslContextFactory.setKeyStorePassword(getSsl().getKeyStorePassword()); + } + if (getSsl().getKeyPassword() != null) { + sslContextFactory.setKeyManagerPassword(getSsl().getKeyPassword()); + } + sslContextFactory.setCertAlias(getSsl().getKeyAlias()); + try { + sslContextFactory.setKeyStoreResource(Resource.newResource(ResourceUtils + .getURL(getSsl().getKeyStore()))); + } + catch (IOException e) { + throw new EmbeddedServletContainerException("Could not find key store '" + + getSsl().getKeyStore() + "'", e); + } + + if (getSsl().getCiphers() != null) { + sslContextFactory.setIncludeCipherSuites(getSsl().getCiphers()); + } + + if (getSsl().getTrustStorePassword() != null) { + sslContextFactory.setTrustStorePassword(getSsl().getTrustStorePassword()); + } + + if (getSsl().getTrustStore() != null) { + try { + sslContextFactory.setTrustStoreResource(Resource + .newResource(ResourceUtils.getURL(getSsl().getTrustStore()))); + } + catch (IOException e) { + throw new EmbeddedServletContainerException( + "Could not find trust store '" + getSsl().getTrustStore() + "'", + e); + } + } + } + /** * 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 0780f488c18..109987b3441 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; @@ -41,6 +42,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; @@ -49,12 +51,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 @@ -230,11 +236,64 @@ 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) { + if (connector.getProtocolHandler() instanceof AbstractHttp11JsseProtocol) { + AbstractHttp11JsseProtocol jsseProtocol = (AbstractHttp11JsseProtocol) connector + .getProtocolHandler(); + configureJsseProtocol(jsseProtocol, getSsl()); + connector.setScheme("https"); + connector.setSecure(true); + } + else { + throw new IllegalStateException( + "To use SSL, the connector's protocol handler must be an AbstractHttp11JsseProtocol subclass"); + } + } + for (TomcatConnectorCustomizer customizer : this.tomcatConnectorCustomizers) { customizer.customize(connector); } } + protected void configureJsseProtocol(AbstractHttp11JsseProtocol jsseProtocol, Ssl ssl) { + jsseProtocol.setSSLEnabled(true); + jsseProtocol.setSslProtocol(getSsl().getProtocol()); + if (getSsl().getClientAuth() == ClientAuth.NEED) { + jsseProtocol.setClientAuth(Boolean.TRUE.toString()); + } + else if (getSsl().getClientAuth() == ClientAuth.WANT) { + jsseProtocol.setClientAuth("want"); + } + jsseProtocol.setKeystorePass(getSsl().getKeyStorePassword()); + jsseProtocol.setKeyPass(getSsl().getKeyPassword()); + jsseProtocol.setKeyAlias(getSsl().getKeyAlias()); + try { + jsseProtocol.setKeystoreFile(ResourceUtils.getFile(getSsl().getKeyStore()) + .getAbsolutePath()); + } + catch (FileNotFoundException e) { + throw new EmbeddedServletContainerException("Could not find key store " + + getSsl().getKeyStore(), e); + } + + jsseProtocol.setCiphers(StringUtils.arrayToCommaDelimitedString(getSsl() + .getCiphers())); + + if (getSsl().getTrustStore() != null) { + try { + jsseProtocol.setTruststoreFile(ResourceUtils.getFile( + getSsl().getTrustStore()).getAbsolutePath()); + } + catch (FileNotFoundException e) { + throw new EmbeddedServletContainerException("Could not find trust store " + + getSsl().getTrustStore(), e); + } + } + + jsseProtocol.setTruststorePass(getSsl().getTrustStorePassword()); + } + /** * 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..74bbd7aab93 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,192 @@ 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 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 +512,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..312867d1d35 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,25 @@ 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(); + 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 6c0964491f8..349a6d59770 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; @@ -221,6 +223,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