Every API developer is looking for ways to manage their application more securely, without sacrificing speed or ease of implementing new features. To that end, we recently updated the core Stormpath product – our REST API – to Spring Boot. Along the way, we utilized a number of critical efficiencies that would be of value to anyone developing an API using Spring Boot.

Many teams find it difficult to manage authentication and access control to their APIs, so we want to share a few architectural principles and tips from our migration to make it easier to manage your Spring Boot API.

Note: Below we use the command line tool httpie (https://github.com/jkbrzt/httpie) to exercise the examples.



1. Use the @RestController Annotation

Using @RestController (instead of simply @Controller ) ensures that you will return a Java Object rather than a reference to an HTML template. Like this:

@RestController public class HelloController { @RequestMapping("/") public String home() { return "hello"; } } 1 2 3 4 5 6 7 8 9 @ RestController public class HelloController { @ RequestMapping ( "/" ) public String home ( ) { return "hello" ; } }

Execute: http -v localhost:8080

HTTP/1.1 200 OK Content-Length: 5 Content-Type: text/plain;charset=UTF-8 Date: Tue, 14 Jun 2016 23:55:16 GMT Server: Apache-Coyote/1.1 hello 1 2 3 4 5 6 7 8 HTTP / 1.1 200 OK Content - Length : 5 Content - Type : text / plain ; charset = UTF - 8 Date : Tue , 14 Jun 2016 23 : 55 : 16 GMT Server : Apache - Coyote / 1.1 hello

2. Take Advantage of Automatic POJO to JSON Conversion

Spring Boot automatically converts your POJOs (plain old Java classes) to JSON for you!

@RestController public class HelloController { @RequestMapping("/") public ApiResponse home() { return new ApiResponse("SUCCESS", "hello"); } } public class ApiResponse { private String status; private String message; public ApiResponse(String status, String message) { this.status = status; this.message = message; } public String getStatus() { return status; } public String getMessage() { return message; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @ RestController public class HelloController { @ RequestMapping ( "/" ) public ApiResponse home ( ) { return new ApiResponse ( "SUCCESS" , "hello" ) ; } } public class ApiResponse { private String status ; private String message ; public ApiResponse ( String status , String message ) { this . status = status ; this . message = message ; } public String getStatus ( ) { return status ; } public String getMessage ( ) { return message ; } }

Execute: http -v localhost:8080

HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Date: Tue, 14 Jun 2016 23:54:19 GMT Server: Apache-Coyote/1.1 Transfer-Encoding: chunked { "message": "hello", "status": "SUCCESS" } 1 2 3 4 5 6 7 8 9 10 11 HTTP / 1.1 200 OK Content - Type : application / json ; charset = UTF - 8 Date : Tue , 14 Jun 2016 23 : 54 : 19 GMT Server : Apache - Coyote / 1.1 Transfer - Encoding : chunked { "message" : "hello" , "status" : "SUCCESS" }

3. Use Dependency Injection With Autowired Services

Autowiring services enables abstracting out business logic without having complex setup, configuration, or instantiation of Java Objects.

@Service public class HelloService { public String getGreeting(HttpServletRequest req) { String greeting = "World"; Account account = AccountResolver.INSTANCE.getAccount(req); if (account != null) { greeting = account.getGivenName(); } return greeting; } } @RestController public class HelloController { private HelloService helloService; @Autowired public HelloController(HelloService helloService) { Assert.notNull(helloService, "helloService must not be null!"); this.helloService = helloService; } @RequestMapping("/") public ApiResponse home(HttpServletRequest req) { String greeting = helloService.getGreeting(req); return new ApiResponse("SUCCESS", "Hello " + greeting); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @ Service public class HelloService { public String getGreeting ( HttpServletRequest req ) { String greeting = "World" ; Account account = AccountResolver . INSTANCE . getAccount ( req ) ; if ( account ! = null ) { greeting = account . getGivenName ( ) ; } return greeting ; } } @ RestController public class HelloController { private HelloService helloService ; @ Autowired public HelloController ( HelloService helloService ) { Assert . notNull ( helloService , "helloService must not be null!" ) ; this . helloService = helloService ; } @ RequestMapping ( "/" ) public ApiResponse home ( HttpServletRequest req ) { String greeting = helloService . getGreeting ( req ) ; return new ApiResponse ( "SUCCESS" , "Hello " + greeting ) ; } }

NOTE: I recently refactored the above code from using field level injection to using constructor injection. From the perspective of Spring dependency injection, the result is the same. Even one of the Spring engineers advises against field level dependency injection. TL;DR: While slightly more verbose, constructor injection is safer and more easily tested than field level injection.

This example uses Stormpath to return a personalized greeting once you are authenticated. To exercise this you’ll first need to setup a Stormpath account as outlined here. If you followed the instructions and put your Stormpath API Key file in the standard location (~/.stormpath/apiKey.properties) there’s nothing else to do!

Fire up the app and execute this: http -v localhost:8080

HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Date: Wed, 15 Jun 2016 00:56:46 GMT Server: Apache-Coyote/1.1 Transfer-Encoding: chunked { "message": "Hello World", "status": "SUCCESS" } 1 2 3 4 5 6 7 8 9 10 11 HTTP / 1.1 200 OK Content - Type : application / json ; charset = UTF - 8 Date : Wed , 15 Jun 2016 00 : 56 : 46 GMT Server : Apache - Coyote / 1.1 Transfer - Encoding : chunked { "message" : "Hello World" , "status" : "SUCCESS" }

Next, we need to authenticate so we can move forward with our example, so we’ll exercise Stormpath’s built in OAuth 2.0 functionality to authenticate and get back a personalized message. Make sure you’ve created a user for your Stormpath application in the Admin Console. For more information on Stormpath’s OAuth support in the Java SDK and its integrations, check out our Java Product Documentation.

http -v -f POST localhost:8080/oauth/token \ Origin:http://localhost:8080 \ grant_type=password \ username=<email address of the user you setup> \ password=<password of the user you setup> 1 2 3 4 5 6 http - v - f POST localhost : 8080 / oauth / token \ Origin : http : //localhost:8080 \ grant_type = password \ username = < email address of the user you setup > \ password = < password of the user you setup >

Response:

HTTP/1.1 200 OK Cache-Control: no-store Content-Length: 938 Content-Type: application/json;charset=UTF-8 Date: Wed, 15 Jun 2016 00:59:43 GMT Pragma: no-cache Server: Apache-Coyote/1.1 { "access_token": "eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwic3R0IjoiYWNjZXNzIiwiYWxnIjoiSFMyNTYifQ.eyJqdGkiOiIzVFhQZ01Ld0NiQTk1VEp6VzBXTzRWIiwiaWF0IjoxNDY1OTUyMzgzLCJpc3MiOiJodHRwczovL2FwaS5zdG9ybXBhdGguY29tL3YxL2FwcGxpY2F0aW9ucy82dkZUNEFSZldDbXVIVlY4Vmt0alRvIiwic3ViIjoiaHR0cHM6Ly9hcGkuc3Rvcm1wYXRoLmNvbS92MS9hY2NvdW50cy8zcVlHbUl6VWh4UEtZTzI4a04wSWJSIiwiZXhwIjoxNDY1OTU1OTgzLCJydGkiOiIzVFhQZ0owckkwckFTZUU4SmtmN1NSIn0.o_pIHZVDZWogNuhJN2dmG4UKxACoWFxpRpp5OCyh6C4", "expires_in": 3600, "refresh_token": "eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwic3R0IjoicmVmcmVzaCIsImFsZyI6IkhTMjU2In0.eyJqdGkiOiIzVFhQZ0owckkwckFTZUU4SmtmN1NSIiwiaWF0IjoxNDY1OTUyMzgzLCJpc3MiOiJodHRwczovL2FwaS5zdG9ybXBhdGguY29tL3YxL2FwcGxpY2F0aW9ucy82dkZUNEFSZldDbXVIVlY4Vmt0alRvIiwic3ViIjoiaHR0cHM6Ly9hcGkuc3Rvcm1wYXRoLmNvbS92MS9hY2NvdW50cy8zcVlHbUl6VWh4UEtZTzI4a04wSWJSIiwiZXhwIjoxNDcxMTM2MzgzfQ.mJBfCgv4Sdnw7Ubzup7CZ1xdAIC9iO31AJE3NMmp05E", "token_type": "Bearer" } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 HTTP / 1.1 200 OK Cache - Control : no - store Content - Length : 938 Content - Type : application / json ; charset = UTF - 8 Date : Wed , 15 Jun 2016 00 : 59 : 43 GMT Pragma : no - cache Server : Apache - Coyote / 1.1 { "access_token" : "eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwic3R0IjoiYWNjZXNzIiwiYWxnIjoiSFMyNTYifQ.eyJqdGkiOiIzVFhQZ01Ld0NiQTk1VEp6VzBXTzRWIiwiaWF0IjoxNDY1OTUyMzgzLCJpc3MiOiJodHRwczovL2FwaS5zdG9ybXBhdGguY29tL3YxL2FwcGxpY2F0aW9ucy82dkZUNEFSZldDbXVIVlY4Vmt0alRvIiwic3ViIjoiaHR0cHM6Ly9hcGkuc3Rvcm1wYXRoLmNvbS92MS9hY2NvdW50cy8zcVlHbUl6VWh4UEtZTzI4a04wSWJSIiwiZXhwIjoxNDY1OTU1OTgzLCJydGkiOiIzVFhQZ0owckkwckFTZUU4SmtmN1NSIn0.o_pIHZVDZWogNuhJN2dmG4UKxACoWFxpRpp5OCyh6C4" , "expires_in" : 3600 , "refresh_token" : "eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwic3R0IjoicmVmcmVzaCIsImFsZyI6IkhTMjU2In0.eyJqdGkiOiIzVFhQZ0owckkwckFTZUU4SmtmN1NSIiwiaWF0IjoxNDY1OTUyMzgzLCJpc3MiOiJodHRwczovL2FwaS5zdG9ybXBhdGguY29tL3YxL2FwcGxpY2F0aW9ucy82dkZUNEFSZldDbXVIVlY4Vmt0alRvIiwic3ViIjoiaHR0cHM6Ly9hcGkuc3Rvcm1wYXRoLmNvbS92MS9hY2NvdW50cy8zcVlHbUl6VWh4UEtZTzI4a04wSWJSIiwiZXhwIjoxNDcxMTM2MzgzfQ.mJBfCgv4Sdnw7Ubzup7CZ1xdAIC9iO31AJE3NMmp05E" , "token_type" : "Bearer" }

Once that’s done, save the Access Token for use with our application:

ACCESS_TOKEN=eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwic3R0IjoiYWNjZXNzIiwiYWxnIjoiSFMyNTYifQ.eyJqdGkiOiIzVFhQZ01Ld0NiQTk1VEp6VzBXTzRWIiwiaWF0IjoxNDY1OTUyMzgzLCJpc3MiOiJodHRwczovL2FwaS5zdG9ybXBhdGguY29tL3YxL2FwcGxpY2F0aW9ucy82dkZUNEFSZldDbXVIVlY4Vmt0alRvIiwic3ViIjoiaHR0cHM6Ly9hcGkuc3Rvcm1wYXRoLmNvbS92MS9hY2NvdW50cy8zcVlHbUl6VWh4UEtZTzI4a04wSWJSIiwiZXhwIjoxNDY1OTU1OTgzLCJydGkiOiIzVFhQZ0owckkwckFTZUU4SmtmN1NSIn0.o_pIHZVDZWogNuhJN2dmG4UKxACoWFxpRpp5OCyh6C4 1 2 ACCESS_TOKEN = eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwic3R0IjoiYWNjZXNzIiwiYWxnIjoiSFMyNTYifQ . eyJqdGkiOiIzVFhQZ01Ld0NiQTk1VEp6VzBXTzRWIiwiaWF0IjoxNDY1OTUyMzgzLCJpc3MiOiJodHRwczovL2FwaS5zdG9ybXBhdGguY29tL3YxL2FwcGxpY2F0aW9ucy82dkZUNEFSZldDbXVIVlY4Vmt0alRvIiwic3ViIjoiaHR0cHM6Ly9hcGkuc3Rvcm1wYXRoLmNvbS92MS9hY2NvdW50cy8zcVlHbUl6VWh4UEtZTzI4a04wSWJSIiwiZXhwIjoxNDY1OTU1OTgzLCJydGkiOiIzVFhQZ0owckkwckFTZUU4SmtmN1NSIn0 . o_pIHZVDZWogNuhJN2dmG4UKxACoWFxpRpp5OCyh6C4

Now, let’s hit our application again with authentication:

http -v localhost:8080 Authorization:"Bearer $ACCESS_TOKEN" HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Date: Wed, 15 Jun 2016 01:05:35 GMT Server: Apache-Coyote/1.1 Transfer-Encoding: chunked { "message": "Hello Micah", "status": "SUCCESS" } 1 2 3 4 5 6 7 8 9 10 11 12 13 http - v localhost : 8080 Authorization : "Bearer $ACCESS_TOKEN" HTTP / 1.1 200 OK Content - Type : application / json ; charset = UTF - 8 Date : Wed , 15 Jun 2016 01 : 05 : 35 GMT Server : Apache - Coyote / 1.1 Transfer - Encoding : chunked { "message" : "Hello Micah" , "status" : "SUCCESS" }

Now, we get the personalized response from our Service that the Controller has access to thanks to dependency injection.

4. Layer in Spring Security

Spring Security adds an authorization layer to Spring applications that makes it really easy to determine who should have access to what. It uses a declarative configuration syntax and includes annotations to limit who can access methods based on group membership and fine-grained permissions.

If you’re interested in learning more, I’ve also written an in-depth Stormpath, Spring Boot and Spring Security tutorial. We also have a great tutorial that takes you from zero to full functioning Spring Security + Spring Boot WebMVC app in our open-source Java SDK project. Find the tutorial documentation here.

By default everything is locked down in Spring Security and the Stormpath Spring Security integration is a great example that follows this convention. To try out Spring Security with Stormpath, you simply need to apply the Stormpath integration in a configuration like so:

@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.apply(stormpath()).and() .authorizeRequests() .antMatchers("/").permitAll(); } } 1 2 3 4 5 6 7 8 9 10 11 @ Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @ Override public void configure ( HttpSecurity http ) throws Exception { http . apply ( stormpath ( ) ) . and ( ) . authorizeRequests ( ) . antMatchers ( "/" ) . permitAll ( ) ; } }

http.apply(stormpath()) is all that’s needed to configure the Stormpath Spring Security integration. The next two lines allow unauthenticated access to the “/” endpoint.

Let’s take a look at how this impacts a method in our controller:

@RequestMapping("/restricted") public ApiResponse restricted(HttpServletRequest req) { // guaranteed to have account because of Spring Security return new ApiResponse( "SUCCESS", "Hello " + AccountResolver.INSTANCE.getAccount(req).getGivenName() ); } 1 2 3 4 5 6 7 8 9 @ RequestMapping ( "/restricted" ) public ApiResponse restricted ( HttpServletRequest req ) { // guaranteed to have account because of Spring Security return new ApiResponse ( "SUCCESS" , "Hello " + AccountResolver . INSTANCE . getAccount ( req ) . getGivenName ( ) ) ; }

In this case, there’s no need to perform the null check on the account since we know that the only way to get into this method if after authentication. For example:

http localhost:8080/restricted HTTP/1.1 302 Found Cache-Control: no-cache, no-store, max-age=0, must-revalidate Content-Length: 0 Date: Wed, 15 Jun 2016 17:32:31 GMT Expires: 0 Location: http://localhost:8080/login 1 2 3 4 5 6 7 8 9 http localhost : 8080 / restricted HTTP / 1.1 302 Found Cache - Control : no - cache , no - store , max - age = 0 , must - revalidate Content - Length : 0 Date : Wed , 15 Jun 2016 17 : 32 : 31 GMT Expires : 0 Location : http : //localhost:8080/login

We are redirected to /login since we are unauthenticated. If I use my access token as before, it looks like this:

http localhost:8080/restricted Authorization:"Bearer $ACCESS_TOKEN" HTTP/1.1 200 OK Cache-Control: no-cache, no-store, max-age=0, must-revalidate Content-Type: application/json;charset=UTF-8 Date: Wed, 15 Jun 2016 17:34:34 GMT Expires: 0 Pragma: no-cache { "message": "Hello Micah", "status": "SUCCESS" } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 http localhost : 8080 / restricted Authorization : "Bearer $ACCESS_TOKEN" HTTP / 1.1 200 OK Cache - Control : no - cache , no - store , max - age = 0 , must - revalidate Content - Type : application / json ; charset = UTF - 8 Date : Wed , 15 Jun 2016 17 : 34 : 34 GMT Expires : 0 Pragma : no - cache { "message" : "Hello Micah" , "status" : "SUCCESS" }

5. Uniform Error Handling

Good API design dictates that your API returns a common response, even when something goes wrong. This makes parsing and marshalling JSON into Java Objects easier and more reliable.

Let’s try out an example. Here, we require a header called: Custom-Header . If that header is not present, an exception is thrown:

@RequestMapping("/custom-header") public ApiResponse customHeader(HttpServletRequest req) throws MissingCustomHeaderException { String customHeader = req.getHeader("Custom-Header"); if (customHeader == null) { throw new MissingCustomHeaderException( "'Custom-Header' on the request is required." ); } return new ApiResponse("SUCCESS", "Found Custom-Header: " + customHeader); } 1 2 3 4 5 6 7 8 9 10 11 12 @ RequestMapping ( "/custom-header" ) public ApiResponse customHeader ( HttpServletRequest req ) throws MissingCustomHeaderException { String customHeader = req . getHeader ( "Custom-Header" ) ; if ( customHeader == null ) { throw new MissingCustomHeaderException ( "'Custom-Header' on the request is required." ) ; } return new ApiResponse ( "SUCCESS" , "Found Custom-Header: " + customHeader ) ; }

If we look at the “happy path,” all is well:

http localhost:8080/custom-header \ Custom-Header:MyCustomValue \ Authorization:"Bearer $ACCESS_TOKEN" HTTP/1.1 200 OK Cache-Control: no-cache, no-store, max-age=0, must-revalidate Content-Type: application/json;charset=UTF-8 Date: Wed, 15 Jun 2016 22:28:47 GMT { "message": "Found Custom-Header: MyCustomValue", "status": "SUCCESS" } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 http localhost : 8080 / custom - header \ Custom - Header : MyCustomValue \ Authorization : "Bearer $ACCESS_TOKEN" HTTP / 1.1 200 OK Cache - Control : no - cache , no - store , max - age = 0 , must - revalidate Content - Type : application / json ; charset = UTF - 8 Date : Wed , 15 Jun 2016 22 : 28 : 47 GMT { "message" : "Found Custom-Header: MyCustomValue" , "status" : "SUCCESS" }

What if we don’t have the Custom-Header header?

http localhost:8080/custom-header Authorization:"Bearer $ACCESS_TOKEN" HTTP/1.1 500 Internal Server Error Cache-Control: no-cache, no-store, max-age=0, must-revalidate Connection: close Content-Type: application/json;charset=UTF-8 Date: Wed, 15 Jun 2016 22:34:13 GMT { "error": "Internal Server Error", "exception": "com.stormpath.spring.boot.examples.controller.HelloController$MissingCustomHeaderException", "message": "'Custom-Header' on the request is required.", "path": "/custom-header", "status": 500, "timestamp": 1466030053360 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 http localhost : 8080 / custom - header Authorization : "Bearer $ACCESS_TOKEN" HTTP / 1.1 500 Internal Server Error Cache - Control : no - cache , no - store , max - age = 0 , must - revalidate Connection : close Content - Type : application / json ; charset = UTF - 8 Date : Wed , 15 Jun 2016 22 : 34 : 13 GMT { "error" : "Internal Server Error" , "exception" : "com.stormpath.spring.boot.examples.controller.HelloController$MissingCustomHeaderException" , "message" : "'Custom-Header' on the request is required." , "path" : "/custom-header" , "status" : 500 , "timestamp" : 1466030053360 }

So, what’s wrong with this? For one, it doesn’t conform to the response format we’ve already established. Also, it results in a 500 (Internal Server Error) error, which is never good.

Fortunately, Spring Boot makes this an easy fix. All we need to do is add an exception handler. No other code changes are required.

@ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MissingCustomHeaderException.class) public ApiResponse exception(MissingCustomHeaderException e) { return new ApiResponse("ERROR", e.getMessage()); } 1 2 3 4 5 6 @ ResponseStatus ( HttpStatus . BAD_REQUEST ) @ ExceptionHandler ( MissingCustomHeaderException . class ) public ApiResponse exception ( MissingCustomHeaderException e ) { return new ApiResponse ( "ERROR" , e . getMessage ( ) ) ; }

Let’s look at the response now:

http localhost:8080/custom-header Authorization:"Bearer $ACCESS_TOKEN" HTTP/1.1 400 Bad Request Cache-Control: no-cache, no-store, max-age=0, must-revalidate Connection: close Content-Type: application/json;charset=UTF-8 Date: Wed, 15 Jun 2016 22:59:32 GMT { "message": "'Custom-Header' on the request is required.", "status": "ERROR" } 1 2 3 4 5 6 7 8 9 10 11 12 13 http localhost : 8080 / custom - header Authorization : "Bearer $ACCESS_TOKEN" HTTP / 1.1 400 Bad Request Cache - Control : no - cache , no - store , max - age = 0 , must - revalidate Connection : close Content - Type : application / json ; charset = UTF - 8 Date : Wed , 15 Jun 2016 22 : 59 : 32 GMT { "message" : "'Custom-Header' on the request is required." , "status" : "ERROR" }

Now we have the correct response, 400 (Bad Request) . We also have the response in the same format as successful responses.

Bonus Tip: Try Stormpath

Stormpath offers an advanced, developer-centric Identity service that includes both authentication and authorization and can be implemented in minutes. The Stormpath REST API lets developers quickly and easily build a wide variety of user management functions they would otherwise have to code themselves, including: