Angular and Spring Boot, part 2

I wrote a piece about base project setup to make Angular and Spring Boot play nice together. This piece builds upon that, but can also be read in isolation (except for the section where I make changes to the previous post. The previous post showed how to do basic project setup and to configure Angular code to get it built and integrated into a Spring Boot application, following the Maven module structure. This section is concerned with more applicative challenges: exchanging data between Angular and Java, protecting such services using Google Recaptcha, and enabling deep linking.

Updates Since Last Time

A few things set up previously, should be changed slightly.

First, I’ve updated my scripts slightly. Here’s a new copy_assets.sh:

#!/bin/bash

for SRC in projects/*/src/{assets,scripts,styles}/; do
	TMP="${SRC/projects/dist}"
	DST="${TMP/\/src/}"
	if [ -d "$SRC" ]; then
		echo "$SRC -> $DST"
		mkdir -p "$DST"
		rsync -r "$SRC" "$DST"
	fi
done

Difference is, we don’t delete the destination folder and we use rsync to merge in the contents instead of overwriting it. This allows us to mix and overwrite assets defined in other modules.

I’ve also updated my copy_lib_assets.sh:

#!/bin/bash

MODULE="application-name"
FOLDERS="assets/fonts styles scripts"

function aliases() {
	case "$1" in
		"styles")
			echo -n "css resources $1"
			;;
		"scripts")
			echo -n "js $1"
			;;
		"assets/fonts")
			echo -n "webfonts fonts $1"
			;;
		*)
			echo -n "$1"
	esac
}

while [ ! -z "$1" ]; do
	for FOLDER in $FOLDERS; do
		DST="dist/$MODULE/$FOLDER/"
		for ALIAS in $( aliases "$FOLDER" ); do
			SRC="node_modules/$1/$ALIAS/"
			if [ -d "$SRC" ]; then
				echo "$SRC -> $DST"
				mkdir -p "$DST"
				rsync -r "$SRC" "$DST"
			fi
		done
	done
	shift
done

for FOLDER in $FOLDERS; do
	DST="dist/$MODULE/$FOLDER/"
	for ALIAS in $( aliases "$FOLDER" ); do
		#SRC="src/$ALIAS/"
		SRC="projects/$MODULE/src/$ALIAS/"
		if [ -d "$SRC" ]; then
			echo "$SRC -> $DST"
			mkdir -p "$DST"
			rsync -r "$SRC" "$DST"
		fi
	done
done

Now, the script supports that some libraries may put their assets in different folders. The two alternatives in lines 40-41 is to support both multi-project workspaces and regular single project workspaces. The above configuration is for multi-project workspaces, which means we can combine the use of the two scripts in a library re-exporting assets as such in a package.json:

"scripts": {
	"build": "ng build && ./copy_lib_assets.sh '@fortawesome/fontawesome-free' 'primeng' && ./copy_assets.sh",

Second, there’s a few tweaks to the main POM.

We change the npm build to run in the prepare-package stage instead of the generate-resources stage. This will come in handy later.

		<!-- Build -->
		<execution>
			<id>npm-build</id>
			<goals>
				<goal>npm</goal>
			</goals>
			<configuration>
				<arguments>run build</arguments>
			</configuration>
			<phase>prepare-package</phase>
		</execution>

We also need to make sure that our Angular resources are included in our deployable, because adding it to resources is not enough for dumb reasons. I use war packaging, so I have to add something to the effect of:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>eu.group</groupId>
	<artifactId>application-name</artifactId>
	<packaging>war</packaging>
...
	<build>
		<resources>
			<resource>
				<directory>src/main/module/dist/module</directory>
				<targetPath>static</targetPath>
			</resource>
			<resource>
				<filtering>true</filtering>
				<directory>src/main/resources</directory>
			</resource>
		</resources>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-war-plugin</artifactId>
				<version>3.2.3</version>
				<configuration>
					<webResources>
						<resource>
							<directory>src/main/module/dist/module</directory>
							<targetPath>static</targetPath>
						</resource>
					</webResources>
				</configuration>
			</plugin>

The resources part we already saw last time, but including the resources in the WAR is new. You have to do something similar for JAR packaging.

Connecting Java and Angular

The web world likes worse JSON RESTful web-services, so we’ll have to provide that. With Spring Boot, we implement a rest controller:

import java.util.List;
import javax.validation.Valid;
import javax.validation.ValidationException;
import javax.ws.rs.core.MediaType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import lombok.extern.slf4j.Slf4j;

@RestController
@RequestMapping(path = "/api", produces = MediaType.APPLICATION_JSON)
@Slf4j
public class ApiController {
	@Autowired
	private RecaptchaService recaptcha;

	@GetMapping("/organisations")
	public List<Organisation> getOrganisations() {
		return service.getOrganisations();
	}
	
	@PostMapping(path = "/person", consumes = MediaType.APPLICATION_JSON)
	public Person putPerson(@RequestParam String token, @Valid @RequestBody Person person) {
		log.trace("Validating token");
		if (!recaptcha.validateToken(token)) {
			throw new ValidationException("Token invalid");
		}
		log.trace("Received person {}", person);
		return service.save(person);
	}
}

This should all be very standard. Lines 23-24 and 33-36 we’ll get to in the next section. Otherwise, we just use standard Java validation framework annotations to validate our input and a few Spring annotations to set up our web-service.

Our DTOs (like Person and Organisation) are simple Jackson objects created using Lombok. Here’s part of the Person object:

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.SuperBuilder;

@SuperBuilder(toBuilder = true)
@Getter
@Setter
@NoArgsConstructor
@ToString
public class Person {
	private String id;
	@NotNull
	@javax.validation.Valid
	@ApiModelProperty(required = true)
	private Organisation organisation;
	private String name;
	@NotBlank
	@Email
	@ApiModelProperty(required = true)
	private String email;
}

We use Lombok to generate getters/setters and an empty constructor for Jackson, and generate a builder and toString methods for ourselves. Fields are annotated with standard Java validation annotations.

The interface between the above Java service and our Angular code will be described using the Swagger/OpenAPI interface description language. We could write this specification by hand, but in our case we’ll generate it from the Java implementation. It is also possible to start with a Swagger description and generate Java code.

To generate Swagger from our interface, we just need a few annotations. In the code above, we have added some Swagger annotations (ll. 21 and 26) indicating required fields. You may want to add more documentation, but for our purposes, this suffices. For dumb reasons, the Sweagger generator cannot infer a field is required from the @NotNull and @NotBlank annotations.

To generate Swagger from our web-services, we include this in our POM:

		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger2</artifactId>
			<version>2.9.2</version>
		</dependency>
		<!-- 
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-data-rest</artifactId>
			<version>2.9.2</version>
		</dependency>
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-bean-validators</artifactId>
			<version>2.9.2</version>
		</dependency>
		 -->

The first loads Springfox Swagger generator. The last two have been commented out because the current version is not compatible with Spring Boot 2.2 and I do not want to use a development version for this project (3.0 is supposedly working fine). We add a Spring Boot configuration to enable this:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
//@Import({SpringDataRestConfiguration.class, BeanValidatorPluginsConfiguration.class})
public class ServiceConfig {

	@Bean
	public Docket api() {
		return new Docket(DocumentationType.SWAGGER_2) //
				.select()//
				.apis(RequestHandlerSelectors.basePackage(ApiController.class.getPackageName())) //
				.paths(PathSelectors.ant("/api/**"))//
				.build();
	}

}

We have commented out the inclusion of the broken plugins, but otherwise, this sets up a simple Swagger generator that automatically inspects our program classes and exposes a URL <application base>/v2/api-docs that can be used to look up our API documentation.

Now, we want to generate Angular Typescript code for interfacing with our new fancy API. for this, we use the swagger-angular-generator. This generator expects a JSON file with our Swagger description, so we need to somehow tease that out of our application. We could probably generate it based on the source code, but we could also do it the ugly way.

To do it the ugly way, we create a unit test that looks like this:

import java.io.File;
import java.nio.charset.Charset;
import javax.ws.rs.core.MediaType;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@ExtendWith(SpringExtension.class)
@SpringBootTest
public class GenerateSwaggerTest {
	@Autowired
	WebApplicationContext context;

	@Test
	public void generateSwagger() throws Exception {
		MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
		mockMvc.perform(MockMvcRequestBuilders.get("/v2/api-docs").accept(MediaType.APPLICATION_JSON))
				.andDo((result) -> {
					FileUtils.writeStringToFile(new File("target/swagger.json"), result.getResponse().getContentAsString(),
							Charset.defaultCharset());
				});
	}
}

This starts up a Spring context, invokes the endpoint serving the generated API documentation, and stores it in a file swagger.json under our build directory. This means that running this test is now essential for our build pipeline, so we configure Maven Surefire to run even in the case that some idiot builds with -DskipTests:

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-surefire-plugin</artifactId>
	<configuration>
		<skipTests>false</skipTests>
	</configuration>
</plugin>

This runs after the resource and test phases, but crucially before the package (and prepare-package) phases in Maven, so this will all be done once the Angular build is triggered.

We now just add swagger-angular-generator to our Angular build in package.json:

    "scripts": {
        "prebuild": "mkdir -p src/api && swagger-angular-generator -s ../../../target/swagger.json -d src/api --no-store -w",

Adding it as a prebuild script ensures the generated code is ready before the Angular build. We pass in the relative path to the generated swagger.json and generate code under the src/api folder in our Angular project. The –no-store skips generating some classes we don’t need and -w also generates an unwrapped method for simplified use when applicable.

Now, our Angular component sam simply save persons using code similar to:

import { Person } from '../../../api/defs/Person';
import { ApiService, PersonParams } from '../../../api/controllers/Api';

export class RegisterComponent {
    onSubmit() {
        const value = this.form.value as PersonParams;
        this.api.person(value)
            .subscribe((person: Person) => this.handleSuccess(person), error => this.handleError(error));
    }
}

This will work fine when running the actual application, but prevents us from testing using the development server, so we create a simple HttpInterceptor rewriting API URLs:

import { Injectable, Inject } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class ApiInterceptorInterceptor implements HttpInterceptor {

    constructor(
        @Inject('BASE_API_URL') private baseUrl: string) { }

    intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<any>> {
        const apiReq = (request.url.startsWith('http://') || request.url.startsWith('https://')) ? request :
            request.clone({
                url: `${this.baseUrl}/${request.url}`
                    .replace(/\/\/*/g, '/').replace(/^https:\//, 'https://').replace(/^http:\//, 'http://')
            });
        return next.handle(apiReq);
    }
}

This interceptor can conveniently be put in our shared module and loaded there or loaded directly in our application. In any case, it needs an application URL provided. We do that in our mail application module:

@NgModule({
    providers: [
        {
            provide: HTTP_INTERCEPTORS,
            useClass: ApiInterceptorInterceptor,
            multi: true,
        },
        { provide: 'BASE_API_URL', useValue: environment.base_url },
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }

For simplicity, we have used a constant declared in environments.ts and environments.prod.ts to bind to a separate Java applation running locally in the default configuration, allowing starting the Java application and connecting to it using the Angular development server (ng serve):

export const environment = {
    production: false,
    base_url: 'http://localhost:8480/application-root',
};

Or we can connect to the API endpoint being served by the same Spring Boot application serving our API in a fully packaged production configuration:

export const environment = {
    production: true,
    base_url: '/application-root',
};

And now, we can control the API in our entire application by simply changing the Java DTO classes, and using a simple “mvn package” everything is packaged up in a single executable WAR file serving both the REST endpoints and the Angular application presenting them.

Protecting Web-services with Captchas

With Angular, everything is publicly available, so we want to employ protection in our web-services to avoid abuse. For some applications, this entails protecting functionality behind logins and/or throttling, but that is not the topic for here. Instead, we deal with protecting services using a captcha.

Captchas are a way to generate images that only a computer can read, but pretending it requires a human. There are services that can do that on the internet, or you can generate your own. Since we are not insane, we just use an off-the-shelf component, in this case Google’s Recaptcha service.

Using this, just entails registering to get an site key and secret. You then either copy-paste the code you get from Google or use an off-the-shelf Angular component to display it. We do the latter:

            <p-captcha #captcha theme="light" size="normal" siteKey="SITE KEY HERE"
                language="en" (onResponse)="showResponse($event)" (onExpire)="showResponse(null)"></p-captcha>

Our showResponse function extracts a token from the response and includes it as part of our form:

    showResponse(response) {
        this.form.get('token').setValue(response?.response);
    }

On the Java side, we implement a simple service that allows us to validate the token:

import java.net.URI;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
public class RecaptchaService {
	private static final URI URL = URI.create("https://www.google.com/recaptcha/api/siteverify");

	@Value("${eu.group.application-name.recaptcha.secret}")
	private String secret;
	
	@Autowired
	private RestTemplate template;
	
	public boolean validateToken(String token) {
		log.trace("Token: {}", token);
        MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<>();
        parameters.add("secret", secret);
        parameters.add("response", token);
        RecaptchaResponse result = template.postForEntity(URL, parameters, RecaptchaResponse.class).getBody();
		log.trace("Response: {}", result);
		if (!result.isSuccess()) {
			log.info("Failed captcha validation {} with {}", token, result);
		}
		return result.isSuccess();
	}
}

This makes use of a simple DTO:

import java.time.ZonedDateTime;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.SuperBuilder;

@SuperBuilder(toBuilder = true)
@Getter
@Setter
@NoArgsConstructor
@ToString
public class RecaptchaResponse {
	private boolean success;
	@JsonProperty("challenge_ts")
	private ZonedDateTime timestamp;
	private String hostname;
	@JsonProperty("error-codes")
	private List<String> errors;
}

Now, our Java code can call our service method (lines 33-36 in the controller above) to ensure that requests have been initiated by our Angular application (or by somebody who scraped our site key and managed to trick Recaptcha into providing them with tokens. Tokens are single use too, so this provides reasonable protection against abuse.

Enable Deep Linking

Angular uses the concept of routers to support multiple pages. A router essentially maps a URL to an Angular component for display. When using the development node server, you can also directly enter a URL and it will get routed to the appropriate page. When serving using the Java server, this does not work, however, as Java does not know how to convert from the entered URL to the single HTML page responsible for serving the entire Angular application.

To do that, we make use of the Java UrlRewriteFilter and just rewrite any URL (or prefix) that contains pages a user may link to to the home page. We add the module to our POM:

	<dependencies>
		<dependency>
			<groupId>org.tuckey</groupId>
			<artifactId>urlrewritefilter</artifactId>
			<version>4.0.4</version>
		</dependency>

We add a configuration to our Spring Boot 2.2 application to enable the filter:

import java.io.IOException;

import javax.servlet.FilterConfig;
import javax.servlet.ServletException;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.tuckey.web.filters.urlrewrite.Conf;
import org.tuckey.web.filters.urlrewrite.UrlRewriteFilter;

@Component
public class RewriteConfig extends UrlRewriteFilter {
    private static final String CONFIG_LOCATION = "classpath:/urlrewrite.xml";

    @Value(CONFIG_LOCATION)
    private Resource resource;

    @Override
    protected void loadUrlRewriter(FilterConfig filterConfig) throws ServletException {
        try {
            Conf conf = new Conf(filterConfig.getServletContext(), resource.getInputStream(), resource.getFilename(), "");
        checkConf(conf);
        } catch (IOException ex) {
            throw new ServletException("Unable to load URL-rewrite configuration file from " + CONFIG_LOCATION, ex);
        }
    }
}

The code assumes the existence of an urlrewrite.xml file on the classpath. This file describes the redirects we perform:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE urlrewrite
    PUBLIC "-//tuckey.org//DTD UrlRewrite 4.0//EN"
    "http://www.tuckey.org/res/dtds/urlrewrite4.0.dtd">
<urlrewrite>
	<rule>
		<from>^/welcome/?$</from>
		<to>/</to>
	</rule>
</urlrewrite>

This rule just redirects all URLs /welcome or /welcome/ to /. We can also redurect everything under a path or add more redirect rules. See the documentation for an explanation of the file format. Used like this, the user can navigate to <application base>/welcome, Java will rewrite the URL to <application base>/ and find the Angular application, which will properly route it to the component set up as an Angular route:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { RegisterComponent } from './components/register/register.component';
import { WelcomeComponent } from './components/welcome/welcome.component';

const routes: Routes = [
    { path: '', component: RegisterComponent },
    { path: 'welcome', component: WelcomeComponent },
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }

Concluding Remarks

That about sums up this part. I have a hunch there will be a part 3 upcoming as well, dealing with securing an application using OAuth (using Keycloak) in the Angular frontend and using bearer tokens in the web-services. There may also be, either as a separate part or in the same part, be things about the widget framework (PrimeNG) we use and how to set up reactive Angular forms using custom components.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.