SSL-Based/Secured FeignClient Example in Java Microservices With Eureka
In this article, I will explain how to develop a Feign Client application/service to establish two way SSL based communication.
Join the DZone community and get the full member experience.
Join For FreePrerequisite:
- You should have one Discovery Server up and running
- An SSL based microservice registered with the above Discovery Server up and running which we are going to consume from below FeignClient
- You need to generate your client application's keystore and trust store. You need to add your server certificate into your client application's trustStore and add your client application's certificate in the Server application's trustStore.
Before reading this article make sure that you know about Spring Feign Client. In this article, I will explain how to develop a Feign Client application/service to establish two way SSL based communication. In simple words, how a Feign Client can make an HTTPS REST service call. Also for this project, I assumed that we have a Eureka discovery server and one SSL based Spring boot microservice called global-repository. In other words from our FeignClient below, we are going to consume an SSL based microservice (global-repository)with a valid certificate.
Step 1. Click https://start.spring.io/ and open Spring initializr. Now do exactly what I am doing in the below screenshot. (Group, Artifact, etc fill as per your choice)
Now import the project to your eclipse/Intellij
First, add the below two lines of codes into your application class FeignClientDemoApplication.
@EnableFeignClients
@EnableDiscoveryClient
In our case it should look like:
xxxxxxxxxx
public class FeignClientDemoApplication {
Now rename application.properties as application.yml and modify it like below(if you are not using a discovery server please do not use eureka section below and remove @EnableDiscoveryClient from FeignClientDemoApplication.java and also remove the dependency from your pom.xml):
xxxxxxxxxx
server.port: 9909
spring:
application.name: feign-client-demo
eureka:
client:
serviceUrl:
defaultZone: ${EUREKA_URI:http://localhost:9001/eureka}
registryFetchIntervalSeconds: 1
instance:
leaseRenewalIntervalInSeconds: 1
Step 2: Create a new java class called SSLUtil for SSL configuration and modify it like below:
xxxxxxxxxx
package com.example.bhaiti.assam.feignclientdemo;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
import javax.net.ssl.KeyManager;
public class SSLUtil {
private static TrustManager[] trustAllCerts = null;
private static String keymanageralgorithm = null;
private static SSLContext sslContext = null;
/*
static {
//for localhost testing only
javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier(
new javax.net.ssl.HostnameVerifier(){
public boolean verify(String hostname,
javax.net.ssl.SSLSession sslSession) {
if (hostname.equals("localhost")) {
return true;
}
return false;
}
});
}*/
public static void setupSslContext() {
boolean trustall = false;
try {
String keyStorePath = "C:\\Users\\prate\\ssl\\client\\keyStore.jks";
String trustStorePath = "C:\\Users\\prate\\ssl\\client\\trustStore.jks";
String keyStorePw = "deva1972";
String trustStorePw = "deva1972";
String keyPass = "deva1972";
String trustAllCertificate = "False";
String keystoreType = "JKS";
keymanageralgorithm = "SunX509"; // For IBM it should be IbmX509
trustAllCerts = new TrustManager[] { new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
} };
if (trustAllCertificate.equalsIgnoreCase("True")) {
trustall = true;
}
if (keystoreType.equalsIgnoreCase("JKS"))
sslContext = initializeSSLContext(keyStorePath, keyStorePw, trustStorePath, trustStorePw, keyPass,
trustall);
else
sslContext = initializeSSLContextP12Cert(keyStorePath, keyStorePw, trustStorePath, trustStorePw,
keyPass, trustall);
} catch (Exception exp) {
System.out
.println("ConfigException exception occurred while reading the config file : " + exp.getMessage());
exp.printStackTrace();
}
}
public static SSLSocketFactory getClientSSLSocketFactory() {
if(sslContext != null) {
return sslContext.getSocketFactory();
}
return null;
}
/**
*
* @param keyStorePath
* @param pwKeyStore
* @param trustStorePath
* @param pwTrustStore
* @param keyPass
* @return
* @throws Exception
*/
private static SSLContext initializeSSLContext(final String keyStorePath, final String pwKeyStore,
final String trustStorePath, final String pwTrustStore, final String keyPass, final boolean trustall) {
System.out.println(" In initializeSSLContext");
char[] keyStorePw = pwKeyStore.toCharArray();
char[] trustStorePw = pwTrustStore.toCharArray();
char[] keyPw = keyPass.toCharArray();
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextInt();
KeyStore ks = null;
try {
ks = KeyStore.getInstance("JKS");
} catch (KeyStoreException exp) {
System.out.println("KeyStoreException exception occurred while reading the config file : " + exp.getMessage());
}
FileInputStream fis = null;
try {
try {
fis = new FileInputStream(keyStorePath);
} catch (FileNotFoundException exp) {
System.out.println("FileNotFoundException exception occurred " + exp.getMessage());
}
try {
ks.load(fis, keyStorePw);
} catch (NoSuchAlgorithmException exp) {
System.out.println("NoSuchAlgorithmException exception occurred " + exp.getMessage());
} catch (CertificateException exp) {
System.out.println("CertificateException exception occurred " + exp.getMessage());
} catch (IOException exp) {
System.out.println("CertificateException exception occurred " + exp.getMessage());
}
} finally {
if (fis != null)
try {
fis.close();
} catch (IOException exp) {
System.out.println("IOException exception occurred " + exp.getMessage());
}
}
System.out.println("[initializeSSLContext] KMF keystorepw loaded.");
KeyManagerFactory kmf = null;
try {
kmf = KeyManagerFactory.getInstance(keymanageralgorithm);
} catch (NoSuchAlgorithmException exp) {
System.out.println("IOException exception occurred " + exp.getMessage());
}
try {
kmf.init(ks, keyPw);
} catch (UnrecoverableKeyException exp) {
System.out.println("UnrecoverableKeyException exception occurred " + exp.getMessage());
} catch (KeyStoreException exp) {
System.out.println("KeyStoreException exception occurred " + exp.getMessage());
} catch (NoSuchAlgorithmException exp) {
System.out.println("NoSuchAlgorithmException exception occurred " + exp.getMessage());
}
System.out.println("[initializeSSLContext] KMF init done.");
KeyStore ts = null;
try {
ts = KeyStore.getInstance("JKS");
} catch (KeyStoreException exp) {
System.out.println("NoSuchAlgorithmException exception occurred " + exp.getMessage());
}
FileInputStream tfis = null;
SSLContext sslContext = null;
try {
tfis = new FileInputStream(trustStorePath);
ts.load(tfis, trustStorePw);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(keymanageralgorithm);
tmf.init(ts);
System.out.println("[initializeSSLContext] Truststore initialized");
sslContext = SSLContext.getInstance("TLS");
if (trustall)
sslContext.init(kmf.getKeyManagers(), trustAllCerts, secureRandom);
else
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), secureRandom);
} catch (NoSuchAlgorithmException exp) {
System.out.println("NoSuchAlgorithmException exception occurred " + exp.getMessage());
} catch (CertificateException exp) {
System.out.println("NoSuchAlgorithmException exception occurred " + exp.getMessage());
} catch (IOException exp) {
System.out.println("NoSuchAlgorithmException exception occurred " + exp.getMessage());
} catch (KeyStoreException exp) {
System.out.println("NoSuchAlgorithmException exception occurred " + exp.getMessage());
} catch (KeyManagementException exp) {
System.out.println("NoSuchAlgorithmException exception occurred " + exp.getMessage());
} finally {
if (tfis != null)
try {
tfis.close();
} catch (IOException exp) {
System.out.println("NoSuchAlgorithmException exception occurred " + exp.getMessage());
}
}
if ((sslContext == null)) {
System.out.println("[initializeSSLContext] sslContext is null");
System.exit(-1);
}
return sslContext;
}
/**
*
* @param keyStorePath
* @param pwKeyStore
* @param trustStorePath
* @param pwTrustStore
* @param keyPass
* @return
* @throws Exception
*/
private static SSLContext initializeSSLContextP12Cert(final String keyStorePath, final String pwKeyStore,
final String trustStorePath, final String pwTrustStore, final String keyPass, final boolean trustall) {
System.out.println("In initializeSSLContextP12Cert");
SSLContext sslContext = null;
String keystore = keyStorePath;
String keystorepass = pwKeyStore;
String truststore = trustStorePath;
String truststorepass = pwTrustStore;
try {
KeyStore clientStore = KeyStore.getInstance("PKCS12");
clientStore.load(new FileInputStream(keystore), keystorepass.toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(keymanageralgorithm);
kmf.init(clientStore, keystorepass.toCharArray());
KeyManager[] kms = kmf.getKeyManagers();
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(new FileInputStream(truststore), truststorepass.toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(keymanageralgorithm);
tmf.init(trustStore);
TrustManager[] tms = tmf.getTrustManagers();
sslContext = SSLContext.getInstance("TLS");
if (trustall)
sslContext.init(kms, trustAllCerts, new SecureRandom());
else
sslContext.init(kms, tms, new SecureRandom());
} catch (NoSuchAlgorithmException exp) {
System.out.println("NoSuchAlgorithmException exception occurred " + exp.getMessage());
} catch (CertificateException exp) {
System.out.println("CertificateException exception occurred " + exp.getMessage());
} catch (IOException exp) {
System.out.println("IOException occurred while reading the key file " + exp.getMessage());
} catch (KeyStoreException exp) {
System.out.println("KeyStoreException exception occurred " + exp.getMessage());
} catch (KeyManagementException exp) {
System.out.println("KeyManagementException exception occurred " + exp.getMessage());
} catch (UnrecoverableKeyException exp) {
System.out.println("UnrecoverableKeyException exception occurred " + exp.getMessage());
}
if ((sslContext == null)) {
System.out.println("[initializeSSLContext] sslContext is null");
System.out.println("[initializeSSLContext] verify ssl config");
System.out.println("MyREST application exit with status code -1");
}
System.out.println("[initializeSSLContextP12Cert] Truststore and KeyStore initialized");
return sslContext;
}
}
I have designed the above class to support both PKCS12 (p12) and JKS keystore. Check variable keystoreType in the above codes, you need to assign keystoreType="PKCS12" for a PKCS12 (p12) certificate. But keep in mind that your trust store always should be in JKS format for this project. So create a truststore in JKS format.
Important point to note here is that you can see that I have commented out a portion of codes at the top. Normally when you create a key store you need to provide your server name when it prompted for “enter your first name and last name” and if that server name is not same as your REST service hostname you will get a certificate authentication error like localhost name/or your hostserver name not found (in this case hostname is localhost). In that case just uncomment that portion of code and modify localhost to our client server host name or just return always true for testing purpose.
Step 3: Now create a java class called CustomFeignConfiguration.java. This is our customized feign configuration class through which we will configure our keystore and truststore for an SSL Handshake to consume remote REST service through a secured channel.
Please modify it like below:
xxxxxxxxxx
package com.example.bhaiti.assam.feignclientdemo;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory;
import org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import feign.Client;
public class CustomFeignConfiguration {
public Client getfeignClient()
{
Client trustSSLSockets = new Client.Default(
SSLUtil.getClientSSLSocketFactory(),
null);
return trustSSLSockets;
}
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
return new LoadBalancerFeignClient(new Client.Default(null, null),
cachingFactory, clientFactory);
}
}
Step 4: Now create your feign client like below. (You need to make changes as per your requirement this is just an example)
xxxxxxxxxx
package com.example.bhaiti.assam.feignclientdemo;
import java.net.URI;
import java.util.Map;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
//Here we are informing FeignClient that we have a custom configuration class
//FeignConfiguration and requesting it to use that as configuration class
name = "sslClient", configuration = FeignConfiguration.class) (
public interface GRClient{
("/repository")
ResponseEntity<?> getAllRepsitoryForASite(URI determinedBasePathUri,
HttpHeaders header,
value="siteName",required = false) String siteName); (
(value = "/register", consumes = "application/json", produces = "application/json")
public ResponseEntity<String> register(URI determinedBasePathUri,
"Authorization") String token, (
Repository repo);
}
In the above codes please notice, the first parameter of each method. Here we are passing the first parameter a URL of the remote REST service. Feign Client will expand it like determinedBasePathUri/retister (in our case it would be https:localhost:8443/register with a authorization header(token) for authentication (user id and password) and request body repo which is a object of type Repository.
Step 5: Finally create a controller class for this FeignClient for testing the above implementation:
package com.example.bhaiti.assam.feignclientdemo;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClient;
import com.netflix.discovery.shared.Application;
public class GRController {
GRClient grClient;
//if you are not using a EurekaClient remove below codes
//and return a hardcoded url from getBaseUrl() instead.
private EurekaClient eurekaClient;
public String getBaseUrl() {
Application application = eurekaClient.getApplication("global-repository");
InstanceInfo instanceInfo = application.getInstances().get(0);
String hostname = instanceInfo.getHostName();
int port = instanceInfo.getPort();
return "https://" + hostname + ":" + port;
//if you are not using a discovery server just return like below or as per your requirement
//return "https://" + host + port;
}
("/repository")
ResponseEntity<?> getAllRepsitoryForASite( HttpHeaders header, (value="siteName",required = false) String siteName){
URI determinedBasePathUri = URI.create(getBaseUrl());
return grClient.getAllRepsitoryForASite(determinedBasePathUri,header, siteName);
}
("/register")
public ResponseEntity<String> register( ("Authorization") String token, Repository repo) {
//here you need to change Repository repo as per your requirement
URI determinedBasePathUri = URI.create(getBaseUrl());
return grClient.register(determinedBasePathUri,token,repo);
}
}
Now when you make a REST service call to this service like http://localhost:8080/repository?siteName=Jorhat, inside REST api method getAllRepsitoryForASite it will call first getBaseUrl method and from getBaseUrl it will retrieve hostname and port detail of secured microservice global-repository which is registered in Discovery Server. Finally the URL has been passed as first parameter of method getAllRepsitoryForASite(determinedBasePathUri,header, siteName) of FeignClient. The Feign Client will use the certifiate that has been stored in keyStore to make a SSL based REST call to microservice called global-repository.
Hope you enjoyed this article. Making a secured call is not so easy. You should be very careful while creating your own keyStore, trustStore and client certificate etc. If you are doing a home work, while creating the certificate in the prompt “enter your first name and last name”, always provide localhost or your machine name. Never provide your name. To know more about SSL based communication you can refer to my article https://dzone.com/articles/ssl-based-https-restful-and-soap-client-applicatio
Opinions expressed by DZone contributors are their own.
Comments