Java 8 Spring Cloud Scalable Microservice Demo on Github

As mentioned on the homepage, whilst I am a full stack developer overall, I am currently doing more work on the server side area – especially with building more diverse, cloud-oriented applications.; mainly using Java 8 and Spring Cloud. Spring Cloud is a collection of tools, mainly of whom are powered by Netflix technologies such as:

  • Eureka – a service discovery and registry technology, and individual microservices register with Eureka. When one microservice wants to ‘speak’ to another microservice, they check with Eureka for a ‘list’ of other microservices.
  • Feign client – a ‘web service client’, which is the technology we use in a Spring Cloud environment for two microservices to speak to each other.
  • Ribbon – the underlying load-balancing technology used by Feign (and Zuul, a reverse proxy – also by Netflix) to ensure that each available type of microservice is ‘spoken to’ in a balanced way.
View on GitHub
View on GitHub

Update Thanks to Josh Long for mentioning this demo in Spring’s “This week in Spring” roundups, here and here.

Why I Made This Microservice Demo

These Netflix technologies have thankfully been open sourced, and Spring have then wrapped them so that they’re readily available to use in the ‘standard Spring way’ of doing things! So essentially you get the benefits of Spring Boot (such as embedded application servers, and ease of getting up and running), but in a way that can be deployed to ‘the cloud’ fairly easily.

Whilst this all sounds great, getting this all up and running can be a bit tricky to begin with. I think this is partly because getting a set of cloud based, scalable microservices all configured and speaking to one-another will never be plain-sailing (since it’s not a straightforward concept). But also there’s such a diverse set of Spring projects (even within the Spring Cloud umbrella), all of whom are constantly updated, that sometimes the documentation out there doesn’t always ‘work’ as you’d expect.

Hence I have put together a quick set of four Spring Cloud microservices as a demo available on Github, which provide the basics needed to go forward and build your own set of Spring Cloud based, scalable microservices. These four microservices are:

  • eureka-server – a bare-bones Spring Boot application, running on port 8001, which the other 3 microservices register with.
  • jwt-auth-server – a Spring Security OAuth2 (single-sign-on) service, running on port 8002, which uses JWT (JSON Web Tokens) as the SSO implementation. JWT is stateless and is underpinned by a secret signing key (aka to prevent someone generating their own ‘session’ with their own data).
  • business-logic-api – a Spring Boot application, on port 8003, which in OAuth2 terms would be a ‘resource server’. In other words, this is a generally ‘authenticated’ microservice (it requires a valid JWT token to use most of its endpoints). It’s worth noting that I call this an “api” not a “server” since this is where various ‘resource server’ type API endpoints will live.
  • reports-api – ditto to business-logic-api, albeit on port 8004!

I have intentionally used two resource servers in this demo, because I wanted to give a simple example of Feign client. This is used in the business-logic-api’s FizzBuzzController which does a sideways call to an endpoint within reports-api to get some data.

This is to start showing the power of Spring Cloud and the microservice approach, since in production we might have 10 reports-api microservice instances. So when business-logic-api uses Feign client to ‘speak to’ reports-api, Feign uses Eureka (and then Ribbon) to choose which reports-api to actually contact for its request.

And since, as I mentioned earlier, JWT is stateless, we can also scale jwt-auth-server too since we don’t have to worry about sticky sessions. Eureka can naturally be scaled as well.

How it Works

All four microservices have a YAML config file at src/main/resources/application.yml which sets up the basic configuration of each microservice. All 4 are also a parent of the main pom.xml which uses Spring Boot V1.5.9 and Spring Cloud Edgware, and ultimately Spring Starter poms which automatically pull in the most useful set of default dependencies. The only difference I went with is using Undertow instead of Tomcat for the embedded web server, since it is broadly the most efficient right now. Using Undertow instead of Tomcat is as easy as excluding Tomcat and including Undertow via Spring starters, in the pom.xml.

Eureka Server

This is the easiest one to explain, it literally just pulls in a new dependency:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-netflix-eureka-server</artifactId>
    </dependency>
</dependencies>

And then uses a single annotation to say that this is the Eureka server:

@EnableEurekaServer

We then naturally stop this microservice from registering with Eureka (in the YAML config).

JWT Auth Server

This was ‘fun’ to setup. Whilst Spring Boot and Cloud offers many great features, it can sometimes be quite fiddly to setup some of their non-mainstream projects. Whilst (in Spring Boot) you can just define a spring.datasource.url property and Spring will configure the data source for you right then and there, we aren’t so lucky in Spring Cloud. We can’t just say @EnableAuthorisationServer and see a basic OAuth2 implementation kick in. Nor can we say @EnableJWT (a made up annotation) either. Instead we’re left with a decent amount of boilerplate configuration, which mainly resides in OAuth2JwtConfig and WebSecurityConfig.

Anywhoo, before I explain some of the more useful parts of these 2 classes, it’s worth noting that the YAML config points back at our Eureka Server to register with. At this point, this isn’t needed since we don’t have a load-balanced technology (via Feign or Zuul, for example) connecting to this auth server. But it’s worth having since you probably won’t be exposing your JWT Auth Server directly to internet traffic, so a wrapper (such as a Zuul proxy as part of another microservice) could make sense to use.

OAuth2JwtConfig

@Value("${application.jwtSigningKey}")
private String jwtSigningKey;

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    Assert.notNull(jwtSigningKey, "No JWT signing key present, check config params for application.jwtSigningKey");

    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setSigningKey(jwtSigningKey);
    return converter;
}

The above code pulls in the signing key from our configuration (in this case YAML, but this might be environment variables or Spring Cloud Config) and sets up the signer for our JWT token.

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()
            .withClient("default-client")
            .secret("sssshhh")
            .authorizedGrantTypes("password")
            .authorities("ROLE_ADMIN", "USER")
            .scopes("read", "write", "report")
            .resourceIds("default-resources")
            .accessTokenValiditySeconds(99999);
}

This configures a single API client which allows password based authentication, and gives the consuming endpoints read, write and report scopes. We will use these later in some endpoints, so that (e.g.) only people with a ‘read’ scope can use a particular endpoint. If we then had another client that only offered a ‘report’ scope, they wouldn’t be able to use the aforementioned endpoint.

WebSecurityConfig

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication() // creating user in memory
            .withUser("user")
            .password("password").roles("USER")
            .and().withUser("admin")
            .password("password").authorities("ROLE_ADMIN");
}

This then sets in a simple in-memory list of users who can authenticate with their API (using their username, password combination). Note that this is different to the client information above:

  • user is as it sounds, aka someone who uses a username and password to logon to a service such as a website, or in this case a SSO OAuth2 system.
  • client is the overall ‘group’ or type that can then use that SSO system.

As we will see later, we send up the username and password of the user in the body of our REST calls, but we use the client’s name and secret key in the Authorization header of our REST calls.

Business Logic API

This uses standard Spring MVC in FizzBuzzController to ‘get’ data, and return it as part of a REST endpoint. The only real difference is that it uses PreAuthorize to ensure that the user is authenticated (for 2 of its endpoints), and one endpoint checks that the user has the relevant scope (‘permission’) to read its data.

The main work occurs in ResourceServerConfig however, where we say that this is a resource server via:

@EnableResourceServer

In order to be able to use the Business Logic API’s authenticated endpoints, we need a way of knowing if the stateless JWT token is valid or not. The mechanism JWT uses for this is signing it to ensure that what we have received is valid. We can do this in two ways:

  • Use security.oauth2.resource.user-info-uri to specify an endpoint within JWT Auth Server, and this endpoint then checks if the JWT token is valid.
  • Specify our own JWT signing information within the microservice.

Technically the first option is more complaint with the 12 Factors since the JWT server should probably be responsible for checking the validity of JWT tokens received, however the downside is that each and every authenticated API call to business logic API and reports API will then do a sideways call over to the JWT auth server.

You could also argue that a resource server should be able to self-determine whether a JWT token is valid, and since it’s also more efficient (since there’s no sideways call to other microservices), I went down this approach:

@Value("${security.oauth2.resource.jwt.keyValue}")
private String signingKey;

@Override
public void configure(ResourceServerSecurityConfigurer config) {
    config.resourceId("default-resources"); // this matches the resourceId in OAuth2JwtConfig
    config.tokenServices(this.getTokenService());
}

@Bean
public TokenStore jwtTokenStore() {
    return new JwtTokenStore(this.jwtTokenConverter());
}

@Bean
// Get this resource server to verify its own JWT token, instead of passing the request to the jwt-server via security.oauth2.resource.userInfoUri
public JwtAccessTokenConverter jwtTokenConverter() {
    final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setSigningKey(signingKey);
    return converter;
}

@Bean
@Primary
public DefaultTokenServices getTokenService() {
    final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
    defaultTokenServices.setTokenStore(this.jwtTokenStore());
    return defaultTokenServices;
}

You will also notice in FizzBuzzController that two methods have a @PreAuthorize annotation on them. This annotation is activated with the following config:

@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
// 'Activate' the @PreAuthorize annotations we use to only allow access to controller methods based on roles/scopes (etc) in the oauth2 token
public static class GlobalSecurityConfiguration extends GlobalMethodSecurityConfiguration {
    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return new OAuth2MethodSecurityExpressionHandler();
    }
}

This controller also has a /public endpoint with no @PreAuthorize annotation. We allow this through the Spring Security config via:

@Override
public void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().antMatchers("/public/**").permitAll();
}

Two Microservices ‘Speaking’ to Each-Other via Feign

The final thing of note in the business-logic-api is that we get some data from reports-api (currently in a contrived manner). As mentioned earlier, this is to demonstrate the potential power of this cloud-based microservice approach. We do this by having a ReportsApiClient Feign client interface whose endpoint configuration matches the one in reports-api’s FizzReportController. We then autowite this client into our controller, and call it as we would with any other Spring-wired resource.

‘Under the hood’, this is contacting Eureka Server to get a list of possible reports-api microservices that it could send this request onto. Ribbon is then used as a load balancer to choose which microservice to contact (by default in a round-robin manner). The FizzReportController then picks up this request, returns its response which is then available in business-logic-api again.

There’s nothing further to talk about in reports-api since it’s just another resource server, and its config was discussed above.

REST Calls to Use This Demo

If you use Postman, you can import the environment and collection from the /postman-collection/ folder which will get you up and running with the below a lot quicker – especially the authenticated calls, since the JWT bearer token from the authenticate call is saved as a variable and is then automatially sent up in subsequent requests.

Now that I’ve covered how these four microservices are configured and work together, we’ll now look at how to actually use them. Thankfully this is fairly straightforward.

To Authenticate (and thus get a JWT bearer token)

The authentication endpoint requires an Authorization: Basic … header to be sent up, where the value is the base64 encoded version of the clientId and secretKey, aka:

base64(default-client:sssshhh) = ZGVmYXVsdC1jbGllbnQ6c3Nzc2hoaA==

So our API request is:

HTTP POST http://localhost:8002/oauth/token

Headers

  • Authorization: Basic ZGVmYXVsdC1jbGllbnQ6c3Nzc2hoaA==
  • Content-Type: application/x-www-form-urlencoded

Body

  • client_id: default-client
  • username: user
  • password: password
  • grant_type: password

This will return various data, including our bearer token (also known as the access token). You can decode this on jwt.io to see its contents, which includes the username and scopes of this authentication.

We can then use any of our authenticated endpoints simply by passing a header of:

Authorization: Bearer [bearer-token-here]

These endpoints are currently:

  • HTTP GET http://localhost:8003/fizz-buzz
  • HTTP GET http://localhost:8003/fizz/report/1
  • HTTP GET http://localhost:8004/fizz/collate

We can also get basic data from our public (unauthenticated) endpoint via HTTP GET http://localhost:8003/public/fizz-buzz (this endpoint naturally doesn’t require an Authorization header).

Issues Faced

Overall the potential of what is available in this demo is fairly great, since you can easily scale up all the microservices and get a potentially powerful application up and running quickly from this point.

However getting to this point can be a bit fiddly when starting from scratch (hence this demo!). I had two main issues when creating this demo.

Firstly, I wanted this demo in Spring Boot 2 hence I used the POM versions from the Spring Cloud projects website:

Spring version from their website
Spring version from their website

However this later resulted in a runtime ClassNotFoundException since it appears the spring-security-jwt project hasn’t caught up (to Spring Boot 2 I believe) yet:

Full Compile Error
Full Compile Error

Hence I ended up reverting to Spring Boot 1.5.9 and Spring Cloud Edgware which resolved the compile error. This is understandable with the sheer number of projects that Spring offers, but I’ve seen this a couple of times recently which can be a bit of a pain.

Secondly, as I covered a bit earlier, there is quite a lot of boilerplate configuration needed to get to this point. And whilst I can understand this also, sometimes this feels a bit incongruent with the aim of Spring Boot to auto-configure everything smartly based on config params and/or annotations.

Overall though, I think that what Spring offers with Spring Cloud (with natural thanks to Netflix) is very powerful and has a lot of potential.

I hope you have found this useful. Please feel free to get in touch if you have any questions or comments. Thanks!

Leave a Comment