Sunday, February 16. 2020
Testing Spring Boot (Part I)
Today's entry is about Spring Boot. If you follow the blog you know that I am more engaged to JavaEE, but I wanted to test these new technologies and I decided to start with Spring Boot (maybe the most known framework of this type). I prefer standards (JavaEE) over other technologies (Spring) just because I think this is the way that things should be, but better if I start working with them. In my humble opinion the idea around Spring Boot is nice, particularly how the application is bundle in a single jar with all the needed components in it, really perfect to deploy in a container. So my idea with this series is the following:
Testing Spring Boot but try to limit its functionality to JavaEE specification (mainly servlets and jax-rs, which are the typical frameworks I use to create backend applications). I am going to restrict Spring usage to the minimum.
Including swagger and keycloak as external libraries (other libraries commonly used in my projects).
The app should work in the common Spring Boot configuration (Tomcat and Jersey) and using JBoss implementations (undertow and resteasy).
The app should work like standalone jar executable and container war bundle (inside Wildfly).
The goal is taking advantage of the Spring Boot packaging but developing with common JavaEE technologies. Trying to obtain the best of the two worlds. The last points are just to see how easy is combining those different scenarios (jar and war packaging, changing implementations,...). Please remember this is just my idea, and, of course, there will be other people that prefer the opposite solution (use the Spring framework exclusively).
Adding a Servlet
Let's start with a simple Servlet. Using jax-rs is the final goal, but I prefer to start with the basics. So using the Spring initializer a initial pom.xml was created.
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<relativePath/>
</parent>
<groupId>es.rickyepoderi.springboot</groupId>
<artifactId>springboot-sample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-sample</name>
<packaging>jar</packaging>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
And then a simple Hello World servlet is created (The HelloBean is just a POJO used to say hello, it has the greeting and the name properties).
package es.rickyepoderi.springboot.servlet;
import es.rickyepoderi.springboot.HelloBean;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/plain;charset=UTF-8");
try (PrintWriter writer = resp.getWriter()) {
writer.write(new HelloBean(req.getParameter("greeting"), req.getParameter("name")).toString());
}
}
}
In order to make it work with Spring the application is created with an annotation @ServletComponentScan defining the servlets (and other entities of the specification like filters or listeners) that should be initialized.
package es.rickyepoderi.springboot;
import es.rickyepoderi.springboot.servlet.HelloServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication
@ServletComponentScan(basePackageClasses = {HelloServlet.class})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
And it works. You can compile it and make it run with two simple commands:
mvn clean package
java -jar target/springboot-sample-0.0.1-SNAPSHOT.jar
And the Servlet is displayed in the http://localhost:8080/hello URL.
Adding Jax-RS
Now that the simple servlet is working let's move and add the restful web services. The idea is using plain jax-rs, so initially the jersey (jakarta/Oracle implementation) starter is incorporated to the pom.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</artifactId>
</dependency>
The first little drawback was that jersey integration is done using the ResourceConfig class (private class in the Jersey implementation) instead of the standard Application class. This will complicate the change to resteasy unnecessarily. But, it is really easy to use it. The REST application should also be tagged as a @Component to initialize it inside Spring.
package es.rickyepoderi.springboot.rest;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Context;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;
@ApplicationPath("api")
@Component
public class RestApplication extends ResourceConfig {
public RestApplication() {
register(HelloEndpoint.class);
}
}
And finally the REST endpoint is developed. Another Hello World example.
package es.rickyepoderi.springboot.rest;
import es.rickyepoderi.springboot.HelloBean;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
@Path("/hello")
public class HelloEndpoint {
@GET
@Produces("application/json")
public HelloBean sayHello(@QueryParam("greeting") String greeting, @QueryParam("name") String name) {
return new HelloBean(greeting, name);
}
}
And again it works. Just compile and run the application and the rest endpoint is available in http://localhost:8080/api/hello.
Adding Swagger
Now it is time to add swagger to annotate my endpoint. First add the dependency.
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-jaxrs2</artifactId>
<version>2.1.1</version>
</dependency>
The framework will be added like in a common web application, so the previous RestApplication registers the swagger OpenApiResource and initialize the API to include the packages that should be scanned for endpoints.
public RestApplication() {
register(HelloEndpoint.class);
register(OpenApiResource.class);
try {
new JaxrsOpenApiContextBuilder()
.servletConfig(servletConfig)
.application(this)
.openApiConfiguration(new SwaggerConfiguration()
.openAPI(new OpenAPI()
.info(new Info()
.title("Hello App")
.description("Sample Spring Boot Hello App")
.version("0.0.1-SNAPSHOT")))
.prettyPrint(true)
.resourcePackages(Collections.singleton(HelloEndpoint.class.getPackage().getName())))
.buildContext(true);
} catch (OpenApiConfigurationException e) {
throw new RuntimeException(e);
}
}
And now it is turn to annotate all the endpoints and methods. In my case just the sayHello operation.
@GET
@Produces("application/json")
@Operation(summary = "Say Hello",
tags = {"hello"},
description = "Say hello to the given username and greeting",
responses = {
@ApiResponse(description = "The hello bean", content = @Content(schema = @Schema(implementation = HelloBean.class)))
})
public HelloBean sayHello(
@Parameter(description = "The user name") @QueryParam("greeting") String greeting,
@Parameter(description = "The greeting to use") @QueryParam("name") String name) {
return new HelloBean(greeting, name);
}
Finally the swagger-ui.html is added and (I suspect that) it is done in an unusual way for Spring Boot, the needed files were just included in the webapp directory (I just obtained the dist directory from the project and installed in the app directory, the index.html was modified to obtain the hello/openapi.json definition and renamed to swagger-ui.html). And again, everything continues working correctly.
Adding keycloak
The next step is adding keycloak to the restful endpoint. The servlet-filter adapter is going to be used, although there is a Spring and a Spring Boot adapter, the filter adapter is standard and it also works in common web applications. First add the dependency.
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-servlet-filter-adapter</artifactId>
<version>8.0.2</version>
</dependency>
Then a new filter was created extending the default servlet filter.
package es.rickyepoderi.springboot.keycloak;
import javax.servlet.annotation.WebFilter;
import org.keycloak.adapters.servlet.KeycloakOIDCFilter;
@WebFilter(urlPatterns = {"/api/hello", "/keycloak/*"})
public class KeycloakFilter extends KeycloakOIDCFilter {
public KeycloakFilter() {
super(new ClasspathKeycloakConfigResolver());
}
}
The filter is applied to the /api/hello endpoint and the internal /keycloak/* and a configuration resolver is used to obtain the keycloak.json from the classpath.
package es.rickyepoderi.springboot.keycloak;
import java.io.InputStream;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.spi.HttpFacade;
public class ClasspathKeycloakConfigResolver implements KeycloakConfigResolver {
private final KeycloakDeployment deployment;
public ClasspathKeycloakConfigResolver() {
InputStream is = getClass().getResourceAsStream("/keycloak.json");
deployment = KeycloakDeploymentBuilder.build(is);
}
@Override
public KeycloakDeployment resolve(HttpFacade.Request req) {
return deployment;
}
}
And, as it is a common filter, it should also be added to the @ServletComponentScan annotation to be integrated as another servlet class.
@SpringBootApplication
@ServletComponentScan(basePackageClasses = {HelloServlet.class, KeycloakFilter.class})
public class Application {
Finally a index.html was added to be used as a public application with keycloak (I did the trick of using the same client as the public and the bearer-only application, ideally there should be two applications, but the same can be used). And again everything works.
RBAC security
The last thing I wanted to add is some security based on roles. Usually in JavaEE the restful endpoints are annotated as a @Stateless local EJB and RBAC security is easily added to them (@PermitAll, @DenyAll, @RolesAllowed annotations). But enterprise beans is a technology I do not like a lot, the security trick only uses local and stateless and a full JavaeEE application server like wildfly gives that framework out of the box, so it is very easy to use. Nevertheless a similar technique can be achieved using a jax-rs ContainerRequestFilter (a filter that can be added to the rest web services). This entry explains this idea in detail. So the following filter is added.
package es.rickyepoderi.springboot.rest;
import java.io.IOException;
import java.lang.reflect.Method;
import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
public class RolesAuthorizationFilter implements ContainerRequestFilter {
@Context
private ResourceInfo resourceInfo;
@Context
private HttpServletRequest httpServletRequest;
private void notAllowed(ContainerRequestContext ctx) {
ctx.abortWith(Response.status(Response.Status.FORBIDDEN).build());
}
private void checkRoles(ContainerRequestContext ctx, String[] rolesAllowed) {
if (rolesAllowed != null) {
for (String role : rolesAllowed) {
if (httpServletRequest.isUserInRole(role)) {
// user is allowed
return;
}
}
}
// user is not of allowed roles => deny
notAllowed(ctx);
}
@Override
public void filter(ContainerRequestContext ctx) throws IOException {
// look for method/class annotations to allow access
Method method = resourceInfo.getResourceMethod();
Class clazz = method.getDeclaringClass();
if (method.isAnnotationPresent(DenyAll.class)) {
// deny at method level
notAllowed(ctx);
} else if (method.isAnnotationPresent(PermitAll.class)) {
// allowed at method level => ok
} else if (method.isAnnotationPresent(RolesAllowed.class)) {
// check roles allowed
checkRoles(ctx, ((RolesAllowed) method.getAnnotation(RolesAllowed.class)).value());
} else if (clazz.isAnnotationPresent(DenyAll.class)) {
// deny at class level
notAllowed(ctx);
} else if (clazz.isAnnotationPresent(PermitAll.class)) {
// allowed at class level => ok
} else if (clazz.isAnnotationPresent(RolesAllowed.class)) {
// check roles allowed in the class
checkRoles(ctx, ((RolesAllowed) clazz.getAnnotation(RolesAllowed.class)).value());
}
// no annotation is like PermitAll => allowed
}
}
This way the endpoint can be annotated (at class and/or method level) to define what users can execute the endpoints. For example in my little hello example the class is annotated to only be executed by the Users role.
@Path("/hello")
@RolesAllowed("Users")
public class HelloEndpoint {
The RolesAuthorizationFilter should also be registered in the RestApplication to be incorporated to the request execution.
Video
And that is all. The application is a typical rest backend application which is annotated using swagger and secured with keycloak and RBAC. The only difference is that this time the app uses Spring Boot. The video shows first the servlet. Then the endpoint is accessed but it is not allowed because it requires authentication. Finally the index page is requested, the javascript keycloak adapter asks for login, and sending the bearer token the user ricky can execute the method (this user belongs to Users role) but admin receives a forbidden response (roles are working as expected). Finally you can see the swagger-ui which reads OK the defining annotations.
As a little summary, this entry presents a modern JavaEE application (rest web services, swagger, OIDC and RBAC) but using Spring Boot instead of a common container. The application is almost a JavaEE app but deployed with Spring Boot. The maven project can be downloaded from here. In a second entry the application will be moved to JBoss implementations (undertow and resteasy) and finally the application will be deployed inside wildfly (instead the full jar bundle). Let's see how complicated that second part is.
Regards!
Comments