All Articles

Creation of RESTful Web Service With Grails and Spring Boot

Posted 2017-02-09 12:02

26 minutes to read

Sergey Vasilenko
Java Team at Rozdoum

Need to create a web service for a mobile application? Rozdoum mobile developers offer this highly informative tutorial that will help you to create RESTful web service from scratch. The detailed description and code samples of two simple solutions for Grails and Spring Boot are provided in the article.

Intro

Nowadays mobile devices are an integral part of everyday human life. Instant messaging, playing media content, emailing, video calls, social networking, Internet surfing, games — you name it. Some of these features require an Internet connection (basically, a server is somewhere out of there) others are self-sufficient. In this post, we’d like to touch the first category.

Why would an application need a server? Here are some of the reasons:

  • to bring people together (online games, social networks, etc.)
  • to provide data in a centralized way (e.g. client bank applications providing the currency rate info)
  • to extend existing web experience (online shops give their users the possibility to access the shop showcase without needing a PC at hand).

How does this communication work? The easiest solution is the creation of a RESTful web service — a web service which provides the means to operate textual representations of data in a stateless fashion. In other words, a web service capable of performing CRUD (create | read | update | delete) operations on data using JSON/ XML format.

There are numerous frameworks which allow creating web services easily: Grails, Play Framework, Rails, Spring Boot, etc.

In this post we’ll describe writing web services using Grails and Spring Boot in greater details.

For the demo purposes we’re going to make a simple secured RESTful web service.

Grails

What is Grails?

Grails is a Groovy-based web framework which allows rapid development and deployment of production-grade applications. It combines different Java frameworks under its hood such as Spring MVC, Hibernate, Ehcache, Gradle/ Maven/ Ivy as a dependency management system.

Grails is shipped in 2 variations:

  • Grails 2 uses Maven as a dependency management tool
  • Grails 3 uses Gradle as a dependency management tool and is built on the top of Spring Boot.

Currently, both are being still developed (Grails 3 updates are released more often).

Grails provides the possibility to use 3rd party plugins in the applications which drastically help to solve different tasks (add and configure security, and send emails, etc.). The mentioned plugins can be found at the Grails Plugins Portal.

The fact that Grails does the majority of the configuration by itself and provides a vast amount of plugins – this greatly speeds up the development.

Grails solution

We’re going to use Spring Security REST — a Grails plugin for securing our application. At the moment, the plugin author provides a released 1.5.4 version of the plugin which is compatible only with the Grails 2.0+ and a milestone version 2.0.0.M2 which is compatible with Grails 3.0+ as well.

In our demo, we’re going to use Grails 3 as it’s newer, although it forces us to use milestone version of the Spring Security REST plugin, the steps to achieve the same results in Grails 2 will be similar. The biggest difference would be in the way plugins are enabled — Grails 2 uses BuildConfig.groovy, Grails 3 uses build.gradle.

Here are the steps we’re going to take in order to make a small application providing two REST API endpoints — secure and insecure:

  1. Create an application. By default grails uses h2 as the database for all applications.
$ grails create-app servergrails
  1. Add the line into the dependencies section of the <project-dir>/build.gradle:
compile "org.grails.plugins:spring-security-rest:2.0.0.M2"
  1. Create a controller by invoking the following command inside the project directory.
    $ grails create-controller org.test.mserver.controller.Index
    | Created grails-app/controllers/org/test/mserver/controller/IndexController.groovy
    | Created src/test/groovy/org/test/mserver/controller/IndexControllerSpec.groovy

    This’ll create a controller with an empty index() method.

  2. Create a class inside <project-dir>src/main/groovy/org/test/mserver/bean (these directories need to be created as well) called Dummy with the following content:
    package org.test.mserver.bean class Dummy { String msg } 
  3. Add a new method to the IndexController.
    @Secured("ROLE_ANONYMOUS")
     def insecure() {
            render new Dummy(msg: "hi there") as JSON
      }

    This method will create an instance of our class and convert it into JSON. This method is accessible to unauthenticated user because of the annotation @Secured(“ROLE_ANONYMOUS”).

  4. Start a server and send a GET request to the http://localhost:8080/index/insecure using curl, Postman, PowerShell or any other tool of your choice.
  5. The next step: we’re going to finish setting up spring security plugin (specify which classes it should use for authorization purposes).
    $ grails s2-quickstart org.test.mserver.model User Role
    | Creating User class 'User' and Role class 'Role' in package 'org.test.mserver.model'
    | Rendered template Person.groovy.template to destination grails-app\domain\org\test\mserver\model\User.groovy
    | Rendered template Authority.groovy.template to destination grails-app\domain\org\test\mserver\model\Role.groovy
    | Rendered template PersonAuthority.groovy.template to destination grails-app\domain\org\test\mserver\model\UserRole.groovy
    |
    ************************************************************
    * Created security-related domain classes. Your *
    * grails-app/conf/application.groovy has been updated with *
    * the class names of the configured domain classes; *
    * please verify that the values are correct. *
    ************************************************************

    This will create 3 classes inside org.test.mserver.model package: for a user, for a role and for a user-role mapping.

  6. Create a sample user using BootStrap groovy-class (<project-dir>/grails-app/init/servergrails/BootStrap.groovy).
    package servergrails import org.test.mserver.model.Role import org.test.mserver.model.User import org.test.mserver.model.UserRole class BootStrap { def init = { servletContext -&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt; def roleUser = new Role('ROLE_USER').save() def user = new User("user", "user").save() UserRole.create user, roleUser } def destroy = { } }
  7. Add a new method to the IndexController.
    @Secured("ROLE_USER")
    def secure() {
    render new Dummy(msg: "hi there, ${springSecurityService.currentUser.username}" ) as JSON
    }

    Annotation @Secured(“ROLE_USER”) ensures that method is accessible only to logged-in users with role ROLE_USER

  8. Divide security configuration to make RESTful and browser requests be processed differently.

a) Add a new URL mapping into UrlMappings.groovy (should be located at <project-dir>\grails-app\controllers\servergrails\UrlMappings.groovy). This’ll make all controller actions being accessible by URLs starting with /api.

"/api/$controller/$action?/$id?(.$format)?" {
            constraints {
                // apply constraints here
            }
        }

b) Add a new filterchain configuration into application.groovy (<project-dir>\grails-app\conf\application.groovy).

grails.plugin.springsecurity.filterChain.chainMap = [
		//Stateless chain
		[
				pattern: '/api/**',
				filters: 'JOINED_FILTERS,-anonymousAuthenticationFilter,-exceptionTranslationFilter,-authenticationProcessingFilter,-securityContextPersistenceFilter,-rememberMeAuthenticationFilter'
		],

		//Traditional chain
		[
				pattern: '/**',
				filters: 'JOINED_FILTERS,-restTokenValidationFilter,-restExceptionTranslationFilter'
		]
]

c) Invoke a GET request via REST chain to URL http://localhost:8080/api/index/secure and you’ll get a “401 Unauthorized” response with JSON in a response body.

d) Invoke a GET request via traditional chain to URL http://localhost:8080/index/secure (no /api/ in url) and you’ll get a “200 OK” response with an HTML code for the login form.

And that’s basically it — our server is ready to use.

At this point we have:

  • 2 different security chains: stateful (http session contains info about the logged-in user) and stateless (we’ll need to provide authentication token each time we send requests to the secured endpoints)
  • Index controller with 2 methods: secure() and insecure(): the first one requires authentication, the second can be accessed without access_token
  • An automatically configured h2 database.

To access secured endpoints, you’ll need to provide an access token. Here’s the workflow:

1. You get access token by sending POST here: http://localhost:8080/api/login with JSON.

{
"username": "user",
"password": "user"
}

And you should get the similar response:

{
  "username": "user",
  "roles": [
    "ROLE_USER"
  ],
  "token_type": "Bearer",
"access_token":  "eyJhbGciOiJIUzI1NiJ9.eyJwcmluY2lwYWwiOiJINHNJQUFBQUFBQUFBSlZTc1U0Y01SQ2RQUTRCUWtvQUNTUUthQ0JkdENlRjhpcEFCeEphQWVLNEJxUkV2dDFoTVhqdHhmYkNYWU91Z29JQ1JJS0VrbFwvZ1Q2REpCMFNrU0V1ZE51T0ZZdzhhRkZmMitQbTlOMjk4OHdEOVJzTmN",
"expires_in": 3600,
"refresh_token": "eyJhbGciOiJIUzI1NiJ9.eyJwcmluY2lwYWwiOiJINHNJQUFBQUFBQUFBSlZTc1U0Y01SQ2RQUTRCUWtvQUNTUUthQ0JkdENlRjhpcEFCeEphQWVLNEJxUkV2dDFoTVhqdHhmYkNYWU91"
}

access_token is used to provide user data to secure endpoints,

refresh_token is used to request a new access_token if current one is expired

2. You send the request to a secure endpoint (e.g. http://localhost:8080/api/index/secure) and provide a header.

 Name  Value
 Authorization Bearer <access_token>

(<access_token> should be replaced with the value retrieved in the first step).

To refresh an expired access token, you simply need to send a POST request to the URL http://localhost:8080/oauth/access_token composed in the following way:

  • Add a header:
 Name  Value
 Content-Type application/x-www-form-urlencoded
  • Add 2 body parameters:
 Name  Value
 grant_type  refresh_token
 refresh_token  <refresh_token>

(<refresh_token> should be replaced with the value retrieved in the first step).

The code for this example is available here.

Spring Boot

What is Spring Boot and why should you care?

Back in the day whenever developer needed to create an application using Spring, it took an enormous amount of time to configure the project (especially if the app developer is new to this). Framework developers were aware of this issue and had come up with a solution — Spring Boot.

This project bears the burden of the majority of configuration process on its shoulders. It uses libraries present in the classpaths as hints of what and how should work. If the app developer is not happy with this configuration, it’s fairly easy to customize it up to the project needs — here.

Implementing a simple web application takes as much effort as adding some dependencies to the pom.xml/build.gradle and creating an entry point in the form of public static void main() method inside the app’s main class.

There are some tools which can help with building a stub project:

Initializr has a user-friendly UI which allows developers to specify the nature of their application as well as necessary dependencies.

CLI solution allows initialization of the project from the terminal, providing required parameters (unfortunately, there’s no way to see available dependencies using this solution — the developer needs to know them).

Both of these tools will produce an archive with a stub for the application.

Spring Boot solution

You can find all the code here.

The whole development process can be divided into 4 stages/steps — Initialization, Tests, Web UI, and Spring Data. Each stage has its own git tag. To shorten this section, we won’t provide the detailed explanation of the code for each step, just a short description of what’s done and what for. The end result will be explained in greater details later.

Step 0. Initialization

The goals of this step are:

  • Generate a gradle-based project stub with WEB and SECURITY dependencies (we’ve used https://start.spring.io).
  • Make the application hot deployable by providing spring-boot-devtools gradle dependency, i.e. make changes apply without need to restart our app.
  • Implement an insecure endpoint (controller + security config) of our application which would return a JSON representation of a custom bean.
  • Use OAuth2 authorization server configuration provided by spring-oauth2 by adding @EnableAuthorizationServer (org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer) an annotation to a spring boot application class.

Step 1. Tests

The goals of this step are:

  • Update security configuration. Introduce a new class for OAuth2 security configuration. OAuth2 server consists of a resource server and an authorization server. The resource server hosts the protected data, and the authorization server verifies the identity of the user and then issues access tokens to the application.
  • Change the implementation of the secured URL (/api/test/secured) to make it return XML instead of JSON.
  • Implement tests. Our implementation can be found here. These tests check both controller endpoints for unauthorized access (/api/test/insecure succeeds, /api/test/secured — fails) and perform full authentication process for the /api/test/secured.

Step 2. Web UI

The goals of this step are:

  • Provide a custom implementation of OAuth2 authorization server to allow several client configurations, e.g. a client with the secret key, a client without the secret key, etc.
  • Add Thymeleaf as a server-side web template engine.
  • Render a simple index page.
  • Divide security into 2 separate chains: the RESTful chain (OAuth2Config) and the web chain (SecurityConfig).
  • Provide 2 URLs for web pages: secure and insecure (similar to what we have with a RESTful approach).
  • Create a login page.

Step 3. Spring Data

The goals of this step are:

  • Implement persistence layer for the application using Spring Data and Hibernate JPA.
  • Add app users to the database on application startup.
  • Separate persistence configuration based on spring profiles (dev, production).
  • Change security configuration to use database users for authentication/ authorization instead of hardcoded ones.
  • Implement a service layer for user operations (a wrapper for spring data repository).
  • Implement a simple user search page which uses spring data repository through service for querying db.

Detailed explanation

In this section, you’ll find an explanation of the classes of the final version of the application as well as their source code. The full application source code is available here.

  • Member class is a simple bean to demonstrate data transfer between the client and the server.
package com.painsomnia.mobileserver.bean;
public class Member {
    private String username;
    private String password;

    //constructors
    //getters
    //setters
}
  • AppBootstrap class is a component which performs operations on startup (in our case — user creation).
package com.painsomnia.mobileserver.bootstrap;

import com.painsomnia.mobileserver.constant.Authority;
import com.painsomnia.mobileserver.model.User;
import com.painsomnia.mobileserver.model.UserProfile;
import com.painsomnia.mobileserver.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
public class AppBootstrap {

    public static final int INITIAL_USER_COUNT = 20;
    private final UserService userService;

    @Autowired
    public AppBootstrap(UserService userService) {
        this.userService = userService;
    }

    @PostConstruct
    public void init() {
        createAdmin();
        if (userService.count() &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt; INITIAL_USER_COUNT) {
            createMultipleUsers();
        }
    }

    private void createAdmin() {
        if (!userService.exists(User.ADMIN)) {
            final User user = new User();
            user.setUsername(User.ADMIN);
            user.setEmailAddress("admin@admin.com");
            user.setPassword("admin");
            user.addAuthority(Authority.ADMIN);

            final UserProfile userProfile = new UserProfile();
            userProfile.setLastName(User.ADMIN);
            userProfile.setFirstName(User.ADMIN);
            userProfile.setPhoneNumber("123");
            userProfile.setCompany("company");
            user.setUserProfile(userProfile);
            userProfile.setUser(user);

            userService.create(user);
        }
    }

    private void createMultipleUsers() {
        for (int i = 0; i &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt; INITIAL_USER_COUNT; i++) {
            createTestUser(i);
        }
    }

    private void createTestUser(int i) {
        final User user = new User();
        final String username = "user_" + i;
        user.setUsername(username);
        user.setEmailAddress(username + "@admin.com");
        user.setPassword("admin");

        final UserProfile userProfile = new UserProfile();
        userProfile.setFirstName("first_" + username);
        userProfile.setLastName("last_" + username);
        userProfile.setPhoneNumber("123");
        userProfile.setCompany("company");
        user.setUserProfile(userProfile);

        userService.create(user);
    }
}
  • OAuth2Config class contains configuration for OAuth2 server components — the resource server and the authorization server.
package com.painsomnia.mobileserver.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

@Configuration
public class OAuth2Config {

    @Configuration
    @EnableResourceServer
    static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http
                    .requestMatchers()
                    .antMatchers("/api/**")
                    .and()
                    .authorizeRequests()
                    .antMatchers("/api/test/insecure").permitAll()
                    .anyRequest().authenticated();
        }
    }

    @Configuration
    @EnableAuthorizationServer
    protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

        private final TokenStore tokenStore;

        private final AuthenticationManager authenticationManager;

        @Autowired
        public AuthorizationServerConfiguration(TokenStore tokenStore, AuthenticationManager authenticationManager) {
            this.tokenStore = tokenStore;
            this.authenticationManager = authenticationManager;
        }

        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

            clients
                    .inMemory()
                    .withClient("client")
                    .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
                    .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
                    .scopes("read", "write", "trust")
                    .accessTokenValiditySeconds(60)
                    .and()
                    .withClient("client-with-secret")
                    .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
                    .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
                    .scopes("read", "write", "trust")
                    .secret("secret");
                }

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints
                    .tokenStore(tokenStore)
                    .authenticationManager(authenticationManager);
        }

        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
            oauthServer.realm("mserver/client");
        }

    }

    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

}

ResourceConfiguration here specifies that it handles URLs starting with /api/ and all URLs except /api/test/insecure require authentication.

AuthorizationServerConfiguration configures 2 in-memory OAuth2 clients: with and without the secret key.

  • PersistenceConfigDev and PersistenceConfigProduction provide persistence configuration based on active spring profile. dev uses in-memory db, production uses in-memory db with file system persistence.
package com.painsomnia.mobileserver.config;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

import javax.sql.DataSource;

@Configuration
@Profile("production")
public class PersistenceConfigProduction {

    @Bean
    public DataSource dataSource() {
        return DataSourceBuilder
                .create()
                .url("jdbc:hsqldb:file:${user.home}/.mobile-server/db/mserver")
                .username("sa")
                .password("sa")
                .build();
    }
}
  • SecurityConfig provides security configuration for the web filterchain. We’re going to use the database as a source of users. All passwords will be encrypted with bcrypt. Index and login page will be available to all users.
package com.painsomnia.mobileserver.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.sql.DataSource;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final DataSource dataSource;

    @Autowired
    public SecurityConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Autowired
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .jdbcAuthentication()
                .dataSource(this.dataSource)
                .passwordEncoder(this.passwordEncoder());
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/webjars*//**", "/images*//**", "/oauth/uncache_approvals", "/oauth/cache_approvals");
    }

    @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .requestMatchers().antMatchers("/ui/**", "/login")
                    .and()
                    .authorizeRequests()
                    .antMatchers("/ui/index").permitAll()
                    .antMatchers("/ui").permitAll()
                    .antMatchers("/login").permitAll()
                    .anyRequest().authenticated()
                    .and().formLogin().loginPage("/ui/login").failureUrl("/ui/login?error").permitAll()
                    .and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/ui/logout")).logoutSuccessUrl("/ui").permitAll()
        ;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  • TestController is used for REST API and it listens to /api/test/insecure (JSON) and /api/test/secured (XML) URLs.
package com.painsomnia.mobileserver.controller.rest;

import com.painsomnia.mobileserver.bean.Member;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/test")
public class TestController {

    @RequestMapping(value = "/insecure", produces = {MediaType.APPLICATION_JSON_VALUE})
    public Object index() {
        return new Member("zxc", "pwd");
    }

    @RequestMapping(value = "/secured", produces = {MediaType.APPLICATION_XML_VALUE})
    public Object indexSecured() {
        return new Member("sec", "ured");
    }
}
  • IndexController is used for rendering web pages for the user.
package com.painsomnia.mobileserver.controller.ui;

import com.painsomnia.mobileserver.model.User;
import com.painsomnia.mobileserver.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

/**
* Author: Sergey Vasilenko
* Created: 14.10.16.
*/
@Controller
@RequestMapping("/ui")
public class IndexController {

    private final UserService userService;

    @Autowired
    public IndexController(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping(value = {"", "/index"})
    public String index() {
        return "index";
    }

    @RequestMapping("/secured")
    public String secured() {
        return "secured";
    }

    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String login() {
        return "login";
    }

    @RequestMapping("/users")
    public String users(@RequestParam(name = "q", required = false) String query, ModelMap modelMap) {
        final Iterable users;
        if (query != null) {
users = userService.searchUsers(query);
} else {
            users = userService.findAll();
        }
        modelMap.addAttribute("users", users);
        return "users";
    }
}
  • User and UserProfile classes are entity beans, and they are used for storing the user info.
package com.painsomnia.mobileserver.model;

import com.painsomnia.mobileserver.constant.Authority;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "users")
public class User {

    public static final String ADMIN = "admin";
    @Id
    private String username;

    @Column(nullable = false, unique = true)
    private String emailAddress;

    @Column(nullable = false)
    private String password;

    @Column
    private boolean enabled;

    @ElementCollection
    @CollectionTable(name = "Authorities", joinColumns = @JoinColumn(name = "username"))
    @Column(name = "authority")
    private Set authorities;

    @OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
    private UserProfile userProfile;
            //getters and setters
}
package com.painsomnia.mobileserver.model;

import javax.persistence.*;

@Entity
public class UserProfile {

    @Id
    private String username;

    @Column(nullable = false)
    private String firstName;

    @Column(nullable = false)
    private String lastName;

    @Column(nullable = false)
    private String phoneNumber;

    @Column(nullable = false)
    private String company;

    @OneToOne
    @PrimaryKeyJoinColumn
    private User user;

    //getters and setters
}
  • UserRepository is an interface which provides CRUD operations (thanks to its parent CrudRepository) as well as a method to find objects by a complicated query. The implementation of this interface is created at runtime by Spring Data.
package com.painsomnia.mobileserver.persistence;

import com.painsomnia.mobileserver.constant.RepositoryQuery;
import com.painsomnia.mobileserver.model.User;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface UserRepository extends CrudRepository&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;User, String&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt; {
    @Query(value = "select u " + RepositoryQuery.SEARCH_BY_MASK_QUERY,
            countQuery = "select count(u) " + RepositoryQuery.SEARCH_BY_MASK_QUERY)
    List searchUsers(@Param("searchTerm") String searchTerm);
}
package com.painsomnia.mobileserver.constant;

public class RepositoryQuery {
    public static final String SEARCH_BY_MASK_QUERY = "from User u " +
            "join u.userProfile p " +
            "where lower(u.username) like lower(:searchTerm) or " +
            "lower(p.firstName) like lower(:searchTerm) or " +
            "lower(p.lastName) like lower(:searchTerm) or " +
            "lower(u.emailAddress) like lower(:searchTerm)";

    private RepositoryQuery(){}
}
  • UserServiceImpl and its interface UserService act as a wrapper for UserRepository and responsible for operations on User entity.
package com.painsomnia.mobileserver.service.impl;

import com.painsomnia.mobileserver.constant.Authority;
import com.painsomnia.mobileserver.model.User;
import com.painsomnia.mobileserver.model.UserProfile;
import com.painsomnia.mobileserver.persistence.UserRepository;
import com.painsomnia.mobileserver.service.UserService;
import org.hibernate.NonUniqueObjectException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    @Autowired
    public UserServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public Iterable findAll() {
        return userRepository.findAll();
    }

    @Override
    public User get(String username) {
        return userRepository.findOne(username);
    }

    @Override
    public User create(User user, Authority... authorities) {
        final String username = user.getUsername();
        if (exists(username)) {
            throw new NonUniqueObjectException("User with the given username already exists", username, User.class.getName());
        }

        //just in case set bidirectional fields
        final UserProfile userProfile = user.getUserProfile();
        userProfile.setUsername(username);
        userProfile.setUser(user);

        user.setEnabled(true);
        final String rawPassword = user.getPassword();
        encodeUserPassword(user, rawPassword);
        user.addAuthority(Authority.USER);
        for (Authority authority : authorities) {
            user.addAuthority(authority);
        }
        return userRepository.save(user);
    }

    @Override
    public User update(User user) {
        return userRepository.save(user);
    }

    @Override
    public boolean exists(String username) {
        return userRepository.exists(username);
    }

    @Override
    public long count() {
        return userRepository.count();
    }

    @Override
    public List searchUsers(String searchTerm) {
        return userRepository.searchUsers("%" + searchTerm + "%");
    }

    private void encodeUserPassword(User user, String rawPassword) {
        final String encodedPassword = passwordEncoder.encode(rawPassword);
        user.setPassword(encodedPassword);
    }
}
  • SpringSecurityMobileApplicationTests contains a few integration tests for our app.
package com.painsomnia;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.web.context.WebApplicationContext;

import java.util.Map;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SpringSecurityMobileApplicationTests {

    @Autowired
    WebApplicationContext context;

    @Autowired
    FilterChainProxy filterChain;

    private final ObjectMapper objectMapper = new ObjectMapper();

    private MockMvc mvc;

    @Before
    public void setUp() {
        this.mvc = webAppContextSetup(this.context).addFilters(this.filterChain).build();
        SecurityContextHolder.clearContext();
    }

    @Test
    public void testUnauthorizedAccess() throws Exception {
        this.mvc.perform(get("/api/test/insecure").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk()).andDo(print());

        this.mvc.perform(get("/api/test/secured").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isUnauthorized()).andDo(print());
    }

    @Test
    public void testSecuredAccess() throws Exception {
        String header = "Basic " + new String(Base64.encode("client-with-secret:secret".getBytes()));
        MvcResult result = this.mvc
                .perform(post("/oauth/token")
                        .header("Authorization", header)
                        .param("grant_type", "password")
                        .param("scope", "read")
                        .param("username", "admin")
                        .param("password", "admin"))
                .andExpect(status().isOk()).andDo(print()).andReturn();
        Object accessToken = this.objectMapper
                .readValue(result.getResponse().getContentAsString(), Map.class)
                .get("access_token");

        this.mvc.perform(get("/api/test/secured")
                .accept(MediaType.APPLICATION_XML)
                .header("Authorization", "Bearer " + accessToken))
                .andExpect(header().string("Content-Type",
                        MediaType.APPLICATION_XML_VALUE + ";charset=UTF-8"))
                .andExpect(status().isOk()).andDo(print()).andReturn();
    }

    @Test
    public void testRefreshToken() throws Exception {
        String header = "Basic " + new String(Base64.encode("client-with-secret:secret".getBytes()));
        MvcResult result = this.mvc
                .perform(post("/oauth/token")
                        .header("Authorization", header)
                        .param("grant_type", "password")
                        .param("scope", "read")
                        .param("username", "admin")
                        .param("password", "admin"))
                .andExpect(status().isOk()).andDo(print()).andReturn();
        Object refreshToken = this.objectMapper
                .readValue(result.getResponse().getContentAsString(), Map.class)
                .get("refresh_token");

        this.mvc
                .perform(post("/oauth/token")
                        .header("Authorization", header)
                        .param("grant_type", "refresh_token")
                        .param("refresh_token", (String) refreshToken))
                .andExpect(status().isOk()).andDo(print()).andReturn();
    }

}

Conclusion

In the sections above, we’ve examined 2 different approaches to a RESTful web service creation.

Which one should you choose? There’s no straightforward answer to this question as you’ll need to consider multiple variables: time to implement the solution, the necessity to have a modularized solution, the scale of the project, etc.:

  • If you need a small application and there are no plans for its extension — Grails would be your safest choice.
  • If you need a distributed application, non-interconnected components of which all depend on a single component (e.g. there are 2 web applications depending on a single service layer) — it’ll be easier to achieve this by using Spring.

It doesn’t mean that you cannot use Grails in complicated distributed applications which require modularization (you might need to develop Grails plugins for such modules) or use Spring for small applications. You can.

In fact, we strongly advise you to check different technologies, and discover which ones work better for you in different circumstances.

Take care, and create your app.



Sergey Vasilenko
Java Team at Rozdoum