Let’s build an HTTP API. Honestly, this is just a pretense to have something to secure with the Spring Security OAuth support, so we’ll make this quick. First, go to the Spring Initializr, and then add the following dependencies: `
-
Spring for RabbitMQ
-
Spring Integration
-
Spring Web
-
OAuth2 Resource Server
-
Spring Data JDBC
-
PostgreSQL Driver
I gave the newly minted project a group of bootiful
and named it api
.
Click Generate
, unzip the newly minted .zip
file, and then open the project in your IDE.
The domain’s a trivial one: customer data, like a CRM (customer relationship manager). Each Customer
entity has an id
field, a name
field, and an email
.
link:api/src/main/java/bootiful/api/Customer.java[role=include]
And of course there’s a Spring Data JDBC repository to make working with that data easier…
link:api/src/main/java/bootiful/api/CustomerRepository.java[role=include]
The Spring Data JDBC repository talks to a SQL table, called customer
, whose definition we must specify.
link:api/src/main/resources/schema.sql[role=include]
Let’s insert some customer
records, just for good measure, so that we have something to look at, and so that the system is in a well-known state.
link:api/src/main/resources/data.sql[role=include]
And if we write data in a database but have no HTTP controller by which to examine the data, did we actually write any data at all? Philosophers disagree, but just to be safe let’s build an HTTP controller with good ol' Spring MVC.
link:api/src/main/java/bootiful/api/CustomerHttpController.java[role=include]
Remember we’re going to be securing this with Spring Security’s OAuth support, and just to be sure everything’s worked, we’ll have a simple HTTP endpoint that injects and then spits out the current authenticated user’s username, so you can see who the system thinks is authenticated at the moment.
This is particularly useful in the client, which is goign to want to at least acknowldge that the user has successfully authenticated with a message like, Welcome <USER>
, where <USER>
might be jlong
. You might also leak other inforamation to the client, like the first name and last name.
link:api/src/main/java/bootiful/api/MeHttpController.java[role=include]
And, finally, we’ve got a little integration that sends a message using the AMQP protocol via RabbitMQ to another service called processor
.
We’ll introduce that thing later.
The idea is that you’ll be able to click a button to get some sort of email sent to each user. We’re not going to actually send an email.
I haven’t really thought out what sort of email we would send if we were!
Just use your imagination.
The significant bit here is that sending email is one of those things you don’t necessarily want to do in the hot path of handling HTTP requests. It should be farmed out to another node in the system which can be dedicated to scaling up and down as is required to handle the email-sending load, and to scale independently of the front-office HTTP traffic load. Sending email may take a while, be error-prone, etc. It’s the kind of dirty integration stuff that Spring Integration is purpose built for. So we’ll use Spring Integration.
Tip
|
This is what we used to call a backoffice process.
I’m calling it processor , because I’m amazing with names, like everyone on the Spring team is.
We named our MVC framework Spring MVC, our data framework Spring Data, our batch framework Spring Batch… Well, you get the idea.
(Don’t ask about Spring Boot, tho!)
|
Requests originate here, in this controller.
Each request contains a Customer
payload, and header, jwt
, which contains the JWT associated with the current authenticated user.
We’ll use that JWT later to validate the request is from a trusted, authenticated, user.
link:api/src/main/java/bootiful/api/EmailController.java[role=include]
And there’s a bit of Spring Integration plumbing to route those requests to our RabbitMQ broker running in a separate process.
This IntegrationFlow
looks at requests (which come in the shape of a Message<T>
object, which has headers and a payload) coming in from the injected MessageChannel
, transforms them into JSON data, and then sends them, along with a JWT token associated to the current authenticated user, on to the broker, where it’ll eventually get delivered to the consumer, processor
.
link:api/src/main/java/bootiful/api/EmailRequestsIntegrationFlowConfiguration.java[role=include]
-
inbound adapters translate events from the real world into Spring Framework
Message<T>
objects. Outbound adapters translateMessage<T>
objects into events in the real world. This adapter lets us interface with RabbitMQ via the AMQP protocol. -
In this case, messages pass through the
MessageChannel
… -
…and into the next stage in the flow, a transformer, which will translate the
Message<Customer>
into aMessage<String>
, with a JSON payload -
and from there, it gets routed to the outbound AMQP adapter, which will translate the Spring Framework
Message<String>
into a request sent over AMQP to the RabbitMQ broker
We’re using Spring Security’s Resource Server support to protect requests to the API, rejecting requests that don’t have a valid OAuth 2 token. It does this by connecting to the OAuth 2 IDP (our amazing Spring Authorization Server instance) and validating the JWT.
The Spring Security Resource Server support, the Spring Data support, the Spring Integration AMQP support, all of it requires configuration, which brings us to our application.properties
.
link:api/src/main/resources/application.properties[role=include]
-
the issuer URI is the address of the Spring Authorization Server against which Spring Security can validate a JWT token
-
we need to connect to the RabbitMQ instance..
-
and the PostgresSQL database…
-
the Spring Authorization Server is already running on port
8080
, so we’ll need to run this Java application on port8081
. (Remember that!)
Warning
|
I’ve hardcoded database connectivity credentials and RabbitMQ credentials and so on. It’s worth restating: don’t do this in a production application. Use environment variables or the Spring Cloud Config Server or Spring Cloud Vault or any of the infinitely more secure approaches than hardcoding credentials in plaintext on a public Git repository! |
With all that in place, you should be able to start the application.
It’s got a REST endpoint.
You can try it out by hitting the new /customers
endpoint that we created.
curl http://localhost:8081/customers
But it fails!
Which is good.
It fails because it’s an authenticated request.
We need a valid JWT token.
We can get one because, when we registered the client with the Spring Authorization Server, we listed client_credentials
as an authorized grant type.
This in turn allows us to make a request without a user context.
And if there’s no user, then there’s no need to verify the user, and thus no need for a browser or a web page or anything.
We only need the client ID and client secret: crm
/crm
.
curl -X POST \
-H "Authorization: Basic $(echo -n 'crm:crm' | base64)" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&scope=user.read" \
http://localhost:8080/oauth2/token
That should dump a token in a JSON document. On my machine I got this very long JSON document that I’ve abbreviated for you here.
{"access_token":"eyJraWQiOiI4YzQyNGU...Qy1Fg","scope":"user.read","token_type":"Bearer","expires_in":299}
The important bit is the access_token
attribute.
If you have the jq
command line utility installed, you can extract out the access_token
like this
TOKEN=$( curl -X POST \
-H "Authorization: Basic $(echo -n 'crm:crm' | base64)" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&scope=user.read" \
http://localhost:8080/oauth2/token | jq -r .access_token )
By the way, you could also issue the same request using Spring’s RestTemplate
or WebClient
or RestClient
.
Here’s the equivalent using ye ole RestTemplate
:
link:api/src/main/java/bootiful/api/ClientCredentialsClient.java[role=include]
You can use this token to then issue a request to the HTTP server:
curl -H "Authorization: Bearer $TOKEN" http://localhost:8081/customers
And there’s the data!
At long last, reunited with the data.
Feels good doesn’t it?
We’ve got an HTTP API that is secure and all we had to do was specify the issuer-uri
int he property file.
Our Spring Authorization Server is paying dividends already!
We’ve got honest-to-goodness identity management, security, and more, all for the cost of one lousy little property.
And if we want to protect any other microservices, its the same story.
One little issuer-uri
property, and our services will automatically be protected and automatically be able to work with the identities in the centralized Spring Authorization Server.
I love this for us. We did something amazing here. Do you feel the possibilities? We stood up one little Spring Authorization Server and suddenly every microservice in our system can be protected. No need to redundantly duplicate the requests.
We sort of cheated here, though.
We got a token using the client_credentials
authorization grant type.
No user context, remember?
We want users.
That’s sort of the point of this whole exercise.
Somewhere, somehow, we’ll need to get a user involved.
Once they’re involved, they’ll have a token that we can use to make requests to this resource server.
The thing that originates the token, that forces the redirect to the OAuth IDP where the user will be asked to consent?
That’s called an OAuth 2 client.
An OAuth 2 client is both an OAuth 2 resource server, in that it’ll reject invalid requests, and it has this extra ability to initiate and handle the OAuth flow - the "OAuth dance" - we looked at earlier when we discussed Yelp.com’s authentication flow.
Let’s build one.