This tutorial introduces Spring’s comprehensive support for REST-powered web applications.
Today’s applications don’t exist in a vacuum. They are distributed, portable, mobile, integrated, social, and connected. Devices are small enough now that there’s no reason an application can’t go with you. We’ve seen the transformation that the rise of smart phones and tablets has had on our lives. What’s powering this transformation? How are these systems communicating? They’re certainly not using SOAP. Or CORBA. Most of them are using REST.
REST is a constraint on HTTP that was originally proffered by Dr. Roy Fielding in his 2000 doctoral dissertation. It is more a style than a standard. REST is not an all-or-nothing technology choice. Users can, and often do, choose to embrace the parts of REST that most effectively suit their use cases. APIs today are often said to be “RESTful” — or not — according to the depth of their compliance with the principals of REST.
We can use the Richardson Maturity Model by Leaonard Richardson to classify REST APIs. I think Martin Fowler does a good job explaining the concepts of the various levels, so we won’t endeavor to duplicate their explanations here. Here’s an abbreviated look.
-
Level 0 (The Swamp of POX) - In this level, the use of HTTP is almost incidental. HTTP is being used merely as the transport. For example, while SOAP services are often deployed over HTTP, they don’t have to be. They could, just as readily, be deployed over JMS or SMTP. Such services are said to be the least RESTful. POX refers to plain old XML, as many technologies in this level – including SOAP and XML-RPC - use HTTP as a transport for data encoded in XML, usually in a schema that models remote procedure call semantics.
-
Level 1 (Resources) - In this level, a singular service endpoint expands into multiple, distinct HTTP resources, corresponding to objects in your system, like a customer record, or a user record. Responses from these resources might reference other resources by their URIs in this topology. Here, for example, you might expose resources for each entity in a system, like
/users/1
,/users/2
, etc. -
Level 2 (Transport Native Properties) - In this level the API leverages transport-specific capabilities like headers, status codes, and verbs to build idiomatic HTTP APIs. This is an acceptable entry point into REST architectures.
-
Level 3 (Hypermedia) - The final level introduces something that you often hear referred to under the ugly acronym of HATEOAS (Hypertext As The Engine Of Application State). It involves decoupling the API from the URI topology and exposing navigation options to the client as response payload meta-data. This navigation meta-data is conveyed using a wrapper structure for response payloads that contain a collection of navigation options (
link
), and the payload intended for conveyance.
It’s interesting that most REST framework technologies will start you off at Level 2. If you’re using just Spring MVC and want to build RESTful services, this is where you start, by default. Let’s look at how to build such an API with Spring MVC. Before we can do that, however, we need to understand the domain of the backend service.
Let’s look at an example that has a simple domain: User
entities that contain collections of Customer
records. Our service tier has the contract shown in Listing JL-1.
CrmService
supports user profile and customer record manipulationimport org.springframework.http.MediaType; import java.util.Collection; public interface CrmService { ProfilePhoto readUserProfilePhoto(long userId); void writeUserProfilePhoto(long userId, MediaType mediaType, byte[] bytesForProfilePhoto); User findById(long userId); User createUser(String username, String password, String firstName, String lastName); User removeUser(long userId); User updateUser(long userId, String username, String password, String firstName, String lastName); User findUserByUsername(String username); Customer removeAccount(long userId, long customerId); Customer addAccount(long userId, String firstName, String lastName); Collection<Customer> loadCustomerAccounts(long userId); Customer findCustomerById(long customerId); // simple abstraction to hold // information about the profile photo. static class ProfilePhoto { private Long userId; private byte[] photo; private MediaType mediaType; public ProfilePhoto(long userId, byte[] data, MediaType mediaType) { this.mediaType = mediaType; this.photo = data; this.userId = userId; } public MediaType getMediaType() { return this.mediaType; } public byte[] getPhoto() { return this.photo; } public Long getUserId() { return this.userId; } } }
It in turn delegates to a repository type that we have defined as shown in Listing JL-2.
User
.package com.jl.crm.services; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.springframework.data.rest.repository.annotation.RestResource; import java.util.List; public interface UserRepository extends PagingAndSortingRepository<User, Long> { User findByUsername(@Param ("username") String username); List<User> findUsersByFirstNameOrLastNameOrUsername( @Param ("firstName") String firstName, @Param ("lastName") String lastName, @Param ("username") String username); }
The interface extends Spring Data’s PagingAndSortingRepository
, which provides generic methods for working with JPA entities. In our case, we’re typing this to User
entities, which have a generic key of type Long
.
The more interesting thing here are the two methods defined in the interface, findByUsername
and findUsersByFirstNameOrLastNameOrUsername
. These methods will be implemented dynamically by Spring Data. Roughly, the dynamic JPA implementation of findUsersByFirstNameOrLastNameOrUsername
will use the JPA EntityManager
to run a JPA query of roughly the form SELECT U FROM User U WHERE U.firstName = ? OR U.lastName = ? OR U.username = ?
. Pretty neat, right?
The web application hosting our REST API is a standard Servlet 3-compatible Spring MVC application. The application uses the Servlet 3 facility to bootstrap the container programmatically (as opposed to using a web.xml
)
The CrmWebApplicationInitializer
(shown in Listing JL-3) sets up the standard two-tiered configuration in Spring MVC applications: web-application-global Spring contexts are initialized as parents to DispatcherServlet
-local contexts. Template methods make it easy to register Servlet infrastructure pieces like Filter
instances, and to customize how the servlet was registered.
CrmWebApplicationInitializer
configures or Servlet 3 context and our Spring application contexts.package com.jl.crm.web; import com.jl.crm.services.ServiceConfiguration; import org.springframework.context.annotation.*; import org.springframework.web.filter.HiddenHttpMethodFilter; import org.springframework.web.multipart.MultipartResolver; import org.springframework.web.multipart.support.*; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; import javax.servlet.*; import java.io.File; public class CrmWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { private int maxUploadSizeInMb = 5 * 1024 * 1024; // 5 MB @Override protected Class<?>[] getRootConfigClasses() { return new Class<?>[]{ServiceConfiguration.class}; } @Override protected Class<?>[] getServletConfigClasses() { return new Class<?>[]{WebMvcConfiguration.class}; } @Override protected String[] getServletMappings() { return new String[]{"/"}; } @Override protected Filter[] getServletFilters() { return new Filter[]{new HiddenHttpMethodFilter(), new MultipartFilter()}; } @Override protected void customizeRegistration(ServletRegistration.Dynamic registration) { File uploadDirectory = ServiceConfiguration.CRM_STORAGE_UPLOADS_DIRECTORY; MultipartConfigElement multipartConfigElement = new MultipartConfigElement( uploadDirectory.getAbsolutePath(), maxUploadSizeInMb, maxUploadSizeInMb * 2, maxUploadSizeInMb / 2); registration.setMultipartConfig(multipartConfigElement); } }
The class WebMvcConfiguration
(shown in Listing JL-4) enables Spring MVC (with the @EnableWebMvc
annotation) and plugs in an implementation of Spring’s MultipartResolver
interface that delegates to the Servlet 3 javax.servlet.http.Part
API. The MultipartResolver
implementation is used to specify how Spring supports file uploads.
WebMvcConfiguration
@Configuration @ComponentScan @EnableWebMvc class WebMvcConfiguration { @Bean public MultipartResolver multipartResolver() { return new StandardServletMultipartResolver(); } }
The @EnableWebMvc annotation provides quite a punch for such a puny annotation! Spring MVC will do a lot of things automatically at this point.
-
Spring MVC supports object-to-XML marshalling in REST services if a JAXB implementation is on the CLASSPATH.
-
Spring MVC supports validation of request payloads using JSR 303 annotations if an implementation like Hibernate Validator is on the CLASSPATH.
-
Spring MVC supports rendering ATOM or ROME feeds if the Rome library is on the CLASSPATH.
-
Spring MVC exposes every bean registered with the
@Controller
annotation on it as HTTP endpoints.
We know that we want to support a few different resource URIs to work with our data.
-
/users
- The root URI for manipulatingUser
records. -
/users/{user}
- Access and manipulate a specificUser
.{user}
is a path variable expression that will be substituted at runtime for whatever values in a URI match this pattern. -
/users/{user}/customers
- Support traversal and modification of thecustomers
collection. -
/users/{user}/customers/{customer}
- Support manipulation of individualcustomer
records. -
/users/{user}/photo
- Support file upload and download for the profile photo for the given user.
The class shown in Listing JL-5 implements these endpoints.
package com.jl.crm.web; import com.jl.crm.services.*; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import javax.inject.Inject; import java.util.ArrayList; @Controller @RequestMapping (value = "/users", produces = MediaType.APPLICATION_JSON_VALUE) class UserController { private CrmService crmService; @Inject void setCrmService(CrmService crmService) { this.crmService = crmService; } @RequestMapping (method = RequestMethod.DELETE, value = "/{user}") @ResponseBody User deleteUser(@PathVariable Long user) { return crmService.removeUser(user); } @RequestMapping (method = RequestMethod.GET, value = "/{user}") @ResponseBody User loadUser(@PathVariable Long user) { return crmService.findById(user); } @RequestMapping (method = RequestMethod.GET, value = "/{user}/customers") @ResponseBody CustomerList loadUserCustomers(@PathVariable Long user) { CustomerList customerResourceCollection = new CustomerList(); customerResourceCollection.addAll(this.crmService.loadCustomerAccounts(user)); return customerResourceCollection; } @RequestMapping (method = RequestMethod.GET, value = "/{user}/customers/{customer}") @ResponseBody Customer loadSingleUserCustomer( @PathVariable Long user, @PathVariable Long customer) { return crmService.findCustomerById(customer); } static class CustomerList extends ArrayList<Customer> { } }
This is standard Spring MVC: requests are routed by the DispatcherServlet
to methods on controller methods based on a matching process that draws on the configuration specified in each @RequestMapping annotation.
The various controller methods return a value. Most of them return a domain model type — Customer
or User
(or collections of either) — and have been annotated with @ResponseBody
. When discovered on a method’s return value, @ResponseBody
triggers the conversion of the returned value into an HTTP response by delegating to any of the configured HttpMessageConverter
instances (supporting XML, JSON, etc.).
UserProfilePhotoController
(shown in Listing JL-6) supports reading and writing user profile images.
UserProfilePhotoController
receives uploaded file data and streams file data and ultimately depends on the MultipartResolver
configured in Listing JL-4. Arguably, the file upload endpoint should be exposed over HTTP PUT
, not POST
, but POST
works well enough and is easy to use from HTTP browsers. For practical reasons, you might consider exposing file endpoints as both POST
and PUT
.package com.jl.crm.web; import com.jl.crm.services.*; import org.springframework.http.*; import org.springframework.stereotype.Controller; import org.springframework.util.FileCopyUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import javax.inject.Inject; import java.net.URI; import java.util.Collections; @Controller @RequestMapping (value = "/users/{user}/photo") class UserProfilePhotoController { private CrmService crmService; @Inject void setCrmService(CrmService crmService) { this.crmService = crmService; } @RequestMapping (method = RequestMethod.POST) HttpEntity<Void> writeUserProfilePhoto( @PathVariable Long user, @RequestParam MultipartFile file) throws Throwable { // write it byte bytesForProfilePhoto[] = FileCopyUtils.copyToByteArray(file.getInputStream()); MediaType mt = MediaType.parseMediaType(file.getContentType()); this.crmService.writeUserProfilePhoto(user, mt, bytesForProfilePhoto); // tell the client what happened HttpHeaders httpHeaders = new HttpHeaders() ; URI uriOfUser = ServletUriComponentsBuilder.fromCurrentContextPath() .pathSegment( "/users/{user}" ) .buildAndExpand(Collections.singletonMap("user", user)) .toUri(); httpHeaders.setLocation( uriOfUser ); return new ResponseEntity<Void>(httpHeaders, HttpStatus.CREATED); } @RequestMapping (method = RequestMethod.GET) HttpEntity<byte[]> loadUserProfilePhoto(@PathVariable Long user) throws Throwable { // read it CrmService.ProfilePhoto profilePhoto = crmService.readUserProfilePhoto(user); if (null != profilePhoto){ // send it back to the client HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(profilePhoto.getMediaType()); return new ResponseEntity<byte[]>(profilePhoto.getPhoto(), httpHeaders, HttpStatus.OK); } throw new UserProfilePhotoReadException(user); } }
Our API works fine, but there are few things that we can improve. First, our API is fragile in that any changes to the URLs would break clients connecting to the system. Additionally, there’s no way to signal to the client what entities are related to this entity and how. Clients must simply understand the domain and the relationships a priori.
One approach that is well defined in the canonical book REST in Practice is called HATEOAS. HATEOAS is part of REST-creator Roy Fielding’s uniform access principle. Roy clarifies these concepts in a few points in this 2008 blog.
-
"A REST API must not define fixed resource names or hierarchies (an obvious coupling of client and server). Servers must have the freedom to control their own namespace."
-
"A REST API should never have “typed” resources that are significant to the client… The only types that are significant to a client are the current representation’s media type and standardized relation names…" That is to say: the client need only know how to work with ATOM/RSS, images, XLS, generic data encoded as JSON or XML, HTML, media, etc. No further knowledge about implementation types should be required.
-
The client should know about only one URI, the entry point (bookmark) URI. All other navigation should be discovered while interacting with the API. Navigation information is well conveyed using
link
entities, like this in XML:<link rel ="users" href="http://127.0.0.1:8080/users" />
or this in JSON:{ "rel" : "users", "href" : "http://127.0.0.1:8080/users" }
.
Spring HATEOAS layers on top of Spring MVC and makes it easy to incorporate support for these concerns in your Spring MVC applications. This brings us to Level 3 of the Richardson Maturity Model.
To enable Spring HATEOAS, add the spring-hateoas
dependencies to your CLASSPATH, and then update your WebMvcConfiguration
class as shown in Listing JL-7.
WebMvcConfigurationSupport
class.@Configuration @ComponentScan @EnableWebMvc @EnableHypermediaSupport class WebMvcConfiguration extends WebMvcConfigurationSupport { @Override public void configureContentNegotiation(ContentNegotiationConfigurer c) { c.defaultContentType(MediaType.APPLICATION_JSON); } @Bean public MultipartResolver multipartResolver() { .. } }
Central to Spring HATEAOS is the concept of a org.springframework.hateoas.Resource
- which is a wrapper for a response payload and navigation links that apply to that payload.
There are many competing formats for resource representation including Collection+JSON, HAL and Siren, and no real established winner (yet, though the trends seem to be favoring HAL). Spring HATEOAS supports its own representation, as well as HAL. Listing JL-8 demonstrates a resource (and associated links) using Spring HATEOAS' default representation format.
{ "property1FromJsonPayload" : .., "property2FromJsonPayload" : .., "links" : [ { "rel" : "customers", "href" : "http://.../customers" }, {..}, .. ] }
Listing JL-9 demonstrates a rewritten UserController that embraces Spring HATEOAS' Resource abstraction. The new implementaton delegates to resource assemblers to build the Resource
instances, then returns them wrapped in HttpEntity instances. HttpEntity instances convey a response payload (the Resource
) as well as HTTP response specifics like HTTP headers and status codes, key to building a idiomatic REST API.
UserController
that uses Spring HATEOAS Resource
import com.jl.crm.services.*; import org.springframework.hateoas.*; import org.springframework.http.*; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import javax.inject.Inject; import java.util.*; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; @Controller @ExposesResourceFor (User.class) @RequestMapping (value = ApiUrls.ROOT_URL_USERS, produces = MediaType.APPLICATION_JSON_VALUE ) class UserController { private CrmService crmService; private UserResourceAssembler userResourceAssembler; private CustomerResourceAssembler customerResourceAssembler; @Inject void setCrmService(CrmService crmService) { this.crmService = crmService; } @Inject void setUserResourceAssembler(UserResourceAssembler userResourceAssembler) { this.userResourceAssembler = userResourceAssembler; } @Inject void setCustomerResourceAssembler(CustomerResourceAssembler customerResourceAssembler) { this.customerResourceAssembler = customerResourceAssembler; } @RequestMapping (method = RequestMethod.DELETE, value = ApiUrls.URL_USERS_USER) HttpEntity<Resource<User>> deleteUser(@PathVariable Long user) { Resource<User> userResource = userResourceAssembler.toResource(crmService.removeUser(user)); return new ResponseEntity<Resource<User>>(userResource, HttpStatus.OK); } @RequestMapping (method = RequestMethod.GET, value = ApiUrls.URL_USERS_USER) HttpEntity<Resource<User>> loadUser(@PathVariable Long user) { Resource<User> resource = this.userResourceAssembler.toResource(crmService.findById(user)); return new ResponseEntity<Resource<User>>(resource, HttpStatus.OK); } @RequestMapping (method = RequestMethod.GET, value = ApiUrls.URL_USERS_USER_CUSTOMERS) HttpEntity<Resources<Resource<Customer>>> loadUserCustomers(@PathVariable Long user) { Collection<Resource<Customer>> customerResourceCollection = new ArrayList<Resource<Customer>>(); for (Customer c : this.crmService.loadCustomerAccounts(user)) { customerResourceCollection.add(customerResourceAssembler.toResource(c)); } Resources<Resource<Customer>> customerResources = new Resources<Resource<Customer>>(customerResourceCollection); customerResources.add(linkTo(methodOn(UserController.class).loadUserCustomers(user)).withSelfRel()); return new ResponseEntity<Resources<Resource<Customer>>>(customerResources, HttpStatus.OK); } @RequestMapping (method = RequestMethod.GET, value = ApiUrls.URL_USERS_USER_CUSTOMERS_CUSTOMER) HttpEntity<Resource<Customer>> loadSingleUserCustomer(@PathVariable Long user, @PathVariable Long customer ) { Resource<Customer> customerResource = customerResourceAssembler.toResource( this.crmService.findCustomerById(customer )); return new ResponseEntity<Resource<Customer>>(customerResource, HttpStatus.OK); } }
The ResourceAssembler implementations adapt our response payloads into Resource instances; they assemble them. Shown in Listing JL-10 is the resource assembler for Customer
entities.
ResourceAssembler
that adapts Customer
entities to Resource<Customer>>
instances.import com.jl.crm.services.*; import org.springframework.hateoas.*; import org.springframework.stereotype.Component; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; @Component class CustomerResourceAssembler implements ResourceAssembler<Customer, Resource<Customer>> { public static final String USER_REL = "user"; private Class<UserController> controllerClass = com.jl.crm.web.UserController.class; @Override public Resource<Customer> toResource(Customer customer) { long userId = customer.getUser().getId(); customer.setUser(null); Resource<Customer> customerResource = new Resource<Customer>(customer); Link selfLink = linkTo(methodOn(controllerClass).loadSingleUserCustomer(userId, customer.getId())).withSelfRel(); Link userLink = linkTo(methodOn(controllerClass).loadUser( userId)).withRel(USER_REL); customerResource.add(selfLink); customerResource.add(userLink); return customerResource; } }
The ResourceAssembler
creates a Resource
object with a payload and attaches Link
instances. Java doesn’t (yet) support method references, but Spring HATEOAS’s ControllerLinkBuilder
can create a dynamic proxy of the target controller method’s containing class. When a method on this proxy is invoked, its @RequestMapping
mapping information is extracted and used to generate the URI in the link. In this way, configuration (like URI mappings) is only ever specified once. It’s OK to pass in null
for all the arguments of the controller method except for the arguments that contribute to the URI itself, such as @PathVariable
-annotated arguments. You should pass in the real value there as it’ll contribute to the link’s URI.
The Link
builder fluid DSL captures both the URI (through the method-capturing proxy) and a rel
value, a string that describes the relevance of the URI for any client working with this resource. A resource
should only return Link
navigations that are valid for the resource in its current state. It would make no sense, for example, to return a Link
to obtain a refund on a resource for an order
that hasn’t been paid for yet.
From the documentation, `the goal of the `rest-shell
is to make it much easier to work with HATEOAS HTTP-based REST services by providing a command shell interface that keeps track of where you are in the HTTP resource hierarchy and allowing you to specify URLs by short, relative paths, or by rel
reference.'' Releases are available on the project’s GitHub page and via the brew
package manager on OSX (brew install rest-shell
). Listing JL-11 reproduces a session spent in the shell interacting with our hypermedia-powered API.
rest-shell
.Joshuas-MacBook-Pro:the-spring-rest-stack jlong$ rest-shell ___ ___ __ _____ __ _ _ _ _ __ | _ \ __/' _/_ _/' _/| || | / / | \ \ | v / _|`._`. | | `._`.| >< | / / / > > |_|_\___|___/ |_| |___/|_||_| |_/_/ /_/ 1.2.0.RELEASE Welcome to the REST shell. For assistance hit TAB or type "help". http://localhost:8080:> follow users http://localhost:8080/users:> follow 21 http://localhost:8080/users/21:> get > GET http://localhost:8080/users/21/ < 200 OK < Server: Apache-Coyote/1.1 < Content-Type: application/json < Transfer-Encoding: chunked < Date: Wed, 26 Jun 2013 11:39:16 GMT < { "links" : [ { "rel" : "self", "href" : "http://localhost:8080/users/21" }, { "rel" : "customers", "href" : "http://localhost:8080/users/21/customers" }, { "rel" : "photo", "href" : "http://localhost:8080/users/21/photo" } ], "id" : 21, "firstName" : "Josh", "profilePhotoMediaType" : "image/jpeg", .. } http://localhost:8080/users/21:> discover rel href ===================================================== self http://localhost:8080/users/21 customers http://localhost:8080/users/21/customers photo http://localhost:8080/users/21/photo http://localhost:8080/users/21:> follow customers http://localhost:8080/users/21/customers:> get > GET http://localhost:8080/users/21/customers/ < 200 OK < Server: Apache-Coyote/1.1 < Content-Type: application/json < Transfer-Encoding: chunked < Date: Wed, 26 Jun 2013 11:39:33 GMT < { "links" : [ { "rel" : "self", "href" : "http://localhost:8080/users/21/customers" } ], "content" : [ { "links" : [ { "rel" : "self", "href" : "http://localhost:8080/users/21/customers/76" }, { "rel" : "user", "href" : "http://localhost:8080/users/21" } ], "id" : 76, "signupDate" : 1371091042082, "firstName" : "Michael", "lastName" : "Chang", "databaseId" : 76 }, ... } http://localhost:8080/users/21/customers:>
In the session, I navigate URIs by following the link
elements. Commands like get
and post
support issuing the HTTP verbs of the same name. The rest-shell
can also discover
relevant navigation links if it understands the representation.
The UserProfilePhotoController
is doing about as little work as possible to satisfy the requirements of the endpoint it exposes. I’m not certain there’s really much that could be done to make this simpler. Our UserController
, on the other hand, deals a lot with the state management of User
and Customer
records — all that has to do with storing, updating, reading or removing them. When we implement this behavior — as often as not — we simply unwrap the request and forward it on to the injected crmService
, which in turn — as often as not — forwards the request on to the injected UserRepository
instance. So, why not cut out the middle men?
With Spring Data REST, we can do just that! Spring Data REST makes it dead simple to expose RESTful endpoints that work with Spring Data repository instances.
Add Spring Data’s pre-packaged Java configuration class, RepositoryRestMvcConfiguration
to the array of configuration classes returned in the getServletConfigClasses
method of CrmWebApplicationInitializer
. Listing JL-12 shows the updated code.
CrmWebApplicationInitializer
... public class CrmWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { ... @Override protected Class<?>[] getServletConfigClasses() { return new Class<?>[]{ // provided by Spring Data REST RepositoryRestMvcConfiguration.class, WebMvcConfiguration.class}; } ...
The RepositoryRestMvcConfiguration
class automatically detects Spring Data repositories that are annotated with @RestResource
and exports them as REST resources. Add the annotation to the UserRepository
interface and CustomerRepository
interfaces, as shown in Listing JL-13.
@RestResource
.@RestResource (path = "users", rel = "users") public interface UserRepository extends PagingAndSortingRepository<User, Long> { ... }
With Spring Data REST, all of the functionality in our existing UserController
is redundant! Disable the UserController
class by commenting out the @Controller
annotation on the UserController
.
Run the updated application ( hateoas-data
) and you’ll see the usual endpoints for /users
, /users/{user}
, /customers
, /customers/{customer}
as we had from before. But wait, there’s more! The only endpoints that Spring Data REST didn’t make redundant are the endpoints concerned with manipulating the profile photo. You’ll notice that you get a lot more out of the box than that when you first launch the browser.
/
returns a sort of useful index page of the possible resources and the collections they manage.
Spring Data REST can produce a schema for each resource, for example, http://localhost:8080/users/schema
(Listing JL-14).
users
resource.{ "links" : [ { "rel" : "users.user.customers", "href" : "http://localhost:8080/users/%257Bid%257D/customers" } ], "name" : "com.jl.crm.services.User", "properties" : { "id" : { "type" : "long", "description" : null, "required" : false }, "lastName" : { "type" : "string", "description" : null, "required" : false }, ...
Spring Data REST automatically exports repository finder methods as HTTP endpoints. These finder endpoints simply expose searches from a repository over HTTP. For example, the repository query method findUsersByFirstNameOrLastNameOrUsername
is exposed as http://localhost:8080/users/search/findUsersByFirstNameOrLastNameOrUsername?username=joshlong&lastName=long&firstName=josh
. I’m not sure you’d want to expose these finder endpoints to external consumers of your API, but it’s a nice feature anyway.
Spring Data REST respects Spring Data’s concepts of paging and sorting. As an example, http://localhost:8080/users/?sort=firstName&page=1
would return all the users
, sorted by the firstName
property.
Spring Data REST has a lot of extension points. For example, it is easy to pre- and post-process entity state while persisting entities with @RepositoryEventHandler
-annotated beans. Spring will invoke any annotated callback-methods for the appropriate events in the entity lifecycle. This gives us a chance to affect, or validate, the state of the entity before its persisted (Listing JL-15).
Customer
entities. It responds to events for the Customer
entity.import com.jl.crm.services.*; import org.apache.commons.logging.*; import org.springframework.data.rest.repository.annotation.*; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @Component @RepositoryEventHandler (Customer.class) public class CustomerEventHandler { private Log logger = LogFactory.getLog(getClass()); @HandleBeforeCreate public void handleBeforeCreate(Customer customer) { if (StringUtils.hasText(customer.getFirstName()) && StringUtils.hasText(customer.getLastName()) && customer.getUser() != null){ if (customer.getSignupDate() == null){ customer.setSignupDate(new java.util.Date()); } } else { throw new CustomerWriteException(customer, new RuntimeException("you must specify a 'firstName' and a 'lastName' and a valid user reference.")); } logger.debug("handling before create for " + customer.toString()); } @HandleAfterSave public void handleAfterSave(Customer customer) { logger.debug("saved customer #" + customer.getId()); } @HandleAfterDelete public void handleAfterDelete(Customer customer) { logger.debug("deleted customer #" + customer.getId()); } }
Spring Data REST returns well-formatted Spring HATEOAS-powered JSON representations, complete with useful link
elements for the associations on the entity. Automatically… That’s pretty darned good if you think about it!
Delete the UserController
, as it is now obsolete. This impacts all of the link
instances we created earlier, as they depended on a proxy that matches the UserController
interface. Instead, use the EntityLinks
link-builder. It supports common conventions for entities. For example: given the entity User
, it’s expected that /users
maps to the collection of User
entities, and that /users/{id}
will map to a single instance of an entity (as identified by an ID).
The ResourceAssembler
implementations are no longer needed and so can be deleted. The only thing remaining is our UserProfilePhotoController
, which creates link
elements. Creating link
elements is something common enough that it makes sense to extract it out to a common component, like the UserLinks
class shown in Listing JL-16.
UserLinks
class codifies the creation of commonly used Link
types.import com.jl.crm.services.User; import org.springframework.hateoas.*; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import javax.inject.Inject; @Component public class UserLinks { private static final String PHOTO = "photo"; private static final String PHOTO_REL = "photo"; private static final String CUSTOMER = "customers"; private static final String CUSTOMER_REL = "customers"; private final EntityLinks entityLinks; @Inject UserLinks(EntityLinks entityLinks) { Assert.notNull(entityLinks, "EntityLinks must not be null!"); this.entityLinks = entityLinks; } Link getSelfLink(User user) { return this.entityLinks.linkForSingleResource(User.class, user.getId()) .withSelfRel(); } Link getCustomersLink(User user) { return this.entityLinks.linkForSingleResource(User.class, user.getId()) .slash(CUSTOMER).withRel(CUSTOMER_REL); } Link getPhotoLink(User user) { return this.entityLinks.linkForSingleResource(User.class, user.getId()) .slash(PHOTO).withRel(PHOTO_REL); } }
Now, revise the UserProfilePhotoController
class to use the UserLinks
bean, as shown in Listing JL-17.
UserProfilePhotoController
using the UserLinks
bean@Controller @RequestMapping (value = "/users/{user}/photo") public class UserProfilePhotoController { ... private UserLinks userLinks; @Inject void setUserLinks(UserLinks userLinks) { this.userLinks = userLinks; } @RequestMapping (method = RequestMethod.POST) HttpEntity<Void> writeUserProfilePhoto(@PathVariable User user, @RequestParam MultipartFile file) throws Throwable { ... Link photoLink = this.userLinks.getPhotoLink(user); Link userLink = this.userLinks.getSelfLink(user); Links wrapperOfLinks = new Links(photoLink, userLink); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add("Link", wrapperOfLinks.toString()); httpHeaders.setLocation(URI.create(photoLink.getHref())); return new ResponseEntity<Void>(httpHeaders, HttpStatus.ACCEPTED); } ... }
HTTP’s flexibility and simplicity can be deceptive. Just how does one secure a REST service? The answer is as always: it depends. What do you mean by authorization? Encryption? Authentication? Most of the concerns we have are already addressed and supported by the open web and Spring Security. Let’s look at the oauth
module, which introduces security to the REST service we built up in our last installment.
HTTP Secure (HTTPS) is a communications protocol for secure communication on top of the SSL/TLS protocol. It protects against man-in-the-middle attacks and attempts to ensure the integrity of the payload communicated between two parties. You can easily set this feature up on most web servers as well as most cloud platforms. It’s fairly easy to setup SSL/TLS with Apache Tomcat or with a cloud platform like AppFog or Heroku. What little there is to do at the application level, Spring Security ably supports. HTTPS is something configured at the runtime-level, however, so we won’t explore this topic any further in this post. At the time of this writing, however, it was discovered that HTTPS, particularly older versions, has been compromised.
HTTP Basic Authentication provides a very simple way to handle access control. Another approach to securing an application is to simply use a login form on the application somewhere, typically in conjunction with something like a server-side session. Neither HTTP Basic Authentication nor sign-in forms provide any guarantee of confidentiality of transmitted content. They’re both best used in conjunction with SSL/TLS.
We’ll use the Spring Security Java Configuration support to configure our application’s security rules. Spring Security is ultimately implemented in a web application as a javax.filter.Filter
object configured in the servlet container. The Spring Security filter should be the front-line of the web application, inspecting every request as it goes through. To automate the installation of all of these components, Spring Security supports a custom WebApplicationInitializer
subclass called AbstractSecurityWebApplicationInitializer
. You simply extend the class and fill out the abstract template methods with information that it needs. See Listing JL-1 for an example.
AbstractSecurityWebApplicationInitializer
to secure our Spring applicationpackage com.jl.crm.web; import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter; import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; import org.springframework.security.web.session.HttpSessionEventPublisher; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.filter.HiddenHttpMethodFilter; import org.springframework.web.multipart.support.MultipartFilter; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer; import javax.servlet.ServletContext; public class CrmSecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer { @Override protected String getDispatcherWebApplicationContextSuffix() { return AbstractDispatcherServletInitializer.DEFAULT_SERVLET_NAME; } @Override protected void afterSpringSecurityFilterChain(ServletContext servletContext) { insertFilters(servletContext, new HiddenHttpMethodFilter(), new MultipartFilter() , new OpenEntityManagerInViewFilter()); } @Override protected boolean enableHttpSessionEventPublisher() { return true; } }
This WebApplicationInitializer
instance is additive - it will be included in addition to any other initializers present. As you might imagine, this is a very convenient way for modules to provide out-of-the-box integration with the Servlet container. CrmSecurityApplicationInitializer
implements the abstract callback method, afterSpringSecurityFilterChain
which gives the client a chance to insert javax.filter.Filter
instances into the Filter
chain after Spring Security’s filter is installed. As we’re installing the Filter
instances here, we can remove the corresponding entries from our CrmWebApplicationInitializer
.
The Spring Security Java configuration class presented in Listing JL-2 demonstrates how to setup form-based signin and signout functionality, and how to specify which URIs should be protected by that signin form. Our sample application uses H2, the embedded Java database. The H2 database console itself uses HTML frame
elements and so runs afoul of a particular type of protection that Spring Security installs by default. The configurer object returned from the headers()
method allows us to override the default HeaderWriter
instances to avoid this problem.
@Configuration @EnableWebSecurity class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Inject private UserDetailsService userDetailsService; @Override protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(this.userDetailsService); } @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/crm/signin.html") .loginProcessingUrl("/signin") .defaultSuccessUrl("/crm/welcome.html") .failureUrl("/crm/signin.html?error=true") .usernameParameter("username") .passwordParameter("password") .permitAll(true); http.headers() .addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN)) .addHeaderWriter(new XContentTypeOptionsHeaderWriter()) .addHeaderWriter(new XXssProtectionHeaderWriter()) .addHeaderWriter(new CacheControlHeadersWriter()) .addHeaderWriter(new HstsHeaderWriter()); http.logout().logoutUrl("/signout").deleteCookies("JSESSIONID"); // nb: the H2 administration console should *not* be left exposed. // comment out the mapping path below so that it requires an authentication to see it. String[] filesToLetThroughUnAuthorized = { H2EmbeddedDatbaseConsoleInitializer.H2_DATABASE_CONSOLE_MAPPING, "/favicon.ico" }; http.authorizeRequests() .antMatchers(filesToLetThroughUnAuthorized).permitAll() .anyRequest().authenticated(); } }
The userDetailsService
instance registered in registerAuthentication
is an implementation of Spring Security’s UserDetailsService
interface. This interface is a strategy interface with numerous other backend implementations available supporting technologies like LDAP, Active Directory, pam
, SAML, etc.
Our system already has a notion of a user. It’s easy enough to adapt Spring Security to our user management logic with a custom implementation of UserDetailsService
. This interface is integral to Spring Security. Listing JL-3 demonstrates how we wrap and adapt our existing user-management logic to fit in the Spring Security framework. The implementation mostly delegates what it can to our underlying service, and hard codes the rest as our application isn’t all that involved at the moment. Note, particularly, the scopes and role harccoded for the resulting UserDetails
instances as we’ll refer to those again later.
package com.jl.crm.services.security; import com.jl.crm.services.CrmService; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.*; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import javax.inject.Inject; import java.util.*; @Component ("userService") public class CrmUserDetailsService implements UserDetailsService { private CrmService crmService; @Inject public void setCrmService(CrmService crmService) { this.crmService = crmService; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { com.jl.crm.services.User user = crmService.findUserByUsername(username); return new CrmUserDetails(user); } @SuppressWarnings("serial") public static class CrmUserDetails implements UserDetails { public static final String SCOPE_READ = "read"; public static final String SCOPE_WRITE = "write"; public static final String ROLE_USER = "ROLE_USER"; private Collection<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>(); private com.jl.crm.services.User user; public CrmUserDetails(com.jl.crm.services.User user) { Assert.notNull(user, "the provided user reference can't be null"); this.user = user; for (String ga : Arrays.asList(ROLE_USER, SCOPE_READ, SCOPE_WRITE)) { this.grantedAuthorities.add(new SimpleGrantedAuthority(ga)); } } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.grantedAuthorities; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUsername(); } @Override public boolean isAccountNonExpired() { return isEnabled(); } @Override public boolean isAccountNonLocked() { return isEnabled(); } @Override public boolean isCredentialsNonExpired() { return isEnabled(); } @Override public boolean isEnabled() { return user.isEnabled(); } public com.jl.crm.services.User getUser() { return this.user; } } }
With all of this in place, any unauthorized HTTP request will be routed to the signin
view (signin.html
). When authentication is successful, the client is then redirected to /crm/welcome.html
. JL-4 presents the @Controller
handler for the signin
and welcome
views, both of which are used in the signin process.
@Controller
that simply returns views for /crm/signin.html
and /crm/welcome.html
. These could, as easily, be view controllers.package com.jl.crm.web; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class PageControllers { @RequestMapping ("/crm/welcome.html") public String welcome() { return "welcome"; } @RequestMapping ("/crm/signin.html") public String signin() { return "signin"; } }
The markup for each view (shown in Listing JL-5 and JL-6) is simple, fairly typical HTML (again in the interest of conciseness and transparency). You could easily use Spring Mobile to conditionally render experiences tailored to mobile, desktop, or tablet clients. In this case, the markup is so simple that it looks equally boring on all platforms, so we didn’t need Spring Mobile.
signin.jsp
page<%@ taglib prefix="authz" uri="http://www.springframework.org/security/tags" %> <html> <head> <title> Sign In </title> </head> <body> <h1> Sign In</h1> <div>${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message} </div> <authz:authorize ifAllGranted="ROLE_USER"> <A href="${pageContext.request.contextPath}/signout">Sign Out</A> </authz:authorize> <authz:authorize ifNotGranted="ROLE_USER"> <p> Please enter your username and password to log into the application. </p> <form method="post" action="${pageContext.request.contextPath}/signin"> <DIV> <label style="width: 100px; display: inline-block;" class="control-label" for="username"> User Name: </label> <br/> <input id="username" name="username" type="text"/> </DIV> <DIV> <label style="width: 100px; display: inline-block" class="control-label" for="password"> Password: </label> <br/> <input class="input-xlarge" id="password" name="password" type="password"/> </DIV> <input type="submit"/> </form> </authz:authorize> </body> </html>
welcome.jsp
page<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <html> <head> <title> Greetings! </title> </head> <body> <sec:authorize ifAllGranted="ROLE_USER"> <h1>Hi, <sec:authentication property="principal.username"/>!</h1> <a href="${pageContext.request.contextPath}/signout">Sign Out</a>. </sec:authorize> <sec:authorize ifNotGranted="ROLE_USER"> <h1>Hello, stranger.</h1> </sec:authorize> </body> </html>
First things first: we don’t want users reading other users' information. We can deny access to the /users/
endpoint simply by adding another restriction in Spring Security.
/users/*
endpoint in the SecurityConfiguration
class.... http.authorizeRequests() .antMatchers(filesToLetThroughUnAuthorized).permitAll() .antMatchers("/users/*").denyAll() .anyRequest().authenticated(); ...
We still need a way to access the currently authenticated user. As a convenience, it’s useful to implement an endpoint to
return the currently authenticated user, which we know we can get at from the Spring Security context.
This listing demonstrates one possible implementation for a REST endpoint situated at /user
.
/user
package com.jl.crm.web; import com.jl.crm.services.User; import org.springframework.hateoas.*; import org.springframework.http.*; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import javax.inject.Inject; import java.util.*; @Controller public class CurrentUserController { private UserLinks userLinks; @RequestMapping (value = "/user", method = RequestMethod.GET) public HttpEntity<Resource<User>> currentUser(@ModelAttribute User self) { List<Link> linkList = new ArrayList<Link>(); linkList.add(this.userLinks.getSelfLink(self)); linkList.add(this.userLinks.getPhotoLink(self)); linkList.add(this.userLinks.getCustomersLink(self)); UserResource userResource = new UserResource(self, linkList); return new ResponseEntity<Resource<User>>(userResource, HttpStatus.OK); } @Inject public void setUserLinks(UserLinks userLinks) { this.userLinks = userLinks; } static class UserResource extends Resource<User> { public UserResource(User content, Iterable<Link> links) { super(content, links); } } }
The controller injects the current User as a @ModelAttribute
into the controller method argument. As you might imagine,
having the current authenticated user available can be a handy thing in any complicated system. We could already simply inject
the current Spring Security Authentication
object then dereference the principal
, but this involves an ugly cast that
would end up littered throughout the code. Instead, it’s easy enough to put that code in a Spring MVC @ControllerAdvice
component. A @ControllerAdvice
instance looks and feels very much like a controller in that it may register exception handlers (with the @ExceptionHandler
annotation),
and model attributes (with the @ModelAttribute
annotation), etc., in a central place and is involved in every
request. Here’s an implementation - SecurityControllerAdvice
- that sets up a @ModelAttribute
for the currently authenticated
users.
/user
package com.jl.crm.web; import com.jl.crm.services.*; import com.jl.crm.services.security.CrmUserDetailsService; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; import javax.inject.Inject; @ControllerAdvice public class SecurityControllerAdvice { @Inject private CrmService service; @ModelAttribute public User currentUser(Authentication authentication) { if (null == authentication){ return null; } CrmUserDetailsService.CrmUserDetails crmUserDetails = (CrmUserDetailsService.CrmUserDetails) authentication.getPrincipal(); long userId = crmUserDetails.getUser().getId(); return this.service.findById(userId); } }
With all of this in place, launch the application and then authenticate yourself using any of the users and passwords in the user_account
table in our schema. By default, the application preloads data from crm-schema-h2.sql
.
Username and password-based access control works well enough for trusted clients. If you are the only person who knows your (hopefully randomized) password, then the chances that a malicious client might afflict your account are far lower. A password authentication scheme is a simple way to identify that a client is who it says it is. However, this scheme starts to fall apart in a world where multiple clients may interact with a service provider on your behalf. This is typical, for example, when using service-provider clients for Netflix, Facebook, Twitter, and Google+ clients across phones, desktop browsers, tablets, and TVs.
For a specific example, let’s look at the Facebook application ecosystem. Facebook opened up the Facebook object graph through its REST APIs and opened the door to a slew of application developers that wanted to deliver applications (``Sign In with Facebook'') and games integrated with your Facebook experience and data. Some of these API clients were better than others. Some were downright abusive! However, no matter how abusive a client got, it could never lock you out of your own account because it never had access to your password. Instead, clients authenticate with Facebook and other service providers using a system called OAuth.
You’ll recognize the telltale signs of OAuth when you use a Sign In with Facebook'' or
Sign In with Twitter'' button on any number of web properties. Invariably, these buttons launch Facebook.com, prompt the user to sign in (if not already signed in) (as shown in Figure JL-1) and then prompt the user to authorize the permissions required by the application (shown in Figure JL-2). Once permission is granted, you’ll end up back where you started, but the application will have what it needs to authenticate you and sign you in. Convenient, eh?
When a user authorizes the application and requested permissions, Facebook ultimately redirects the client to the requesting application and conveys in that request an accessToken
. The accessToken
is like a session cookie, and tells the server which client is connecting on behalf of the authenticated user. Additionally, Facebook has the ability to track which applications are installed in an application console (shown in Figure JL-3). The console is powerful: here, a user may centrally control (including revoke) access to any and all clients, selectively.
A full discussion of OAuth is out of scope here, but you might check out Dr. David Syer’s many amazing posts on the subject from the Cloud Foundry blog in 2012. OAuth provides a few things we’ll desire as part of our system.
So: a username and password scheme certifies that a request is being made by a user. OAuth certifies that the request being made has the permission to do so from the user. That’s close enough to certifying the user for most applications that OAuth works with.
Integrating OAuth into our application is a snap thanks to Spring Security OAuth. Spring Security OAuth introduces the concept of a client
- a logical notion composed of the authenticated user, a unique identifier, certain permissions (or scopes) that the client is permitted, and the type of OAuth connectivity supported by that client. OAuth supports varying levels of security, and can force the client to go through more hoops to increase the confidence in the security provided. We’re going to assume the common, but not exclusive, case of working with an Android mobile client. Spring Security OAuth needs information about which clients will connect.
The listing below demonstrates the things added to the Spring Security Java configuration class - SecurityConfiguration
- to support Spring Security OAuth.
OAuth2ServerConfigurerAdapter
.@Configuration @EnableWebSecurity class SecurityConfiguration extends OAuth2ServerConfigurerAdapter { private final String applicationName = ServiceConfiguration.CRM_NAME; @Inject private UserDetailsService userDetailsService; @Inject private DataSource dataSource; @Override protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception { auth.apply(new InMemoryClientDetailsServiceConfigurer()) .withClient("android-crm") .resourceIds(applicationName) .scopes("read", "write") .authorities("ROLE_USER") .authorizedGrantTypes("authorization_code", "implicit", "password") .secret("123456"); auth.userDetailsService(userDetailsService); } @Override protected void configure(HttpSecurity http) throws Exception { // .. http.apply(new OAuth2ServerConfigurer()) .tokenStore(new JdbcTokenStore(this.dataSource)) .resourceId(applicationName); // .. } @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Bean public TextEncryptor textEncryptor() { return Encryptors.noOpText(); } }
The first thing you’ll notice in Listing JL-8 is that the class extends OAuth2ServerConfigurerAdapter
instead of WebSecurityConfigurerAdapter
. The two beans - passwordEncoder
and textEncryptor
- below are just no-op implementations of two APIs that Spring Security requires.
In Spring Security OAuth, the ClientDetailsService
manages the information about which clients might connect, and how. To simplify setup, register an InMemoryClientDetailsServiceConfigurer
in the registerAuthentication
method. You could as easily plugin an implementation backed by a database.
Here, we have only one client in mind, so we’ll configure a client (android-crm
) that requests two scope's, read
and write
. Scopes are arbitrary strings. In the Facebook world, they correspond to things an API client might want to do, like post to your wall, access your email address, etc. In the Spring Security world, they line up with the grantedAuthorities
returned from the CRM-specific UserDetails
instances vended by our CrmUserDetailsService
instance. In our application, these roles are arbitrary, hard-coded Strings. You can, of course, be as granular as you like in using this facility to limit access to certain parts of your application.
Once the user has successfully authenticated, an OAuth service sends back a token to be transmitted on all subsequent API requests. The service needs to remember that token. Spring Security OAuth delegates this chore to a TokenStore
-implementation, in this case a JdbcTokenStore
.
When users run through the OAuth dance with our application, on the other hand, they’ll be presented with a page to confirm what scopes the user authorizes to the client. We must provide this page ourselves. The code in Listing JL-9 shows an implementation that prepares information and then renders access_confirmation
. The view itself - access_confirmation
- is shown below.
@Controller
to respond to the /oauth/confirm_access
callback in the OAuth dancepackage com.jl.crm.web; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.provider.*; import org.springframework.security.web.WebAttributes; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; import javax.inject.Inject; import javax.servlet.http.*; import java.util.Map; @Controller @SessionAttributes ("authorizationRequest") public class OAuthController { private ClientDetailsService clientDetailsService; @RequestMapping ("/oauth/confirm_access") public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest httpServletRequest) throws Exception { AuthorizationRequest clientAuth = (AuthorizationRequest) model.remove("authorizationRequest"); ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId()); model.put("auth_request", clientAuth); model.put("client", client); HttpSession httpSession = httpServletRequest.getSession(false); if (httpSession != null){ Object exception = httpSession.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); if (exception != null && exception instanceof AuthenticationException){ model.put("exception", exception); } } return new ModelAndView("access_confirmation", model); } @Inject public void setClientDetailsService(ClientDetailsService clientDetailsService) { this.clientDetailsService = clientDetailsService; } }
access_confirmation
view. Prompts the user to grant the client permissions, or not.<%@ taglib prefix="authz" uri="http://www.springframework.org/security/tags" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <html> <head> <title> Please authorize the client "${client.clientId}" to act on your behalf. </title> </head> <body> <h1> Access Confirmation </h1> <authz:authorize ifAllGranted="ROLE_USER"> <p> Do you approve <strong> ${client.clientId} </strong> with the following permissions? </p> <form action="${pageContext.request.contextPath}/oauth/authorize" method="post"> <button type="submit" name="authorize" value="${buttonLabel}">Yes</button> <input name="user_oauth_approval" value="true" type="hidden"/> <div> <strong>Yes</strong>, I authorize <strong>${client.clientId}</strong> to act on my behalf. Your password will <EM>not</EM> be shared with the client. </div> </form> <form action="${pageContext.request.contextPath}/oauth/authorize" method="post"> <button type="submit" name="deny" value="${buttonLabel}"> No </button> <input name="user_oauth_approval" value="false" type="hidden"/> <div> <strong>No</strong>, I do not authorize <strong>${client.clientId}</strong> to act on my behalf. </div> </form> </authz:authorize> </body> </html>
The RestTemplate
- in core Spring - is the pillar of Spring’s REST story on the client. The RestTemplate
reduces common HTTP operations to one liners like JdbcTemplate
does JDBC operations. The RestTemplate
features the ability to plugin HTTP message conversion using the same HttpMessageConverter
hierarchy available on the service-side for REST content negotiation. The RestTemplate
also supports an interceptor model for processing all requests. This makes it easy to handle things like security in an single place.
The RestTemplate
is powerful, and makes a great foundation for any REST client, but it’s too low-level for something like consuming OAuth services. As we’ve just seen, implementing OAuth does introduce some complexity that straight REST calls don’t have. This complexity can’t be avoided either. After all, between OAuth 1.0, 1.0.a, and 2.0, and the various inconsistencies among their implementations (OAuth 2.0 isn’t even really a specification - it’s more like a framework!), building an OAuth client can be quite challenging. There are enough popular service providers out there with APIs exposed using OAuth that simply ignoring OAuth isn’t an option either! Spring Social fills the gap.
Spring Social is an extension of the Spring Framework that allows you to connect your applications with Software-as-a-Service (SaaS) providers, like Facebook and Twitter. As part of that charge, it makes it dead simple to build OAuth clients by building on top of the RestTemplate
to support transparent, secured REST calls, and manage HTTP callbacks.
This core support for OAuth authorization and authentication underpins a rich catalog of API bindings for popular service-providers like Twitter, Facebook, LinkedIn, and more. You should take a quick look at all the repositories on the SpringSource GitHub repository to see the API bindings that SpringSource maintains directly. I, however, think the (very incomplete!) list of some of the third party API bindings is a far more interesting list!
Spring Social is probably most often going to be used inside of a web container. We’re going to build our Spring Social CRM client to be used in an Android client, outside of a web container. OAuth works best when the client to the service provider is itself a web application that can receive callbacks. Spring Social, when used inside of a Spring MVC web application, is almost trivial to setup. That scenario is very well documented elsewhere, and what we really need is a client that can run in Android, so we’ll cover that instead.
All Spring Social clients share basically the same arrangement as the one we’ll implement here. Once you understand how to exercise one, then - while accounting for some small variations between OAuth versions - you’ll use the API in roughly the same way thanks to Spring Social’s abstractions.
A Spring Social binding centers around an API interface and an implementation. The interface for the CRM is, unsurprisingly, CrmOperations
, and the implementation CrmTemplate
. This follows a Spring-project convention: JdbcOperations
and JdbcTemplate
, JmsOperations
and JmsTemplate
, FacebookOperations
and FacebookTemplate
, etc. Our client interface is shown in Listing JL-11.
CrmOperations
is a client-side mirror to our REST API.package com.jl.crm.client; import org.springframework.http.MediaType; import java.net.URI; import java.util.*; public interface CrmOperations { User currentUser(); Customer loadUserCustomer(Long id); Customer createCustomer(String firstName, String lastName, Date signupDate); Collection<Customer> loadAllUserCustomers(); void removeCustomer(Long id); void setUserProfilePhoto(byte[] bytesOfImage, MediaType mediaType); Customer updateCustomer(Long id, String firstName, String lastName); ProfilePhoto getUserProfilePhoto(); }
For our API binding to do any work, it needs to establish a Spring Social connection
to the service provider. The connection
may only be obtained after the token provider (our REST service from the oauth
module) has given the client an accessToken
. Spring Social handles all the work involved in obtaining the valid accessToken
and connection, so let’s focus on writing our REST client using the RestTemplate
.
We won’t rehash the entire implementation. For that, it might be easier to simply look at the source code online. Instead, let’s look at some of the salient excerpts and the things common to all of them.
Some of the code for our implementation - CrmTemplate
- is shown in Listing JL-12. Note that we’re using a client side mirror API of types from the service implementation, not sharing. This is important because our service-side API might depend on classes that we don’t want polluting all users of our client library. In particular, this situation is particularly troublesome in Android where only a subset of the JDK classes are available (whitelisted).
CrmTemplate
implements our client API and is where we provide an HTTP client to the REST API.package com.jl.crm.client; import org.springframework.core.io.ByteArrayResource; import org.springframework.hateoas.*; import org.springframework.http.*; import org.springframework.social.oauth2.AbstractOAuth2ApiBinding; import org.springframework.social.support.ClientHttpRequestFactorySelector; import org.springframework.util.*; import org.springframework.web.util.UriComponentsBuilder; import java.io.File; import java.net.URI; import java.util.*; public class CrmTemplate extends AbstractOAuth2ApiBinding implements CrmOperations { private final File rootFile = new File(System.getProperty("java.io.tmpdir")); private URI apiBaseUri; public CrmTemplate(String accessToken, String apiUrl) { super(accessToken); try { this.apiBaseUri = new URI(apiUrl); setRequestFactory( ClientHttpRequestFactorySelector.bufferRequests(getRestTemplate().getRequestFactory())); } catch (Exception e) { throw new RuntimeException("could not initialize the " + CrmTemplate.class.getName(), e); } } @Override public User currentUser() { ResponseEntity<UserResource> userResponse = this.getRestTemplate().getForEntity(uriFrom("/user"), UserResource.class); UserResource userResource = userResponse.getBody(); return unwrapUser(userResource); } @Override public Customer createCustomer(String firstName, String lastName, Date signupDate) { Customer customer = new Customer(currentUser(), null, firstName, lastName, signupDate); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); // build up a representation of the domain model and transmit it // as JSON no need to use the actual objects. this is simpler and more predictable. Map<String, Object> mapOfCutomerData = customerMap(customer); HttpEntity<Map<String, Object>> customerHttpEntity = new HttpEntity<Map<String, Object>>(mapOfCutomerData, httpHeaders); ResponseEntity<?> responseEntity = this.getRestTemplate().postForEntity(uriFrom("/customers"), customerHttpEntity, ResponseEntity.class); URI newLocation = responseEntity.getHeaders().getLocation(); return customer(newLocation); } @Override public void removeCustomer(Long customer) { URI uri = uriFrom("/customers/" + Long.toString(customer)); this.getRestTemplate().delete(uri); } @Override public void setUserProfilePhoto(byte[] bytesOfImage, final MediaType mediaType) { ByteArrayResource byteArrayResource = new ByteArrayResource(bytesOfImage) { @Override public String getFilename() { return new File(rootFile, "profile-image." + mediaType.getSubtype()).getAbsolutePath(); } }; MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>(); parts.set("file", byteArrayResource); String photoUri = uriFrom("/users/" + currentUser().getDatabaseId() + "/photo").toString(); ResponseEntity<?> responseEntity = this.getRestTemplate() .postForEntity(photoUri, parts, ResponseEntity.class); HttpStatus.Series series = responseEntity.getStatusCode().series(); if (!series.equals(HttpStatus.Series.SUCCESSFUL)){ throw new RuntimeException("couldn't write the profile photo!"); } } @Override public ProfilePhoto getUserProfilePhoto() { ResponseEntity<byte[]> profilePhotoData = this.getRestTemplate() .getForEntity(uriFrom("/users/" + currentUser().getDatabaseId() + "/photo").toString(), byte[].class); MediaType mediaType = profilePhotoData.getHeaders().getContentType(); return new ProfilePhoto(profilePhotoData.getBody(), mediaType); } private Map<String, Object> customerMap(Customer customer) { Map<String, Object> mapOfUserData = null; if (customer.getUser() != null){ mapOfUserData = new HashMap<String, Object>(); mapOfUserData.put("id", customer.getUser().getDatabaseId()); } Map<String, Object> mapOfCutomerData = new HashMap<String, Object>(); mapOfCutomerData.put("firstName", customer.getFirstName()); mapOfCutomerData.put("lastName", customer.getLastName()); if (customer.getSignupDate() != null){ // optional mapOfCutomerData.put("signupDate", customer.getSignupDate()); } if (mapOfUserData != null){ mapOfCutomerData.put("user", mapOfUserData); } return mapOfCutomerData; } private Customer customer(URI uri) { ResponseEntity<CustomerResource> customerResourceResponseEntity = getRestTemplate().getForEntity(uri, CustomerResource.class); Resource<Customer> customerResource = customerResourceResponseEntity.getBody(); return unwrapCustomer(customerResource); } private static User unwrapUser(Resource<User> tResource) { User user = tResource.getContent(); user.setId(tResource.getId().getHref()); return user; } // NB: // these are simply aliases to help avoid casts in our client code public static class CustomerList extends Resources<Resource<Customer>> { } public static class UserResource extends Resource<User> { } public static class CustomerResource extends Resource<Customer> { } ... }
There’s a lot in Listing JL-12. The client extends Spring Social’s AbstractOAuth2ApiBinding
and gets a sweet restTemplate
instance pre-configured to send the OAuth access token on each request (transparently) in the deal. One less thing for us to do! The createCustomer
method demonstrates fairly typical use of the RestTemplate
. To keep things simple, the class just sends Map<String,Object>
instances to the service endpoint. These map-payloads get translated into acceptable JSON input by the RestTemplate
which then sends them to the service.
The currentUser()
method calls the REST endpoint /user
, which is the one new REST endpoint that we added when we installed Spring Security. It simply returns the user
that is currently authenticated. This service call will be useful later, when writing the Spring Social binding code.
The implementation of the setUserProfilePhoto(byte[] bytesOfImage, final MediaType mediaType)
method is interesting because it demonstrates how easy it is to do multipart file uploads. The getUserProfilePhoto()
method does the opposite — it reads the profile photo. Here again, the RestTemplate
makes short work of reading the bytes, the MediaType
, and returning that back to the client.
We need to write a few classes to teach Spring Social about the API we’re writing a client for. We need to introduce Spring Social to our API Binding through the ingenious use of three interfaces: ConnectionFactory<CrmOperations>
(shown in Listing JL-13), ServiceProvider<CrmOperations>
(shown in Listing JL-14), and ApiAdapter<CrmOperations>
(shown in Listing JL-15).
package com.jl.crm.client; import org.springframework.social.connect.*; import org.springframework.social.connect.support.OAuth2ConnectionFactory; import org.springframework.social.oauth2.*; public class CrmConnectionFactory extends OAuth2ConnectionFactory<CrmOperations> { public CrmConnectionFactory(CrmServiceProvider serviceProvider, CrmApiAdapter apiAdapter) { super("crm", serviceProvider, apiAdapter); } public CrmConnectionFactory( OAuth2ServiceProvider<CrmOperations> serviceProvider, ApiAdapter<CrmOperations> apiAdapter) { super("crm", serviceProvider, apiAdapter); } }
The ServiceProvider
implementation — CrmServiceProvider
(shown in Listing JL-14) — is where the rubber meets the road: given an accessToken
, this object needs to be able to hand back a concrete implementation of the service provider API interface (in our case, CrmTemplate
is a concrete implementation of CrmOperations
).
getApi
method is the critical part. It’s where the rubber meets the road and you get back a live API implementation.package com.jl.crm.client; import org.springframework.social.oauth2.*; public class CrmServiceProvider extends AbstractOAuth2ServiceProvider<CrmOperations> { private String baseUrl; public CrmServiceProvider( String baseUrl, String clientId, String consumerSecret, String authorizeUrl, String accessTokenUrl) { super(new OAuth2Template(clientId, consumerSecret, authorizeUrl, accessTokenUrl)); this.baseUrl = safeBaseUrl(baseUrl); } protected String safeBaseUrl(String baseUrl) { if (baseUrl.endsWith("/")){ return "" + baseUrl.subSequence(0, baseUrl.lastIndexOf("/") ); } return baseUrl; } @Override public CrmOperations getApi(String accessToken) { return new CrmTemplate(accessToken, baseUrl); } }
The ApiAdapter<CrmOperations>
implementation — CrmApiAdapter
(shown in Listing JL-15) — asks questions that most service providers can answer. Spring Social doesn’t try to provide an all-singing, all-dancing unified API for all service-providers (far from it!), but there is some overlap in terms of what operations most service-provider APIs can handle, including returning whether the connection itself is valid, returning the currently signed-in user’s profile, and updating the service-provider’s concept of a status, if it exists. On Facebook, for example, the `updateStatus
method would translate to posting something to the user’s wall and on Twitter this would translate to tweeting.
ApiAdapter
implementation can answer commands like: "is the connection live?", "what’s the current user’s general profile information?", and "update my status!".package com.jl.crm.client; import org.springframework.social.connect.*; public class CrmApiAdapter implements ApiAdapter<CrmOperations> { @Override public boolean test(CrmOperations customerServiceOperations) { return (null != customerServiceOperations.currentUser()); } @Override public void setConnectionValues(CrmOperations customerServiceOperations, ConnectionValues values) { User profile = customerServiceOperations.currentUser(); values.setProviderUserId(Long.toString(profile.getDatabaseId())); values.setDisplayName(profile.getUsername()); } @Override public UserProfile fetchUserProfile(CrmOperations customerServiceOperations) { User user = customerServiceOperations.currentUser(); String name = user.getFirstName() + ' ' + user.getLastName(); return new UserProfileBuilder() .setName(name) .setUsername(user.getUsername()) .setFirstName(user.getFirstName()) .setLastName(user.getLastName()) .build(); } @Override public void updateStatus(CrmOperations customerServiceOperations, String message) { System.out.println (String.format("calling updateStatus(CustomerServiceOperations customerServiceOperations, " + "String message) with the status '%s', but this method is a no-op!", message)); } }
We now have everyting we need to use our Spring Social binding in any application, be it a web application, a unit test, or an Android application. Listing JL-16 demonstrates the interaction with the API in a regular application. We won’t reprint the entire configuration class to run Spring Social standalone, but feel free to checkout the source code.
The program launches the system browser to the authorization URI (assumed to be running on localhost
at port 8080). You must then authenticate and confirm the permission prompts. You’ll ultimately be sent back to the /crm/welcome.html
page with an access_token
in the URI. Extract that, and then paste it into the Swing input dialog that’ll be running.
@Inject private CrmConnectionFactory crmConnectionFactory; public void exampleCrmOperationsFlow () throws Exception { // build up the OAuth2Parameters String returnToUrl = environment.getProperty("sscrm.base-url") + "crm/welcome.html"; OAuth2Template oAuth2Operations = crmConnectionFactory.getOAuthOperations(); oAuth2Operations.setUseParametersForClientAuthentication(false); OAuth2Parameters parameters = new OAuth2Parameters(); parameters.setScope("read,write"); if (StringUtils.hasText(returnToUrl)){ parameters.setRedirectUri(returnToUrl); } // figure out what the OAuth "authorize" endpoint should be and open it with // the system's default HTTP browser String authorizationUrl = oAuth2Operations.buildAuthenticateUrl(GrantType.IMPLICIT_GRANT, parameters); Desktop.getDesktop().browse(new URI(authorizationUrl)); // the authorizationUrl above will have at a minimum, prompted an authenticated user to approve // certain permissions. After the approval, it will return with an `access_token` parameter // we must provide that `access_token` here. String i = JOptionPane.showInputDialog(null, "What's the 'access_token'?"); String accessToken = i != null && !i.trim().equals("") ? i.trim() : null; // we have a live connection AccessGrant accessGrant = new AccessGrant(accessToken); connection = crmConnectionFactory.createConnection(accessGrant); UserProfile userProfile = connection.fetchUserProfile(); String userId = userProfile.getUsername(); Set<String> userIdSet = Sets.newHashSet(userId); // find out whether we've already connected before. // If not, save this connection information ConnectionRepository connectionRepository = this.usersConnectionRepository.createConnectionRepository(userId); if (usersConnectionRepository.findUserIdsConnectedTo( crmConnectionFactory.getProviderId(), userIdSet).size() == 0){ connectionRepository.addConnection(this.connection); } // Spring Social abstraction for service-provider's user profile data. UserProfile userProfile = connection.fetchUserProfile(); // retrieve implementation of CrmTemplate CrmOperations customerServiceOperations = connection.getApi(); // load the CRM User type (not Spring Social) User self = customerServiceOperations.currentUser(); // insert a customer record Customer customer = customerServiceOperations.createCustomer("Chuck", "Norris", new java.util.Date()); // read the user's profile photo ProfilePhoto profilePhoto = customerServiceOperations.getUserProfilePhoto(); // write the profile photo to the desktop File photoOutputFile = new File(new File(SystemUtils.getUserHome(), "Desktop"), "profile.jpg"); InputStream byteArrayInputStream = new ByteArrayInputStream(profilePhoto.getBytes()); OutputStream outputStream = new FileOutputStream(photoOutputFile); IOUtils.copy(byteArrayInputStream, outputStream); }
The Android classloader can be a pain in the neck because it whitelists a subset of a full JDK. Spring Android does not attempt to port all of Spring to Android. It doesn’t even attempt to port the container. Spring Android focuses on enabling delivering value for Android clients and - specifically for our application - Spring Android provides a port of the RestTemplate
available in the core Spring framework.
The Spring RestTemplate
delegates conversion between a content type and well-known object types to the HttpMessageConverter
hierarchy. The implementation that handles XML to object marshalling on Java SE delegates to JAXB. JAXB’s a safe choice with Java SE, but on Android it causes the application to be rejected because it uses classes that are not available there and thus can’t be loaded. Spring Android still supports XML REST request marshalling, but does so by delegating to a Simple XML-powered SimpleXmlHttpMessageConverter
instead.
We’re using the Spring Android core types, but want to bring in our Spring Social binding, and that in turn depends on a few libraries from standard Spring. I’ve done a fair amount of work to exclude as much as possible so that I only have my dependencies and hard runtime dependencies on my CLASSPATH
. Check out the android
module’s pom.xml
build file for details.
Spring Android does not provide a dependency injection solution on Android because - at the moment - there’s no really elegant way to do so and the jury’s still out on whether anything less is worth the effort. There are dependency injection solutions for Android. Some options support JSR 330-compliant dependency injection, including RoboGuice and Square’s Dagger.
Most dependency injection frameworks suffer from the same basic problem: Android manages the lifecycle of interesting components itself, and affords a framework no opportuity to provide instances that the framework manages instead. By the time you have a reference to one of them, they’re already constructed and alive. Dependency injection has to be done retroactively for these types of objects. For other, non-Android objects, the Dagger dependency injection container works as you’d expect given experience with JSR 330 and Spring’s Java configuration API. Our Android application, in the android
Maven module, uses Dagger, Spring Social and Spring Android to drive a CRM Android client. We’ll focus specifically on the authorization screen here.
Dagger’s pretty good. It feels natural if you’re familiar with Spring Java configuration and JSR 330. Some of the integration code that you end up having to write is pretty ugly, but arguably better than the alternative. Integrating Dagger is pretty uninteresting, so I’ll instead defer you to the BaseActivity
class, which integrates Dagger, and to our Android Application
subclass, Crm
, where Dagger is initialized.
Let’s look at our Dagger CrmModule
configuration class in Listing JL-17. This is most simialar to a Spring Java configuration class. We use it to wire up the beans required to work with Spring Social. Once Spring Social’s up and running, we’ll drive an OAuth flow inside of an Android Activity
subclass (AuthenticationActivity
).
The @Module
annotation marks this class as a class to be consulted for bean definitions, just as @Configuration
does with Spring. Because of the performance constraints, scanning packages for beans is cumbersome on Android, and best avoided. Dagger uses an injects
attribute on its modules to declare which classes beans might be injected into. The @Provides
annotation tells Dagger that the method annotated provides bean definitions, just like @Bean
in Spring. Unfortunately, in Dagger, things are not singleton by default, so we have to explicityly mark every singleton with the @Singleton
annotation.
package com.jl.crm.android; import android.content.Context; import android.location.LocationManager; import android.view.LayoutInflater; import com.jl.crm.android.activities.*; import com.jl.crm.android.utils.AndroidUiThreadUtils; import com.jl.crm.client.*; import dagger.*; import org.springframework.security.crypto.encrypt.*; import org.springframework.social.connect.sqlite.SQLiteConnectionRepository; import org.springframework.social.connect.sqlite.support.SQLiteConnectionRepositoryHelper; import org.springframework.social.connect.support.ConnectionFactoryRegistry; import javax.inject.Singleton; import static android.content.Context.LOCATION_SERVICE; @Module (injects = {UserHomeActivity.class, AuthenticationActivity.class, CustomerSearchActivity.class}) public class CrmModule { private Crm application; public CrmModule(Crm crm) { this.application = crm; } @Provides CrmOperations crmOperations(final SQLiteConnectionRepository sqLiteConnectionRepository) { try { CrmOperations ops = sqLiteConnectionRepository.getPrimaryConnection(CrmOperations.class).getApi(); return AndroidUiThreadUtils.runOffUiThread(ops, CrmOperations.class); } catch (Exception e) { throw new RuntimeException(e); } } @Provides @Singleton SQLiteConnectionRepository sqLiteConnectionRepository(SQLiteConnectionRepositoryHelper sqLiteConnectionRepositoryHelper, ConnectionFactoryRegistry connectionFactoryRegistry) { TextEncryptor textEncryptor = AndroidEncryptors.text("password", "5c0744940b5c369b"); return new SQLiteConnectionRepository(sqLiteConnectionRepositoryHelper, connectionFactoryRegistry, textEncryptor); } @Provides @Singleton ConnectionFactoryRegistry connectionFactoryRegistry() { return new ConnectionFactoryRegistry(); } @Provides @Singleton SQLiteConnectionRepositoryHelper repositoryHelper(@InjectAndroidApplicationContext Context context) { return new SQLiteConnectionRepositoryHelper(context); } @Provides @Singleton CrmApiAdapter apiAdapter() { return new CrmApiAdapter(); } @Provides @Singleton CrmServiceProvider serviceProvider(@InjectAndroidApplicationContext Context c) { String baseUrl = c.getString(R.string.base_uri); String clientId = c.getString(R.string.oauth_client_id); String clientSecret = c.getString(R.string.oauth_client_secret); String accessTokenUri = fullUrl(baseUrl, c.getString(R.string.oauth_token_uri)); String authorizeUri = fullUrl(baseUrl, c.getString(R.string.oauth_authorize_uri)); return new CrmServiceProvider(baseUrl, clientId, clientSecret, authorizeUri, accessTokenUri); } @Provides @Singleton CrmConnectionFactory crmConnectionFactory(ConnectionFactoryRegistry connectionFactoryRegistry, CrmServiceProvider crmServiceProvider, CrmApiAdapter crmApiAdapter) { CrmConnectionFactory crmConnectionFactory = new CrmConnectionFactory(crmServiceProvider, crmApiAdapter); connectionFactoryRegistry.addConnectionFactory(crmConnectionFactory); return crmConnectionFactory; } @Provides @Singleton @InjectAndroidApplicationContext Context provideApplicationContext() { return this.application.getApplicationContext(); } private String fullUrl(String baseUrl, String end) { String base = !baseUrl.endsWith("/") ? baseUrl + "/" : baseUrl; String newEnd = end.startsWith("/") ? end.substring(1) : end; return base + newEnd; } }
With these pieces in place, we can write Activity
subclasses that leverage beans we’ve configured in Dagger to support our OAuth flow.
Having an HTTP conversation using Spring Social’s OAuth support is slightly more difficult when one party can’t hear. Our client runs on a mobile device and exports no HTTP endpoints, so it’s something of a one-sided conversation as far as OAuth is concerned. We’ll drive the HTTP flow from inside of a WebView
subclass that handles the OAuth dance for us, based on URIs that we build up using Spring Social. We can’t really receive redirect requests, but we can detect them as an event in the WebView
subclass. Listing JL-18 shows the entire Android authorization screen, AuthenticationActivity
.
package com.jl.crm.android.activities; import android.content.Intent; import android.os.*; import android.view.Window; import com.jl.crm.android.R; import com.jl.crm.android.widget.CrmOAuthFlowWebView; import com.jl.crm.client.*; import org.springframework.social.connect.Connection; import org.springframework.social.connect.sqlite.SQLiteConnectionRepository; import org.springframework.social.oauth2.*; import org.springframework.util.*; import javax.inject.Inject; import java.util.List; public class AuthenticationActivity extends BaseActivity { @Inject SQLiteConnectionRepository sqLiteConnectionRepository; @Inject CrmConnectionFactory connectionFactory; CrmOAuthFlowWebView webView; CrmOAuthFlowWebView.AccessTokenReceivedListener accessTokenReceivedListener = new CrmOAuthFlowWebView.AccessTokenReceivedListener() { @Override public void accessTokenReceived(final String accessToken) { AsyncTask<?, ?, Connection<CrmOperations>> asyncTask = new AsyncTask<Object, Object, Connection<CrmOperations>>() { @Override protected Connection<CrmOperations> doInBackground(Object... params) { AccessGrant accessGrant = new AccessGrant(accessToken); Connection<CrmOperations> crmOperationsConnection = connectionFactory.createConnection(accessGrant); sqLiteConnectionRepository.addConnection(crmOperationsConnection); runOnUiThread(connectionEstablishedRunnable); return crmOperationsConnection; } }; try { asyncTask.execute(new Object[0]); } catch (Exception e) { throw new RuntimeException(e); } } }; Runnable connectionEstablishedRunnable = new Runnable() { @Override public void run() { connectionEstablished(); } }; AsyncTask<?, ?, Connection<CrmOperations>> asyncTaskToLoadCrmOperationsConnection = new AsyncTask<Object, Object, Connection<CrmOperations>>() { @Override protected Connection<CrmOperations> doInBackground(Object... params) { Connection<CrmOperations> connection = sqLiteConnectionRepository.findPrimaryConnection(CrmOperations.class); if (connection != null){ runOnUiThread(connectionEstablishedRunnable); } else { runOnUiThread(new Runnable() { @Override public void run() { webView.noAccessToken(); } }); } return null; } }; @Override public void onStart() { super.onStart(); asyncTaskToLoadCrmOperationsConnection.execute(new Object[]{}); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Window w = this.getWindow(); w.requestFeature(Window.FEATURE_PROGRESS); w.setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_VISIBILITY_ON); this.webView = webView(); setContentView(this.webView); } @SuppressWarnings("unused") protected void clearAllConnections() { MultiValueMap<String, Connection<?>> mvMapOfConnections = sqLiteConnectionRepository.findAllConnections(); for (String k : mvMapOfConnections.keySet()) { List<Connection<?>> connectionList = mvMapOfConnections.get(k); for (Connection<?> c : connectionList) { sqLiteConnectionRepository.removeConnection(c.getKey()); } } } protected void connectionEstablished() { Intent intent = new Intent(AuthenticationActivity.this, CustomerSearchActivity.class); startActivity(intent); } protected CrmOAuthFlowWebView webView() { String authenticateUri = buildAuthenticationUrl(); String returnUri = getString(R.string.oauth_access_token_callback_uri); return new CrmOAuthFlowWebView(this, authenticateUri, returnUri, this.accessTokenReceivedListener); } protected String buildAuthenticationUrl() { OAuth2Operations oAuth2Operations = this.connectionFactory.getOAuthOperations(); if (oAuth2Operations instanceof OAuth2Template){ OAuth2Template oAuth2Template = (OAuth2Template) oAuth2Operations; oAuth2Template.setUseParametersForClientAuthentication(false); } String returnUri = getString(R.string.oauth_access_token_callback_uri); OAuth2Parameters oAuth2Parameters = new OAuth2Parameters(); oAuth2Parameters.setScope("read,write"); if (StringUtils.hasText(returnUri)){ oAuth2Parameters.setRedirectUri(returnUri); } return oAuth2Operations.buildAuthenticateUrl(GrantType.IMPLICIT_GRANT, oAuth2Parameters); } }
This code in Listing JL-18 is not easy to follow, at first blush, because there’s so much code related to running certain parts of the program on Android’s main thread, vs. Android’s UI thread. The program flow tends to get lost in the callback soup. The basic flow is as follows:
-
When the
Activity
starts, it installs a customWebView
subclass widget that handles the OAuth-dance for us. TheActivity
checks to see if there are already any Spring Social connections established in the local SQLite-poweredConnectionRepository
implementation. -
If there are existing connections, then it simply re-instantiates the connection. If there are no existing connections, it triggers the
WebView
subclass to begin the OAuth flow. -
In either case, flow eventually lands at the
connectionEstablished()
method, where we launch a newActivity
which may do what it pleases with the Dagger-injectedCrmOperations crmOperations
field which will be created per-request, and thus always reflect the latest connection.
Figure JL-6 shows the Android signin screen, delegating directly to the view rendered from the web application.
Once signed in, the user must approve the requested permissions, as shown in figure JL-7.
The whole thing culminates in connectionEstablished()
which launches another Activity
where we may inject a reference to our CrmOperations
instance, secure in the knowledge that our connection is already setup and working.
We’ve introduced Spring’s powerful support for building REST services, employing Spring HATEOAS to elevate make our APIs easier for clients to use. We used Spring Data REST to remove the boilerplate, and we used Spring’s REST Shell to easily interact with our service. Finally, we’ve introduced security for both human and automatic clients using Spring Security and Spring Security OAuth, respectively. We built out a client using Spring Social that makes short work of consuming our OAuth-secured REST services. Then, thanks to the magic of Spring Android, we reused all that functionality in an Android application.
There’s a lot more to Spring’s rich web application features. If you’re done reading this and are itching for more, then check out some of these other areas of Spring’s web application support.
-
Beyond REST, Spring MVC supports web-sockets for highly event-driven applications.
-
Spring MVC supports Servlet 3-powered asynchronous controllers.
-
The sister project Reactor provides a highly concurrent, asynchronous-IO focused architecture for building web applications. I’d definitely take a look at this!
-
The sister project Cujo.js aims to provide for JavaScript what Spring does for Java. The project provides several useful libraries.
wire.js
, for example, is used to wire together the application, on top of popular JavaScript module systems.rest.js
provides aRestTemplate
-like client (complete with support for HTTP Basic and OAuth-based connectivity!) for JavaScript clients. There’s a lot of great stuff here. -
Longtime users of Spring will know that Spring supports multiple types of configuration, and that I’ve used all Java-based configuration in both of these installments. I think Java configuration is very readable, but there’s still more that we can do. The recently launched Spring Boot project takes that step for us by introducing opinionated setup, helping to remove all boilerplate interfering with your application, including the setup of a web server itself! Definitely watch this space!
Josh Long is the Spring developer advocate at SpringSource, by Pivotal. Josh is the lead author on Apress’ Spring Recipes, 2nd Edition, O’Reilly’s Spring Roo, and a SpringSource open-source project committer and contributor. When he’s not hacking on code, he can be found at the local Java User Group or at the local coffee shop. Josh likes solutions that push the boundaries of the technologies that enable them. Continue the discussion on his personal blog, the SpringSource Blog and on Twitter (@starbuxman).