Backend Application
Bootstrap App
In this section, you will use a web service called Spring Initializr to prepare the Spring Boot project.
To bootstrap your application
- Go to https://start.spring.io/ to use Spring Initializr.
- Enter the Project Metadata, as follows:
- Group: the group ID, such as
com.finastra
- Artifact: the artifact ID, which is your application name, such as:
events
- Group: the group ID, such as
- Click Generate. A ZIP archive, with the name of your artifact, is ready for you to download.
- Unpack the downloaded archive, and open the project in your favorite IDE.
- Open pom.xml. The
<parent>
version must be the same as the version ofspring-boot-...
dependencies. To do this automatically, add thespring-boot.version
property as a variable in the<properties>
field from the pom.xml file.
...<properties>
...<spring-boot.version>SPRING_BOOT_VERSION</spring-boot.version>
</properties>
...
- In pom.xml add a set of
<dependencies>
from Maven repository, apart from the Spring Initializr ones. Use the${spring-boot.version}
value for the spring-boot dependencies<version>
key.
...<dependencies>
<!-- dependencies added by Spring Initializr -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- dependencies added from Maven repository -->
<!-- spring-boot dependencies -->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- other mandatory dependencies -->
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-contract-wiremock -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-wiremock</artifactId>
<version>2.2.2.RELEASE</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>8.17</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.swagger/swagger-core -->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-core</artifactId>
<version>1.6.1</version>
</dependency>
</dependencies>
...
- Update the
<plugins>
element as follows:
...
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- https://mvnrepository.com/artifact/io.swagger/swagger-codegen-maven-plugin -->
<plugin>
<groupId>io.swagger</groupId>
<artifactId>swagger-codegen-maven-plugin</artifactId>
<version>2.4.14</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec>
<language>spring</language>
<configOptions>
<sourceFolder>src/gen/java/main</sourceFolder>
<java8>true</java8>
<interfaceOnly>true</interfaceOnly>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
...
- Create a configuration file, named openapi.yaml, in /src/main/resources/. Enter the following content:
swagger: '2.0'
info:
description: "."
contact:
name: API Support
url: https://community.fusionfabric.cloud/index.html
x-finastra-category: Other
x-finastra-subcategory: ''
x-finastra-tags: []
x-finastra-maturity-level: BETA
x-finastra-audience: PUBLIC
x-finastra-commitId: 57e5d69943bdd6c4d49f3d422e8fe1b593761c17
title: Clock Service Events
version: 1.0.1
x-finastra-short-description: Clock Service Events
x-finastra-channel-type: B2B
paths:
/sample/clock-service/v1/datetime-published:
post:
tags:
- Clock event
operationId: clock
summary: This event occurs when a new UTC datetime is published.
description: This event occurs every 15 seconds when a new UTC datetime in ISO-8601 format is published.
consumes:
- application/json
parameters:
- name: event
in: body
required: true
schema:
type: object
required:
- tenant
properties:
tenant:
description: Identifier of the tenant
type: string
eventTime:
description: UTC date and time of event expressed in ISO-8601 format
type: string
additionalProperties: true
example:
tenant: sandbox
eventTime: '2019-10-21T13:07:28.219'
responses:
'204':
description: No content
"/sample/clock-service/v1":
options:
summary: Responds with header WebHook-Allowed-Origin to allow registering / as an event endpoint.
operationId: validateEventEndpoint
responses:
200:
description: OK
headers:
WebHook-Allowed-Origin:
type: string
description: Value must be "api.fusionfabric.cloud"
definitions: {}
This file is the YAML representation of the Events specification in OpenAPI format.You can find it under Actions menu. Make sure to convert it to YAML format for your project.
You are now ready to code your event notifications application.
By setting up webhooks in your backend application you will receive notifications from FusionFabric.cloud any time the event occurs.
You will be using a code generator Maven plugin - swagger-codegen-maven-plugin
from io.swagger
, to help you implement the two endpoints of the Clock Service Events specification, namely:
POST /sample/clock-service/v1/datetime-published
- to return the date and time at every 15 seconds.OPTIONS /sample/clock-service/v1
- to allow FusionFabric.cloud to test your webhook URLs. This endpoint will return an HTTP header -WebHook-Allowed-Origin
with the valueapi.fusionfabric.cloud
.
You create 4 packages in the src/main/java/com.finastra.events/ directory:
- controller package - which stores a REST controller object
- configuration package - which stores configuration objects
- exceptions package - which stores exception objects
- filter package - which stores filtering objects applied to any incoming HTTP requests
Controller
To implement the controller
package
- Create a package named controller.
- In Controller package, create the EventsApplicationController class with the following code:
package com.finastra.events.controller;
/**
* This class defines the endpoints on which the application listens
*/
@RestController
@RequestMapping
public class EventsApplicationController implements SampleApi {
private static final String WEB_HOOK_ALLOWED_ORIGIN = "WebHook-Allowed-Origin";
private static final String WEB_HOOK_FUSION_FABRIC_ORIGIN = "api.fusionfabric.cloud";
private final SimpMessagingTemplate template;
@Autowired
EventsApplicationController(SimpMessagingTemplate template) {
this.template = template;
}
@Override
public ResponseEntity<Void> clock(Event event) {
.info("Event received {}", event.toString());
log// Send event to the endpoint
this.template.convertAndSend("/clock/event", event);
// Return status code from the destination query
return ResponseEntity.ok().build();
}
@Override
public ResponseEntity<Void> validateEventEndpoint() {
return ResponseEntity.ok()
.header(WEB_HOOK_ALLOWED_ORIGIN, WEB_HOOK_FUSION_FABRIC_ORIGIN)
.build();
}
}
- Import the required libraries for EventsApplicationController above the class declaration.
import io.swagger.api.SampleApi;
import io.swagger.model.Event;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Configuration
To implement the configuration
package
- Create a package named configuration.
- In the configuration package, create 2 classes:
- MessagingConfiguration
- WebSocketConfiguration
- Write the following code for the MessagingConfiguration class:
package com.finastra.events.configuration;
/**
* This class configures the messaging URL to retrieve its public key
*/
@Component
@ConfigurationProperties(prefix = "messaging")
public class MessagingConfiguration {
private String serverUrl;
public MessagingConfiguration() {
}
public String getServerUrl() {
return serverUrl;
}
public void setServerUrl(String serverUrl) {
this.serverUrl = serverUrl;
}
}
- Import the required libraries for MessagingConfiguration above the class declaration.
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
- Write the following code for the WebSocketConfiguration class:
package com.finastra.events.configuration;
/**
* This class configures the web socket. It means that everytime the backend
* receives the events, they are forwarded to the UI
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
public static final String SOCKET_ENDPOINT = "/socket";
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
.addEndpoint(SOCKET_ENDPOINT)
registry.setAllowedOrigins("*")
.withSockJS();
}
}
- Import the required libraries for the WebSocketConfiguration above the class declaration.
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
Exceptions
To implement the exceptions
package
- Create a package named exceptions.
- In exceptions package, create the JwksException class with the following code:
package com.finastra.events.exceptions;
/**
* This class defines the exception that is thrown when the signature of the event
* could not be validated
*/
public class JwksException extends RuntimeException {
private static final String INVALID_JWKS_EXCEPTION = "JWKS exception: ";
private static final long serialVersionUID = -7708447901493903974L;
public JwksException(Exception e) {
super(INVALID_JWKS_EXCEPTION, e);
}
public JwksException(String message) {
super(INVALID_JWKS_EXCEPTION + message);
}
}
Resource Sharing Filter
To implement the filter
package
- Create a package named filter.
- In the filter package, create 4 classes:
- CorsFilter
- MutipleReadHttpRequest
- SignatureCheckFilter
- TraceFilter
- Write the following code for the CorsFilter class:
package com.finastra.events.filter;
/**
* This class defines the Cross Origin resource sharing filter to allow frontend to
* use backend
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter extends GenericFilterBean {
private static final String CLOCK_VALIDATION_URL = "/sample/clock-service/v1";
@Value("${events-application.allowed-origin}")
private List<String> allowedOrigins;
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
// Access-Control-Allow-Origin
String origin = request.getHeader("Origin");
.setHeader("Access-Control-Allow-Origin", allowedOrigins.contains(origin) ? origin : "");
response.setHeader("Vary", "Origin");
response
// Access-Control-Max-Age
.setHeader("Access-Control-Max-Age", "3600");
response
// Access-Control-Allow-Credentials
.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
responseboolean isWebHookValidation = request.getRequestURL().toString().endsWith(CLOCK_VALIDATION_URL);
if (HttpMethod.OPTIONS.toString().equalsIgnoreCase((request).getMethod())
&& !isWebHookValidation) {
.setStatus(HttpServletResponse.SC_OK);
response} else {
.doFilter(req, res);
chain}
}
}
- Import the required libraries for CorsFilter above the class declaration.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
- Write the following code for the MultipleReadHttpRequest class:
package com.finastra.events.filter;
/**
* This class enables HttpRequests to be read twice for checking the signature for
* de-serialization, which is not allowed by default
*/
public class MultipleReadHttpRequest extends HttpServletRequestWrapper {
private final byte[] body;
MultipleReadHttpRequest(HttpServletRequest request, byte[] body) {
super(request);
this.body = body;
}
static byte[] readBody(ServletRequest request) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
try (BufferedReader bufferedReader = request.getReader()) {
char[] charBuffer = new char[128];
int bytesRead;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
.append(charBuffer, 0, bytesRead);
stringBuilder}
return stringBuilder.toString().getBytes();
}
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return true;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
public int read() {
return byteArrayInputStream.read();
}
};
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
- Import the required libraries for MultipleReadHttpRequest above the class declaration.
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
- Write the following code for the SignatureCheckFilter class:
package com.finastra.events.filter;
/**
* This class defines a filter to check the signature header of the event to protect
* the application against events not sent by FFDC
*/
@Component
public class SignatureCheckFilter extends GenericFilterBean {
private final static Logger LOG = LoggerFactory.getLogger(SignatureCheckFilter.class);
private final RemoteJWKSet<SecurityContext> remoteJWKSet;
@Autowired
public SignatureCheckFilter(MessagingConfiguration messagingConfiguration) {
try {
final String jwksUrl = messagingConfiguration.getServerUrl() + "/jwks.json";
.info("server URL {}", messagingConfiguration.getServerUrl());
LOGthis.remoteJWKSet = new RemoteJWKSet<>(new URL(jwksUrl));
} catch (IOException e) {
.error("Failed to load jwks: ", e);
LOGthrow new JwksException(e);
}
}
/**
* This method filter rejects incoming event not coming from FFDC services
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
= (HttpServletRequest) request;
HttpServletRequest httpRequest String url = httpRequest.getRequestURL().toString();
boolean isWebSocket = (httpRequest.getMethod().equals("GET") || httpRequest.getMethod().equals("POST"))
&& url.contains("/socket/");
boolean isWebHookValidation = url.endsWith("/sample/clock-service/v1");
if (isWebSocket || isWebHookValidation) {
.doFilter(request, response);
chainreturn;
}
byte[] body = MultipleReadHttpRequest.readBody(httpRequest);
= new MultipleReadHttpRequest(httpRequest, body);
MultipleReadHttpRequest wrappedRequest = (HttpServletResponse) response;
HttpServletResponse httpResponse String signature = wrappedRequest.getHeader("Signature");
try {
// check signature
checkSignature(signature, body);
.doFilter(wrappedRequest, response);
chain} catch (JwksException e) {
.error("signature check failed", e);
LOG.sendError(HttpServletResponse.SC_FORBIDDEN);
httpResponse}
}
/**
* This method checks Signature of dataByte that matches the FFDC public Key
*
* @param fullSignature the full signature containing the key id, the algorithm and the signature
* @param dataByte the signed data
*/
private void checkSignature(String fullSignature, byte[] dataByte) {
try {
Signature sig = parseSignature(fullSignature);
final JWKMatcher jwkMatcher = new JWKMatcher.Builder()
.keyID(sig.keyId)
.algorithm(sig.algorithm)
.build();
final List<JWK> jwkList = remoteJWKSet.get(new JWKSelector(jwkMatcher), null); // security context is ignored for Remote JWK set
if (jwkList.size() != 1) {
.info("jwkList{}", jwkList.toString());
LOGthrow new JwksException("Unable to find unique matcher for keyId:"
+ sig.keyId + ", and algorithm:" + sig.algorithm + " in provided JWKS.");
}
final JWK jwk = jwkList.get(0);
final JWSVerifier verifier = new RSASSAVerifier((RSAKey) jwk);
.info("Signature {}", sig.signature);
LOGfinal Base64URL signatureBaseURL =
.encode(Base64.getMimeDecoder().decode(sig.signature));
Base64URLfinal JWSHeader jwsHeader = new JWSHeader(new JWSAlgorithm(jwk.getAlgorithm().getName()));
if (!verifier.verify(jwsHeader, dataByte, signatureBaseURL)) {
throw new JwksException("Invalid signature.");
}
} catch (JOSEException e) {
throw new JwksException(e);
}
}
private Signature parseSignature(String fullSignature) {
String[] signaturesVal = fullSignature.split(",");
if (signaturesVal.length == 3) {
String[] keyVal = signaturesVal[0].split("=");
String[] algorithmVal = signaturesVal[1].split("=");
String[] signVal = signaturesVal[2].split("=");
final String keyId = keyVal[1].replace("\"", "");
final String algorithm = algorithmVal[1].replace("\"", "");
final String signature = signVal[1].replace("\"", "");
return new Signature(keyId, algorithm, signature);
}
throw new JwksException("Unable to parse header " + fullSignature);
}
private static class Signature {
private final String keyId;
private final Algorithm algorithm;
private final String signature;
private Signature(String keyId, String algorithm, String signature) {
this.keyId = keyId;
this.algorithm = new Algorithm(algorithm);
this.signature = signature;
}
}
}
- Import the required libraries for SignatureCheckFilter above the class declaration.
import com.finastra.events.configuration.MessagingConfiguration;
import com.finastra.events.exceptions.JwksException;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKMatcher;
import com.nimbusds.jose.jwk.JWKSelector;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jose.util.Base64URL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URL;
import java.util.Base64;
import java.util.List;
- Write the following code for the TraceFilter class:
package com.finastra.events.filter;
/**
* This class adds diagnostic context to the logs
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TraceFilter extends GenericFilterBean {
public static final String FF_TRACE_ID = "ff-trace-id";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
= (HttpServletRequest) request;
HttpServletRequest httpRequest String ffTraceId = httpRequest.getHeader(FF_TRACE_ID);
if (ffTraceId != null) {
try (MDC.MDCCloseable mdcCloseable = MDC.putCloseable(FF_TRACE_ID, ffTraceId)) {
.doFilter(httpRequest, response);
chain}
} else {
.doFilter(httpRequest, response);
chain}
}
}
- Import the required libraries for TraceFilter above the class declaration.
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
Application Properties
To configure the application properties
- In /src/main/resources open the application.properties file, created by default
- Add the following content:
messaging.serverUrl=https://api.fusionfabric.cloud/messaging/v1
events-application.allowed-origin=http://localhost:4200
logging.level.org.springframework=ERROR
logging.level.com.mkyong=DEBUG
logging.pattern.console=%d{HH:mm:ss.SSS} [%t] %-5level %-36mdc{ff-trace-id:-} %logger{36} - %msg%n
This is how your backend project should look like at the end: