Stéphane Lagraulet
8 years ago
29 changed files with 674 additions and 53 deletions
@ -0,0 +1,87 @@
@@ -0,0 +1,87 @@
|
||||
/* |
||||
* Copyright 2013-2016 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.cloud.netflix.hystrix.security; |
||||
|
||||
import javax.annotation.PostConstruct; |
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.boot.autoconfigure.condition.AllNestedConditions; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
import org.springframework.cloud.netflix.hystrix.security.HystrixSecurityAutoConfiguration.HystrixSecurityCondition; |
||||
import org.springframework.context.annotation.Conditional; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.security.core.context.SecurityContext; |
||||
|
||||
import com.netflix.hystrix.Hystrix; |
||||
import com.netflix.hystrix.strategy.HystrixPlugins; |
||||
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy; |
||||
import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier; |
||||
import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook; |
||||
import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher; |
||||
import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy; |
||||
|
||||
/** |
||||
* @author Daniel Lavoie |
||||
*/ |
||||
@Configuration |
||||
@Conditional(HystrixSecurityCondition.class) |
||||
@ConditionalOnClass({ Hystrix.class, SecurityContext.class }) |
||||
public class HystrixSecurityAutoConfiguration { |
||||
@Autowired(required = false) |
||||
private HystrixConcurrencyStrategy existingConcurrencyStrategy; |
||||
|
||||
@PostConstruct |
||||
public void init() { |
||||
// Keeps references of existing Hystrix plugins.
|
||||
HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance() |
||||
.getEventNotifier(); |
||||
HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance() |
||||
.getMetricsPublisher(); |
||||
HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance() |
||||
.getPropertiesStrategy(); |
||||
HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins.getInstance() |
||||
.getCommandExecutionHook(); |
||||
|
||||
HystrixPlugins.reset(); |
||||
|
||||
// Registers existing plugins excepts the Concurrent Strategy plugin.
|
||||
HystrixPlugins.getInstance().registerConcurrencyStrategy( |
||||
new SecurityContextConcurrencyStrategy(existingConcurrencyStrategy)); |
||||
HystrixPlugins.getInstance().registerEventNotifier(eventNotifier); |
||||
HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher); |
||||
HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy); |
||||
HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook); |
||||
} |
||||
|
||||
static class HystrixSecurityCondition extends AllNestedConditions { |
||||
|
||||
public HystrixSecurityCondition() { |
||||
super(ConfigurationPhase.REGISTER_BEAN); |
||||
} |
||||
|
||||
@ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = true) |
||||
static class HystrixEnabled { |
||||
|
||||
} |
||||
|
||||
@ConditionalOnProperty(name = "hystrix.shareSecurityContext") |
||||
static class ShareSecurityContext { |
||||
|
||||
} |
||||
} |
||||
} |
@ -0,0 +1,78 @@
@@ -0,0 +1,78 @@
|
||||
/* |
||||
* Copyright 2013-2016 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.cloud.netflix.hystrix.security; |
||||
|
||||
import java.util.concurrent.BlockingQueue; |
||||
import java.util.concurrent.Callable; |
||||
import java.util.concurrent.ThreadPoolExecutor; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
import org.springframework.security.concurrent.DelegatingSecurityContextCallable; |
||||
|
||||
import com.netflix.hystrix.HystrixThreadPoolKey; |
||||
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy; |
||||
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariable; |
||||
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableLifecycle; |
||||
import com.netflix.hystrix.strategy.properties.HystrixProperty; |
||||
|
||||
/** |
||||
* @author daniellavoie |
||||
*/ |
||||
public class SecurityContextConcurrencyStrategy extends HystrixConcurrencyStrategy { |
||||
private HystrixConcurrencyStrategy existingConcurrencyStrategy; |
||||
|
||||
public SecurityContextConcurrencyStrategy( |
||||
HystrixConcurrencyStrategy existingConcurrencyStrategy) { |
||||
this.existingConcurrencyStrategy = existingConcurrencyStrategy; |
||||
} |
||||
|
||||
@Override |
||||
public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) { |
||||
return existingConcurrencyStrategy != null |
||||
? existingConcurrencyStrategy.getBlockingQueue(maxQueueSize) |
||||
: super.getBlockingQueue(maxQueueSize); |
||||
} |
||||
|
||||
@Override |
||||
public <T> HystrixRequestVariable<T> getRequestVariable( |
||||
HystrixRequestVariableLifecycle<T> rv) { |
||||
return existingConcurrencyStrategy != null |
||||
? existingConcurrencyStrategy.getRequestVariable(rv) |
||||
: super.getRequestVariable(rv); |
||||
} |
||||
|
||||
@Override |
||||
public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, |
||||
HystrixProperty<Integer> corePoolSize, |
||||
HystrixProperty<Integer> maximumPoolSize, |
||||
HystrixProperty<Integer> keepAliveTime, TimeUnit unit, |
||||
BlockingQueue<Runnable> workQueue) { |
||||
return existingConcurrencyStrategy != null |
||||
? existingConcurrencyStrategy.getThreadPool(threadPoolKey, corePoolSize, |
||||
maximumPoolSize, keepAliveTime, unit, workQueue) |
||||
: super.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, |
||||
keepAliveTime, unit, workQueue); |
||||
} |
||||
|
||||
@Override |
||||
public <T> Callable<T> wrapCallable(Callable<T> callable) { |
||||
return existingConcurrencyStrategy != null |
||||
? existingConcurrencyStrategy |
||||
.wrapCallable(new DelegatingSecurityContextCallable<T>(callable)) |
||||
: super.wrapCallable(new DelegatingSecurityContextCallable<T>(callable)); |
||||
} |
||||
} |
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
/* |
||||
* Copyright 2013-2016 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.cloud.netflix.hystrix.security; |
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
||||
import org.springframework.cloud.netflix.feign.EnableFeignClients; |
||||
import org.springframework.cloud.netflix.hystrix.security.app.UsernameClient; |
||||
import org.springframework.context.annotation.Configuration; |
||||
|
||||
/** |
||||
* @author Daniel Lavoie |
||||
*/ |
||||
@Configuration |
||||
@SpringBootApplication |
||||
@EnableFeignClients(clients = UsernameClient.class) |
||||
public class HystrixSecurityApplication { |
||||
|
||||
} |
@ -0,0 +1,87 @@
@@ -0,0 +1,87 @@
|
||||
/* |
||||
* Copyright 2013-2016 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.cloud.netflix.hystrix.security; |
||||
|
||||
import java.util.Base64; |
||||
|
||||
import org.junit.Assert; |
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.beans.factory.annotation.Value; |
||||
import org.springframework.boot.test.context.SpringBootTest; |
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; |
||||
import org.springframework.cloud.netflix.hystrix.security.app.CustomConcurrenyStrategy; |
||||
import org.springframework.http.HttpEntity; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.test.annotation.DirtiesContext; |
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; |
||||
import org.springframework.web.client.RestTemplate; |
||||
|
||||
/** |
||||
* Tests that a secured web service returning values using a feign client properly access |
||||
* the security context from a hystrix command. |
||||
* @author Daniel Lavoie |
||||
*/ |
||||
@RunWith(SpringJUnit4ClassRunner.class) |
||||
@DirtiesContext |
||||
@SpringBootTest(classes = HystrixSecurityApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT, properties = "username.ribbon.listOfServers=localhost:${local.server.port}") |
||||
public class HystrixSecurityTests { |
||||
@Autowired |
||||
private CustomConcurrenyStrategy customConcurrenyStrategy; |
||||
|
||||
@Value("${local.server.port}") |
||||
private String serverPort; |
||||
|
||||
@Value("${security.user.username}") |
||||
private String username; |
||||
|
||||
@Value("${security.user.password}") |
||||
private String password; |
||||
|
||||
@Test |
||||
public void testFeignHystrixSecurity() { |
||||
HttpHeaders headers = HystrixSecurityTests.createBasicAuthHeader(username, |
||||
password); |
||||
|
||||
String usernameResult = new RestTemplate() |
||||
.exchange("http://localhost:" + serverPort + "/proxy-username", |
||||
HttpMethod.GET, new HttpEntity<Void>(headers), String.class) |
||||
.getBody(); |
||||
|
||||
Assert.assertTrue("Username should have been intercepted by feign interceptor.", |
||||
username.equals(usernameResult)); |
||||
|
||||
Assert.assertTrue("Custom hook should have been called.", |
||||
customConcurrenyStrategy.isHookCalled()); |
||||
} |
||||
|
||||
public static HttpHeaders createBasicAuthHeader(final String username, |
||||
final String password) { |
||||
return new HttpHeaders() { |
||||
private static final long serialVersionUID = 1766341693637204893L; |
||||
|
||||
{ |
||||
String auth = username + ":" + password; |
||||
byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes()); |
||||
String authHeader = "Basic " + new String(encodedAuth); |
||||
this.set("Authorization", authHeader); |
||||
} |
||||
}; |
||||
} |
||||
} |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
package org.springframework.cloud.netflix.hystrix.security.app; |
||||
|
||||
import java.util.concurrent.Callable; |
||||
|
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy; |
||||
|
||||
@Component |
||||
public class CustomConcurrenyStrategy extends HystrixConcurrencyStrategy { |
||||
private boolean hookCalled; |
||||
|
||||
@Override |
||||
public <T> Callable<T> wrapCallable(Callable<T> callable) { |
||||
this.hookCalled = true; |
||||
|
||||
return super.wrapCallable(callable); |
||||
} |
||||
|
||||
public boolean isHookCalled() { |
||||
return hookCalled; |
||||
} |
||||
} |
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
/* |
||||
* Copyright 2013-2016 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.cloud.netflix.hystrix.security.app; |
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
|
||||
/** |
||||
* @author Daniel Lavoie |
||||
*/ |
||||
@RestController |
||||
@RequestMapping("/proxy-username") |
||||
public class ProxyUsernameController { |
||||
@Autowired |
||||
private UsernameClient usernameClient; |
||||
|
||||
@RequestMapping |
||||
public String getUsername() { |
||||
return usernameClient.getUsername(); |
||||
} |
||||
} |
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
/* |
||||
* Copyright 2013-2016 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.cloud.netflix.hystrix.security.app; |
||||
|
||||
import org.springframework.security.core.context.SecurityContextHolder; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import feign.RequestInterceptor; |
||||
import feign.RequestTemplate; |
||||
|
||||
/** |
||||
* This interceptor should be called from an Hyxtrix command execution thread. It is |
||||
* access the SecurityContext and settings an http header from the authentication details. |
||||
* |
||||
* @author Daniel Lavoie |
||||
*/ |
||||
@Component |
||||
public class TestInterceptor implements RequestInterceptor { |
||||
|
||||
@Override |
||||
public void apply(RequestTemplate template) { |
||||
if (SecurityContextHolder.getContext().getAuthentication() != null) |
||||
template.header("username", |
||||
SecurityContextHolder.getContext().getAuthentication().getName()); |
||||
} |
||||
} |
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
/* |
||||
* Copyright 2013-2016 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.cloud.netflix.hystrix.security.app; |
||||
|
||||
import org.springframework.cloud.netflix.feign.FeignClient; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
||||
/** |
||||
* @author Daniel Lavoie |
||||
*/ |
||||
@FeignClient("username") |
||||
public interface UsernameClient { |
||||
|
||||
@RequestMapping("/username") |
||||
public String getUsername(); |
||||
} |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
/* |
||||
* Copyright 2013-2016 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.cloud.netflix.hystrix.security.app; |
||||
|
||||
import org.springframework.web.bind.annotation.RequestHeader; |
||||
import org.springframework.web.bind.annotation.RequestMapping; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
|
||||
/** |
||||
* @author Daniel Lavoie |
||||
*/ |
||||
@RestController |
||||
@RequestMapping("/username") |
||||
public class UsernameController { |
||||
@RequestMapping |
||||
public String getUsername(@RequestHeader String username){ |
||||
return username; |
||||
} |
||||
} |
Loading…
Reference in new issue