Edit guide and refactor example of Spring Boot auto-configuration for Apache Geode/Pivotal GemFire to include Security with Authentication.

This commit is contained in:
John Blum
2019-04-22 23:44:40 -07:00
parent 4f52e19acd
commit 58d9970ce1
7 changed files with 377 additions and 12 deletions

View File

@@ -2,9 +2,12 @@
= Spring Boot Auto-configuration for Apache Geode & Pivotal GemFire
This guide walks you through building a simple Customer Service, Spring Boot application using Apache Geode to manage
Customer interactions. It is assumed that you are already familiar with Spring Boot and Apache Geode. By the end of
this lesson, users should have a better understanding of the _auto-configuration_ support provided by
Spring Boot Data Geode (SBDG).
Customer interactions. It is assumed that you are already familiar with Spring Boot and Apache Geode.
By the end of this lesson, users should have a better understanding of what the _auto-configuration_ support
provided by Spring Boot Data Geode (SBDG) actually does.
This guide builds on and compliments the <<geode-autoconfiguration-annotations>> chapter with concrete examples.
Let's begin.
@@ -108,7 +111,7 @@ SBDG will auto-configure a `ClientCache` instance by default.
We can make this more apparent by disabling the _auto-configuration_ of the `ClientCache` instance provided by SBDG:
.Disabling ClientCache auto-configuration
.Disabling ClientCache Auto-configuration
[source,java]
----
@SpringBootApplication(exclude = ClientCacheAutoConfiguration.class)
@@ -149,7 +152,7 @@ could not be created. The `CustomerRepository` could not be created because the
could not be created. The "Customers" Region could not be created because there was no cache instance available
(e.g. `ClientCache`).
The `ClientCache` auto-configuration is equivalent to the following:
The `ClientCache` _auto-configuration_ is equivalent to the following:
.Equivalent ClientCache configuration
[source,java]
@@ -169,7 +172,7 @@ That is, we would need to explicitly declare the `@ClientCacheApplication` annot
We are also using the Spring Data (Geode) _Repository_ infrastructure in the Customer Service application.
This should be evident from our definition of the application-specific `CustomerRepository` interface.
If we disable the auto-configuration of the Spring Data Repository infrastructure:
If we disable the _auto-configuration_ of the Spring Data Repository infrastructure:
. Disabling Spring Data Repositories
[source,java]
@@ -181,8 +184,8 @@ public class CustomerServiceApplication {
}
----
We would run into a similar error:
We would run into a similar error:
.Error resulting from no proxied CustomerRepository instance
[source,txt]
----
@@ -207,7 +210,7 @@ Parameter 0 of method runner in example.app.crm.CustomerServiceApplication requi
The Spring Data _Repository auto-configuration_ even takes care of locating our application Repository interface
definitions for us.
Without auto-configuration, you would need to:
Without _auto-configuration_, you would need to:
.Equivalent Spring Data Repositories configuration
[source,java]
@@ -648,7 +651,7 @@ application domain model types, such as when using a 3rd party library.
So, SBDG auto-configures PDX and uses Spring Data Geode's `MappingPdxSerializer` as the `PdxSerializer` to de/serialize
all application domain types.
If we disable PDX auto-configuration, we can see the effects of trying to serialize a non-serializable type, `Customer`.
If we disable PDX _auto-configuration_, we can see the effects of trying to serialize a non-serializable type, `Customer`.
First, let's destroy the server-side "Customers" Region:
.Destroy "Customers" Region
@@ -755,13 +758,161 @@ public class CustomerServiceApplication {
`@EnablePdx` is responsible for configuring PDX serialization and registering SDG's `MappingPdxSerializer`.
[[geode-samples-boot-configuration-clientserver-secure]]
[[geode-samples-boot-configuration-clientserver-security]]
== Securing the Client & Server
The last bit of _auto-configuration_ provided by SBDG that we will look at in this guide involves Security,
and specifically Authentication/Authorization (Auth) and Transport Layer Security (TLS) using SSL.
In today's age, Security is no laughing matter and making sure your applications are secure is a first-class concern.
This is why SBDG takes Security very seriously and attempts to make this as simple as possible. You are definitely
encouraged to read the relevant <<geode-security,chapter>> in this Reference Documentation on the provided Security
_auto-configuration_ support.
We will now expand on our example to secure the client and server processes, with both Auth and TLS using SSL, and then
see how SBDG helps us properly configure these concerns, easily and reliably.
[[geode-samples-boot-configuration-clientserver-security-server]]
==== Securing the server
First, we must secure the cluster.
In a nutshell, when using the Apache Geode API (with no help from Spring), you must do the following:
1. Implement the `org.apache.geode.security.SecurityManager` interface.
2. Configure the `SecurityManager` using the GemFire/Geode `security-manager` property in `gemfire.properties`.
3. Either create a `gfsecurity.properties` file and set the `security-username` and `security-password` properties, or...
4. Implement the `org.apache.geode.security.AuthInitialize` interface and set the `security-peer-auth-init` property
in `gemfire.properties` as described {apache-geode-docs}/managing/security/implementing_authentication.html[here].
5. Then launch the cluster, and its members using _Gfsh_ in the proper order.
That is a lot of tedious work and if you mess up any bit of the configuration, then either your servers will fail
to start correctly, or worse, not be secure.
NOTE: SBDG does provide server-side, peer Security _auto-configuration_ support. However, you must then configure
and bootstrap your GemFire/Geode servers with Spring.
[[geode-samples-boot-configuration-clientserver-security-client]]
==== Securing the client
If you were to just run the client, Customer Service application when SSL is not enabled, you would hit
the following error on startup:
.AuthenticationRequiredException
[source,txt]
----
15:26:10.598 [main] ERROR o.a.g.i.c.GemFireCacheImpl - org.apache.geode.security.AuthenticationRequiredException: No security credentials are provided
15:26:10.607 [main] ERROR o.s.b.SpringApplication - Application run failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'runner' defined in example.app.crm.CustomerServiceApplication: Unsatisfied dependency expressed through method 'runner' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'customerRepository': Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'Customers': Cannot resolve reference to bean 'gemfireCache' while setting bean property 'cache'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'gemfireCache': FactoryBean threw exception on object creation; nested exception is java.lang.RuntimeException: Error occurred when initializing peer cache
....
at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) [spring-boot-2.0.9.RELEASE.jar:2.0.9.RELEASE]
at example.app.crm.CustomerServiceApplication.main(CustomerServiceApplication.java:51) [main/:?]
....
Caused by: org.apache.geode.security.AuthenticationRequiredException: No security credentials are provided
at org.apache.geode.internal.cache.tier.sockets.HandShake.readMessage(HandShake.java:1396) ~[geode-core-1.2.1.jar:?]
at org.apache.geode.internal.cache.tier.sockets.HandShake.handshakeWithServer(HandShake.java:1251) ~[geode-core-1.2.1.jar:?]
at org.apache.geode.cache.client.internal.ConnectionImpl.connect(ConnectionImpl.java:117) ~[geode-core-1.2.1.jar:?]
at org.apache.geode.cache.client.internal.ConnectionFactoryImpl.createClientToServerConnection(ConnectionFactoryImpl.java:136) ~[geode-core-1.2.1.jar:?]
at org.apache.geode.cache.client.internal.QueueManagerImpl.initializeConnections(QueueManagerImpl.java:466) ~[geode-core-1.2.1.jar:?]
at org.apache.geode.cache.client.internal.QueueManagerImpl.start(QueueManagerImpl.java:303) ~[geode-core-1.2.1.jar:?]
at org.apache.geode.cache.client.internal.PoolImpl.start(PoolImpl.java:343) ~[geode-core-1.2.1.jar:?]
at org.apache.geode.cache.client.internal.PoolImpl.finishCreate(PoolImpl.java:173) ~[geode-core-1.2.1.jar:?]
at org.apache.geode.cache.client.internal.PoolImpl.create(PoolImpl.java:159) ~[geode-core-1.2.1.jar:?]
at org.apache.geode.internal.cache.PoolFactoryImpl.create(PoolFactoryImpl.java:321) ~[geode-core-1.2.1.jar:?]
at org.apache.geode.internal.cache.GemFireCacheImpl.determineDefaultPool(GemFireCacheImpl.java:2922) ~[geode-core-1.2.1.jar:?]
at org.apache.geode.internal.cache.GemFireCacheImpl.initializeDeclarativeCache(GemFireCacheImpl.java:1369) ~[geode-core-1.2.1.jar:?]
at org.apache.geode.internal.cache.GemFireCacheImpl.initialize(GemFireCacheImpl.java:1195) ~[geode-core-1.2.1.jar:?]
at org.apache.geode.internal.cache.GemFireCacheImpl.basicCreate(GemFireCacheImpl.java:758) ~[geode-core-1.2.1.jar:?]
at org.apache.geode.internal.cache.GemFireCacheImpl.createClient(GemFireCacheImpl.java:731) ~[geode-core-1.2.1.jar:?]
at org.apache.geode.cache.client.ClientCacheFactory.basicCreate(ClientCacheFactory.java:262) ~[geode-core-1.2.1.jar:?]
at org.apache.geode.cache.client.ClientCacheFactory.create(ClientCacheFactory.java:212) ~[geode-core-1.2.1.jar:?]
at org.springframework.data.gemfire.client.ClientCacheFactoryBean.createCache(ClientCacheFactoryBean.java:400) ~[spring-data-geode-2.0.14.RELEASE.jar:2.0.14.RELEASE]
...
----
Even though SBDG provides _auto-configuration_ support for client Security, and specifically Auth in this case,
minimally you still must specify a username and password.
This is as easy as setting a username/password in Spring Boot `application.properties`:
link:{samples-dir}/boot/configuration/src/main/resources/application-security.properties[]
The act of setting a username and password triggers the client Security _auto-configuration_ provided by SBDG. There are
many aspects to configuring client Security in Apache Geode/Pivotal GemFire properly. All you need worry about is
supplying the credentials.
In this example, this has been conveniently setup for you. All you need do is enable the Spring "security" profile
when running the `CustomerServiceApplication` class.
You can enable the Spring "security" profile with:
.Enable Spring "security" Profile
[source,txt]
----
-Dspring.profiles.active=security
----
By doing so, the `application-security.properties` file containing the configured username/password properties
is included at application startup.
However, even with the username/password properties set in Spring Boot `application.properties`, if you were to disable
the client Security _auto-configuration_:
.Disabling Client Security Auto-configuration
[source,java]
----
@SpringBootApplication(exclude = ClientSecurityAutoConfiguration.class)
@EnableEntityDefinedRegions(basePackageClasses = Customer.class)
@EnableClusterConfiguration(useHttp = true)
public class CustomerServiceApplication {
...
}
----
Then, our client, Customer Service application would not be able to authenticate, and again we would receive an error,
as shown above:
.AuthenticationRequiredException
[source,txt]
----
Caused by: org.apache.geode.security.AuthenticationRequiredException: No security credentials are provided
at org.apache.geode.internal.cache.tier.sockets.HandShake.readMessage(HandShake.java:1396) ~[geode-core-1.2.1.jar:?]
at org.apache.geode.internal.cache.tier.sockets.HandShake.handshakeWithServer(HandShake.java:1251) ~[geode-core-1.2.1.jar:?]
....
----
Without the support of SBDG's client Security _auto-configuration_, you would need to explicitly "enable" security:
.Explicitly Enable Security
[source,java]
----
@SpringBootApplication
@ClientCacheApplication
@EnableSecurity
@EnableEntityDefinedRegions(basePackageClasses = Customer.class)
@EnableClusterConfiguration(useHttp = true)
public class CustomerServiceApplication {
...
}
----
That is, you would 1) still set the username/password properties in Spring Boot `application.properties` and 2) you
would additionally need to explicitly declare the `@EnableSecurity` annotation.
Therefore, SBDG (with the help of SDG under-the-hood) does the heavy lifting, automatically for you.
[[geode-samples-boot-configuration-conclusion]]
== Conclusion
Hopefully this guide has now given you a better understanding of what the _auto-configuration_ support provided by
Spring Boot for Apache Geode/Pivotal GemFire is giving you when developing Apache Geode or Pivotal GemFire applications
with Spring.
Spring Boot for Apache Geode/Pivotal GemFire (SBDG) is giving you when developing Apache Geode or Pivotal GemFire
applications with Spring.
This guide is by no means complete in that it does not cover all the _auto-configuration_ provided by SBDG. SBDG
additionally provides _auto-configuration_ for Spring's Cache Abstraction, Continuous Query (CQ), Function Execution
& Implementations, `GemfireTemplates` and Spring Session. However, the concepts and effects are similar to what
has been presented above.
We leave it as an exercise for you to explore and understand the remaining _auto-configuration_ bits using this guide
as a reference for your learning purposes.

View File

@@ -0,0 +1,192 @@
/*
* Copyright 2019 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
*
* https://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 example.app.crm.config;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.util.Optional;
import org.apache.geode.management.internal.security.ResourceConstants;
import org.apache.shiro.util.Assert;
import org.apache.shiro.util.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.data.gemfire.config.admin.remote.RestHttpGemfireAdminTemplate;
import org.springframework.data.gemfire.config.annotation.ClusterConfigurationConfiguration;
import org.springframework.http.HttpHeaders;
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.lang.Nullable;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.client.RestTemplate;
/**
* Spring {@link Configuration} class used to configure and initialize a Java Platform {@link Authenticator},
* which is used by the Java Platform anytime a Java process needs to make a secure network connection.
*
* @author John Blum
* @see java.net.Authenticator
* @see java.net.PasswordAuthentication
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.Profile
* @see org.springframework.core.env.Environment
* @since 1.0.0
*/
@Configuration
@Profile("security")
@SuppressWarnings("unused")
public class HttpSecurityConfiguration {
private static final String DEFAULT_USERNAME = "test";
private static final String DEFAULT_PASSWORD = DEFAULT_USERNAME;
@Bean
public Authenticator authenticator(Environment environment) {
Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
String username =
environment.getProperty("spring.data.gemfire.security.username", DEFAULT_USERNAME);
String password =
environment.getProperty("spring.data.gemfire.security.password", DEFAULT_PASSWORD);
return new PasswordAuthentication(username, password.toCharArray());
}
};
Authenticator.setDefault(authenticator);
return authenticator;
}
@Bean
BeanPostProcessor schemaObjectInitializerPostProcessor(Environment environment) {
return new BeanPostProcessor() {
@Nullable @Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ClusterConfigurationConfiguration.ClusterSchemaObjectInitializer) {
Optional.of(bean)
.map(ClusterConfigurationConfiguration.ClusterSchemaObjectInitializer.class::cast)
.map(schemaObjectInitializer -> invokeMethod(schemaObjectInitializer, "getSchemaObjectContext"))
.filter(ClusterConfigurationConfiguration.SchemaObjectContext.class::isInstance)
.map(ClusterConfigurationConfiguration.SchemaObjectContext.class::cast)
.map(schemaObjectContext -> invokeMethod(schemaObjectContext, "getGemfireAdminOperations"))
.filter(RestHttpGemfireAdminTemplate.class::isInstance)
.map(RestHttpGemfireAdminTemplate.class::cast)
.map(gemfireAdminTemplate -> invokeMethod(gemfireAdminTemplate, "getRestOperations"))
.filter(RestTemplate.class::isInstance)
.map(RestTemplate.class::cast)
.ifPresent(restTemplate -> registerInterceptor(restTemplate,
new SecurityAwareClientHttpRequestInterceptor(environment)));
}
return bean;
}
};
}
private RestTemplate registerInterceptor(RestTemplate restTemplate,
ClientHttpRequestInterceptor clientHttpRequestInterceptor) {
restTemplate.getInterceptors().add(clientHttpRequestInterceptor);
return restTemplate;
}
@SuppressWarnings("unchecked")
private <T> T invokeMethod(Object target, String methodName) {
return this.doOperationSafely(() -> {
Method method = target.getClass().getDeclaredMethod(methodName);
ReflectionUtils.makeAccessible(method);
return (T) ReflectionUtils.invokeMethod(method, target);
});
}
private <T> T doOperationSafely(ExceptionThrowingOperation<T> operation) {
try {
return operation.doOperation();
}
catch (Exception ignore) {
return null;
}
}
@FunctionalInterface
interface ExceptionThrowingOperation<T> {
T doOperation() throws Exception;
}
public static class SecurityAwareClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
private final Environment environment;
public SecurityAwareClientHttpRequestInterceptor(Environment environment) {
Assert.notNull(environment, "Environment is required");
this.environment = environment;
}
protected boolean isAuthenticationEnabled() {
return StringUtils.hasText(getUsername()) && StringUtils.hasText(getPassword());
}
protected String getUsername() {
return this.environment.getProperty("spring.data.gemfire.security.username");
}
protected String getPassword() {
return this.environment.getProperty("spring.data.gemfire.security.password");
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
HttpHeaders requestHeaders = request.getHeaders();
if (isAuthenticationEnabled()) {
requestHeaders.add(ResourceConstants.USER_NAME, getUsername());
requestHeaders.add(ResourceConstants.PASSWORD, getPassword());
}
return execution.execute(request, body);
}
}
}

View File

@@ -0,0 +1,4 @@
# Spring Boot application.properties containing Spring Data GemFire Security properties
spring.data.gemfire.security.username=test
spring.data.gemfire.security.password=test

View File

@@ -1 +1,3 @@
# Spring Boot application.properties containing Spring Data GemFire properties
spring.data.gemfire.cache.log-level=error

View File

@@ -0,0 +1,5 @@
# Gfsh shell script to start a secure GemFire/Geode cluster
start locator --name=LocatorOne --classpath=${SBDG_HOME}/apache-geode-extensions/build/libs/apache-geode-extensions-1.0.0.BUILD-SNAPSHOT.jar --properties-file=${SBDG_HOME}/spring-geode-samples/boot/configuration/src/main/resources/geode/config/gemfire.properties
connect --user=test --password=test
start server --name=ServerOne --classpath=${SBDG_HOME}/apache-geode-extensions/build/libs/apache-geode-extensions-1.0.0.BUILD-SNAPSHOT.jar --properties-file=${SBDG_HOME}/spring-geode-samples/boot/configuration/src/main/resources/geode/config/gemfire.properties

View File

@@ -0,0 +1,4 @@
#!/bin/bash
gfsh -e "run --file=${SBDG_HOME}/spring-geode-samples/boot/configuration/src/main/resources/geode/bin/start-secure-cluster.gfsh"
#gfsh -e "run --file=/Users/jblum/pivdev/spring-boot-data-geode/spring-geode-samples/boot/configuration/src/main/resources/geode/bin/start-secure-cluster.gfsh"

View File

@@ -0,0 +1,7 @@
# GemFire Properties
log-level=config
security-manager=org.springframework.geode.security.TestSecurityManager
#security-peer-auth-init=org.springframework.geode.security.TestAuthInitialize.create
security-username=test
security-password=test