Add header based remote access security

Update the remote endpoints to use 'shared secret' authentication.
Secrets are provided as Environment properties and transfered using a
custom HTTP header.

See gh-3082
This commit is contained in:
Rob Winch
2015-06-01 13:24:18 -07:00
committed by Phillip Webb
parent fe4c0022d7
commit 207347e150
9 changed files with 457 additions and 14 deletions

View File

@@ -34,6 +34,7 @@ import org.springframework.boot.developertools.remote.server.Dispatcher;
import org.springframework.boot.developertools.remote.server.DispatcherFilter;
import org.springframework.boot.developertools.remote.server.Handler;
import org.springframework.boot.developertools.remote.server.HandlerMapper;
import org.springframework.boot.developertools.remote.server.HttpHeaderAccessManager;
import org.springframework.boot.developertools.remote.server.HttpStatusHandler;
import org.springframework.boot.developertools.remote.server.UrlHandlerMapper;
import org.springframework.boot.developertools.restart.server.DefaultSourceFolderUrlFilter;
@@ -56,7 +57,7 @@ import org.springframework.http.server.ServerHttpRequest;
* @since 1.3.0
*/
@Configuration
@ConditionalOnProperty(prefix = "spring.developertools.remote", name = "enabled")
@ConditionalOnProperty(prefix = "spring.developertools.remote", name = "secret")
@ConditionalOnClass({ Filter.class, ServerHttpRequest.class })
@EnableConfigurationProperties(DeveloperToolsProperties.class)
public class RemoteDeveloperToolsAutoConfiguration {
@@ -67,6 +68,14 @@ public class RemoteDeveloperToolsAutoConfiguration {
@Autowired
private DeveloperToolsProperties properties;
@Bean
@ConditionalOnMissingBean
public AccessManager remoteDeveloperToolsAccessManager() {
RemoteDeveloperToolsProperties remoteProperties = this.properties.getRemote();
return new HttpHeaderAccessManager(remoteProperties.getSecretHeaderName(),
remoteProperties.getSecret());
}
@Bean
public HandlerMapper remoteDeveloperToolsHealthCheckHandlerMapper() {
Handler handler = new HttpStatusHandler();
@@ -76,8 +85,8 @@ public class RemoteDeveloperToolsAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DispatcherFilter remoteDeveloperToolsDispatcherFilter(
Collection<HandlerMapper> mappers) {
Dispatcher dispatcher = new Dispatcher(AccessManager.PERMIT_ALL, mappers);
AccessManager accessManager, Collection<HandlerMapper> mappers) {
Dispatcher dispatcher = new Dispatcher(accessManager, mappers);
return new DispatcherFilter(dispatcher);
}

View File

@@ -28,11 +28,24 @@ public class RemoteDeveloperToolsProperties {
public static final String DEFAULT_CONTEXT_PATH = "/.~~spring-boot!~";
public static final String DEFAULT_SECRET_HEADER_NAME = "X-AUTH-TOKEN";
/**
* Context path used to handle the remote connection.
*/
private String contextPath = DEFAULT_CONTEXT_PATH;
/**
* A shared secret required to establish a connection (required to enable remote
* support).
*/
private String secret;
/**
* HTTP header used to transfer the shared secret.
*/
private String secretHeaderName = DEFAULT_SECRET_HEADER_NAME;
private Restart restart = new Restart();
private Debug debug = new Debug();
@@ -45,6 +58,22 @@ public class RemoteDeveloperToolsProperties {
this.contextPath = contextPath;
}
public String getSecret() {
return this.secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public String getSecretHeaderName() {
return this.secretHeaderName;
}
public void setSecretHeaderName(String secretHeaderName) {
this.secretHeaderName = secretHeaderName;
}
public Restart getRestart() {
return this.restart;
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2012-2015 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.developertools.remote.client;
import java.io.IOException;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.Assert;
/**
* {@link ClientHttpRequestInterceptor} to populate arbitrary HTTP headers with a value.
* For example, it might be used to provide an X-AUTH-TOKEN and value for security
* purposes.
*
* @author Rob Winch
* @since 1.3.0
*/
public class HttpHeaderInterceptor implements ClientHttpRequestInterceptor {
private final String name;
private final String value;
/**
* Creates a new {@link HttpHeaderInterceptor} instance.
* @param name the header name to populate. Cannot be null or empty.
* @param value the header value to populate. Cannot be null or empty.
*/
public HttpHeaderInterceptor(String name, String value) {
Assert.hasLength(name, "Name must not be empty");
Assert.hasLength(value, "Value" + " must not be empty");
this.name = name;
this.value = value;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
request.getHeaders().add(this.name, this.value);
return execution.execute(request, body);
}
}

View File

@@ -17,6 +17,8 @@
package org.springframework.boot.developertools.remote.client;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -51,7 +53,10 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.InterceptingClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.Assert;
/**
* Configuration used to connect to remote Spring Boot applications.
@@ -79,7 +84,20 @@ public class RemoteClientConfiguration {
@Bean
public ClientHttpRequestFactory clientHttpRequestFactory() {
return new SimpleClientHttpRequestFactory();
List<ClientHttpRequestInterceptor> interceptors = Arrays
.asList(getSecurityInterceptor());
return new InterceptingClientHttpRequestFactory(
new SimpleClientHttpRequestFactory(), interceptors);
}
private ClientHttpRequestInterceptor getSecurityInterceptor() {
RemoteDeveloperToolsProperties remoteProperties = this.properties.getRemote();
String secretHeaderName = remoteProperties.getSecretHeaderName();
String secret = remoteProperties.getSecret();
Assert.state(secret != null,
"The environment value 'spring.developertools.remote.secret' "
+ "is required to secure your connection.");
return new HttpHeaderInterceptor(secretHeaderName, secret);
}
@PostConstruct

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2012-2015 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.developertools.remote.server;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.util.Assert;
/**
* {@link AccessManager} that checks for the presence of a HTTP header secret.
*
* @author Rob Winch
* @author Phillip Webb
* @since 1.3.0
*/
public class HttpHeaderAccessManager implements AccessManager {
private final String headerName;
private final String expectedSecret;
public HttpHeaderAccessManager(String headerName, String expectedSecret) {
Assert.hasLength(headerName, "HeaderName must not be empty");
Assert.hasLength(expectedSecret, "ExpectedSecret must not be empty");
this.headerName = headerName;
this.expectedSecret = expectedSecret;
}
@Override
public boolean isAllowed(ServerHttpRequest request) {
String providedSecret = request.getHeaders().getFirst(this.headerName);
return this.expectedSecret.equals(providedSecret);
}
}