odrotbohm #342 - Expanded copyright headers to 2018.
Removed trailing whitespace in touched files.
Latest commit d854c6d Feb 22, 2018

README.adoc

Spring Data REST + Spring Security

This example shows how to secure a Spring Data REST application in multiple ways with Spring Security.

Defining the domain

For a basic Spring Data REST application, we need to define some domain objects. In this case, we have a company. The company has both employees and items it manages. The domain objects are declared as POJOs with JPA annotations.

Example 1. src/main/java/example/company/Employee.java
@Entity
public class Employee {

  private @Id @GeneratedValue Long id;
  private String firstName;
  private String lastName;
  private String title;

  …
}
Example 2. src/main/java/example/company/Item.java
@Entity
public class Item {

  private @Id @GeneratedValue Long id;
  private String description;

  …
}

Defining the repositories

Spring Data is based on the repository paradigm. In this case, we are defining a repository for each of these domain objects.

Example 3. src/main/java/example/company/EmployeeRepository.java
public interface EmployeeRepository extends CrudRepository<Employee, Long> {}

EmployeeRepository is about as simple as things can get. It extends Spring Data Commons' CrudRepository. This means that the repo doesn’t HAVE to be tied to JPA. If the underlying Employee object was retooled for MongoDB, this repository definition would require no changes.

Now let’s look at the next repository:

Example 4. src/main/java/example/company/ItemRepository.java
@PreAuthorize("hasRole('ROLE_USER')")
public interface ItemRepository extends CrudRepository<Item, Long>  {

  @PreAuthorize("hasRole('ROLE_ADMIN')")
  @Override
  Item save(Item s);

  @PreAuthorize("hasRole('ROLE_ADMIN')")
  @Override
  void delete(Long aLong);
}

This repository is simple in its functionality, but it has been marked up with Spring Security annotations (@PreAuthorize). The repository at the top level requires that the user have ROLE_USER before ANYTHING is granted.

Note
These code examples use Spring Security’s more modern @PreAuthorize annotations. But you can also use @Secured or JSR-250’s security annotations. (Be advised, that JSR-250 annotations do NOT work at the interface level.)

To fine tune security policies, save(Item) and delete(Item) are overrides of CrudRepository, allowing us to further restrict these operations to require ROLE_ADMIN.

Note
This requires either Spring Security 3.2.6.RELEASE or 4.0.0.RELEASE (or higher).

Writing a security policy

The final bit that is needed is a security policy. By default, when using Spring Boot, everything is locked down and a random password is generated. Usually, you will want to replace this with a user store of some kind. In addition to that, you need to configure method-level security. To top it off, it is also possible to secure Spring Data REST endpoints at the URL level. All of this is shown below:

Example 5. src/main/java/example/company/SecurityConfiguration.java
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

  /**
   * This section defines the user accounts which can be used for
   * authentication as well as the roles each user has.
   */
  @Override
  public void configure(AuthenticationManagerBuilder auth) throws Exception {

    auth.inMemoryAuthentication()
      .withUser("greg").password("turnquist").roles("USER").and()
      .withUser("ollie").password("gierke").roles("USER", "ADMIN");
  }

  /**
   * This section defines the security policy for the app.
   * - BASIC authentication is supported (enough for this REST-based demo)
   * - /employees is secured using URL security shown below
   * - CSRF headers are disabled since we are only testing the REST interface,
   *   not a web one.
   *
   * NOTE: GET is not shown which defaults to permitted.
   */
  @Override
  protected void configure(HttpSecurity http) throws Exception {

    http
      .httpBasic().and()
      .authorizeRequests()
        .antMatchers(HttpMethod.POST, "/employees").hasRole("ADMIN")
        .antMatchers(HttpMethod.PUT, "/employees/**").hasRole("ADMIN")
        .antMatchers(HttpMethod.PATCH, "/employees/**").hasRole("ADMIN").and()
      .csrf().disable();
  }
}

The top section shows the user accounts defined in the app.

The second section shows the URL restrictions that have been applied. Note that GET /employees has no restrictions at all. The other operations require ROLE_ADMIN.

Testing things out

You can drill down into Application.java to find the data that is preloaded.

  1. Run the app.

    $ mvn spring-boot:run
  2. In another shell, look up the list of employees:

    $ curl localhost:8080/employees
    {
      "_embedded" : {
        "employees" : [ {
          "firstName" : "Bilbo",
          "lastName" : "Baggins",
          "title" : "thief",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/employees/1"
            }
          }
        }, {
    ...

    No security required!

  3. Try to POST with no credentials.

    $ curl -X POST -d '{"firstName": "Saruman", "lastName": "the evil one", "title": "the White"}' localhost:8080/employees
    {"timestamp":1412958386366,"status":401,"error":"Unauthorized","message":"Full authentication is required to access this resource","path":"/employees"}

    You are denied due a lack of authentication, i.e. confirming who you are.

  4. Try to POST with USER level credentials.

    $ curl -X POST -d '{"firstName": "Saruman", "lastName": "the evil one", "title": "the White"}' localhost:8080/employees -u greg:turnquist
    {"timestamp":1412958491870,"status":403,"error":"Forbidden","message":"Access is denied","path":"/employees"}

    You are now denied due to not having sufficient authorization.

  5. Try to POST with ADMIN level credentials.

    $ curl -i -X POST -d '{"firstName": "Saruman", "lastName": "the evil one", "title": "the White"}' -H "Content-Type: application/json" localhost:8080/employees -u ollie:gierke
    HTTP/1.1 201 Created
    Server: Apache-Coyote/1.1
    X-Content-Type-Options: nosniff
    X-XSS-Protection: 1; mode=block
    Cache-Control: no-cache, no-store, max-age=0, must-revalidate
    Pragma: no-cache
    Expires: 0
    X-Frame-Options: DENY
    Set-Cookie: JSESSIONID=D738A5C8E5EACF6C118F8452A8C98919; Path=/; HttpOnly
    Location: http://localhost:8080/employees/4
    Content-Length: 0

    Finally you have managed to create a new entry as shown by the Location header. You can also read about these various security-based headers that Spring Security adds by default and what extra protections they add.

  6. Now, try to fetch the list of items.

    $ curl localhost:8080/items
    {"timestamp":1412958853221,"status":401,"error":"Unauthorized","exception":"org.springframework.security.access.AccessDeniedException","message":"Access is denied","path":"/items"}

    This fails at the get go because the entire repository is secured. Only with a USER level or higher can you see anything.

  7. Try to fetch the list of items with USER level credentials.

    $ curl localhost:8080/items -u greg:turnquist
    {
      "_embedded" : {
        "items" : [ {
          "description" : "Sting",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/items/1"
            }
          }
        }, {
          "description" : "the one ring",
          "_links" : {
            "self" : {
              "href" : "http://localhost:8080/items/2"
            }
          }
        } ]
      }
    }

From here on, you can experiment with this sample application:

  • Try to perform various operations with the accounts like fetching, creating, updating, replacing, and deleting through the REST API.

  • Inject the repositories inside some other code and use it there.

  • Write your own custom controller and export either repository your own way. Find out what security controls are carried through by default and what ones you have to add.

  • Finally, fiddle with the roles and permissions and change the security settings.