Learn to create JAX-RS 2.0 REST APIs using Spring Boot and Jersey framework, and add role based security using JAX-RS annotations e.g. @PermitAll, @RolesAllowed or @DenyAll.

In previous post, we already have a practice on How to Create Jersey REST APIs with Spring Boot . In this blog, i will show you how to apply role based security for our created REST APIs.

Project Structure

The project structure of application created in this tutorial is as below:

Spring Boot JAX-RS Security Demo – Project Structure

Create REST APIs

Create Spring Boot Project

Go to Spring Initializr portal and create spring boot application with Jersey (JAX-RS) dependency.

Select Jersey in Spring Boot Initializr

Import in Eclipse

Generate the project as zip file. Extract it in some place in your computer. Import the project as ‘Existing maven application’ into eclipse.

Check maven dependencies

Check the maven file should have spring-boot-starter-jersey dependency in it.

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jersey</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

Create REST APIs

Now create some JAX-RS resources which we will access into testing phase. I have created UserResource class.

UserResource.java

package com.favtuts.jerseydemo;
 
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
 
@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(name = "users")
@Path("/users")
public class UserResource 
{
  private static Map<Integer, User> DB = new HashMap<>(); 
   
  @GET
  @Produces("application/json")
  public Users getAllUsers() {
    Users users = new Users();
    users.setUsers(new ArrayList<>(DB.values()));
    return users;
  }
   
  @POST
  @Consumes("application/json")
  public Response createUser(User user) throws URISyntaxException 
  {
    if(user.getFirstName() == null || user.getLastName() == null) {
      return Response.status(400).entity("Please provide all mandatory inputs").build();
    }
    user.setId(DB.values().size()+1);
    user.setUri("/user-management/"+user.getId());
    DB.put(user.getId(), user);
    return Response.status(201).contentLocation(new URI(user.getUri())).build();
  }
 
  @GET
  @Path("/{id}")
  @Produces("application/json")
  public Response getUserById(@PathParam("id") int id) throws URISyntaxException 
  {
    User user = DB.get(id);
    if(user == null) {
      return Response.status(404).build();
    }
    return Response
        .status(200)
        .entity(user)
        .contentLocation(new URI("/user-management/"+id)).build();
  }
 
  @PUT
  @Path("/{id}")
  @Consumes("application/json")
  @Produces("application/json")
  public Response updateUser(@PathParam("id") int id, User user) throws URISyntaxException 
  {
    User temp = DB.get(id);
    if(user == null) {
      return Response.status(404).build();
    }
    temp.setFirstName(user.getFirstName());
    temp.setLastName(user.getLastName());
    DB.put(temp.getId(), temp);
    return Response.status(200).entity(temp).build();
  }
 
  @DELETE
  @Path("/{id}")
  public Response deleteUser(@PathParam("id") int id) throws URISyntaxException {
    User user = DB.get(id);
    if(user != null) {
      DB.remove(user.getId());
      return Response.status(200).build();
    }
    return Response.status(404).build();
  }
   
  static
  {
    User user1 = new User();
    user1.setId(1);
    user1.setFirstName("John");
    user1.setLastName("Wick");
    user1.setUri("/user-management/1");
 
    User user2 = new User();
    user2.setId(2);
    user2.setFirstName("Harry");
    user2.setLastName("Potter");
    user2.setUri("/user-management/2");
     
    DB.put(user1.getId(), user1);
    DB.put(user2.getId(), user2);
  }
}

Users.java

package com.favtuts.jerseydemo;
  
import java.util.ArrayList;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
  
@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(name = "users")
public class Users {
  
    @XmlElement(name="user")
    private ArrayList<User> users;
  
    public ArrayList<User> getUsers() {
        return users;
    }
  
    public void setUsers(ArrayList<User> users) {
        this.users = users;
    }
}

User.java

package com.favtuts.jerseydemo;
  
import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
  
@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(name = "user")
public class User implements Serializable {
  
    private static final long serialVersionUID = 1L;
  
    @XmlAttribute(name = "id")
    private int id;
  
    @XmlAttribute(name="uri")
    private String uri;
  
    @XmlElement(name = "firstName")
    private String firstName;
  
    @XmlElement(name = "lastName")
    private String lastName;
  
    // Getters and Setters
}

Configure Jersey

Now we have a JAX-RS resource and we want to access it from spring boot application which include Jersey dependency. Let’s register this resource as Jersey resource.

package com.howtodoinjava.jerseydemo;
 
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;
 
@Component
public class JerseyConfig extends ResourceConfig 
{
  public JerseyConfig() 
  {
    register(SecurityFilter.class);
    register(UserResource.class);
  }
}
  • Look at the @Component annotation. It enables this class to be registered while spring boot auto scans the java classes in source folder.
  • ResourceConfig provides advanced capabilities to simplify registration of JAX-RS components.
  • SecurityFilter class is the actual auth details processor which we will see later in this tutorial.

Extend spring boot application with SpringBootServletInitializer.

package com.howtodoinjava.jerseydemo;
 
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;
 
@SpringBootApplication
public class JerseydemoApplication extends SpringBootServletInitializer 
{
  public static void main(String[] args) 
  {
    new JerseydemoApplication().configure(new SpringApplicationBuilder(JerseydemoApplication.class)).run(args);
  }
}

Secure REST APIs with JAX-RS Annotations

Now when our APIs are ready, we will start securing them. Let’s annotate the APIs with JAX-RS annotations based on their desired access level and user roles allowed to access them.

package com.favtuts.jerseydemo;
 
@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(name = "users")
@Path("/users")
public class UserResource 
{
  private static Map<Integer, User> DB = new HashMap<>(); 
   
  @GET
  @PermitAll
  @Produces("application/json")
  public Users getAllUsers() {
    Users users = new Users();
    users.setUsers(new ArrayList<>(DB.values()));
    return users;
  }
   
  @POST
  @Consumes("application/json")
  @RolesAllowed("ADMIN")
  public Response createUser(User user) throws URISyntaxException 
  {
    if(user.getFirstName() == null || user.getLastName() == null) {
      return Response.status(400).entity("Please provide all mandatory inputs").build();
    }
    user.setId(DB.values().size()+1);
    user.setUri("/user-management/"+user.getId());
    DB.put(user.getId(), user);
    return Response.status(201).contentLocation(new URI(user.getUri())).build();
  }
 
  @GET
  @Path("/{id}")
  @Produces("application/json")
  @PermitAll
  public Response getUserById(@PathParam("id") int id) throws URISyntaxException 
  {
    User user = DB.get(id);
    if(user == null) {
      return Response.status(404).build();
    }
    return Response
        .status(200)
        .entity(user)
        .contentLocation(new URI("/user-management/"+id)).build();
  }
 
  @PUT
  @Path("/{id}")
  @Consumes("application/json")
  @Produces("application/json")
  @RolesAllowed("ADMIN")
  public Response updateUser(@PathParam("id") int id, User user) throws URISyntaxException 
  {
    User temp = DB.get(id);
    if(user == null) {
      return Response.status(404).build();
    }
    temp.setFirstName(user.getFirstName());
    temp.setLastName(user.getLastName());
    DB.put(temp.getId(), temp);
    return Response.status(200).entity(temp).build();
  }
 
  @DELETE
  @Path("/{id}")
  @RolesAllowed("ADMIN")
  public Response deleteUser(@PathParam("id") int id) throws URISyntaxException {
    User user = DB.get(id);
    if(user != null) {
      DB.remove(user.getId());
      return Response.status(200).build();
    }
    return Response.status(404).build();
  }
   
  static
  {
    User user1 = new User();
    user1.setId(1);
    user1.setFirstName("John");
    user1.setLastName("Wick");
    user1.setUri("/user-management/1");
 
    User user2 = new User();
    user2.setId(2);
    user2.setFirstName("Harry");
    user2.setLastName("Potter");
    user2.setUri("/user-management/2");
     
    DB.put(user1.getId(), user1);
    DB.put(user2.getId(), user2);
  }
}

You can see the security related JAX-RS annotations in above highlighted lines.

Write security filter using JAX-RS ContainerRequestFilter

Now it’s time to write our security filter which will examine the incoming requests, fetch the authorization information (basic auth in this example), and then will match user name and password, and finally it will verify the user’s access level by it’s role. If everything matches, API will be accessed else user will get access denied response.

package com.favtuts.jerseydemo;
 
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
 
import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
  
/**
 * This filter verify the access permissions for a user based on 
 * user name and password provided in request
 * */
@Provider
public class SecurityFilter implements javax.ws.rs.container.ContainerRequestFilter
{
    private static final String AUTHORIZATION_PROPERTY = "Authorization";
    private static final String AUTHENTICATION_SCHEME = "Basic";
    private static final Response ACCESS_DENIED = Response.status(Response.Status.UNAUTHORIZED).build();
    private static final Response ACCESS_FORBIDDEN = Response.status(Response.Status.FORBIDDEN).build();
    private static final Response SERVER_ERROR = Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
     
    @Context
    private ResourceInfo resourceInfo;
      
    @Override
    public void filter(ContainerRequestContext requestContext)
    {
        Method method = resourceInfo.getResourceMethod();
        //Access allowed for all 
        if( ! method.isAnnotationPresent(PermitAll.class))
        {
            //Access denied for all 
            if(method.isAnnotationPresent(DenyAll.class))
            {
                requestContext.abortWith(ACCESS_FORBIDDEN);
                return;
            }
              
            //Get request headers
            final MultivaluedMap<String, String> headers = requestContext.getHeaders();
              
            //Fetch authorization header
            final List<String> authorization = headers.get(AUTHORIZATION_PROPERTY);
              
            //If no authorization information present; block access
            if(authorization == null || authorization.isEmpty())
            {
                requestContext.abortWith(ACCESS_DENIED);
                return;
            }
              
            //Get encoded username and password
            final String encodedUserPassword = authorization.get(0).replaceFirst(AUTHENTICATION_SCHEME + " ", "");
              
            //Decode username and password
            String usernameAndPassword = null;
            try {
                usernameAndPassword = new String(Base64.getDecoder().decode(encodedUserPassword));
            } catch (Exception e) {
                requestContext.abortWith(SERVER_ERROR);
                return;
            }
  
            //Split username and password tokens
            final StringTokenizer tokenizer = new StringTokenizer(usernameAndPassword, ":");
            final String username = tokenizer.nextToken();
            final String password = tokenizer.nextToken();
              
            //Verifying Username and password
            if(!(username.equalsIgnoreCase("admin") && password.equalsIgnoreCase("password"))){
              requestContext.abortWith(ACCESS_DENIED);
                return;
            }
              
            //Verify user access
            if(method.isAnnotationPresent(RolesAllowed.class))
            {
                RolesAllowed rolesAnnotation = method.getAnnotation(RolesAllowed.class);
                Set<String> rolesSet = new HashSet<String>(Arrays.asList(rolesAnnotation.value()));
                  
                //Is user valid?
                if( ! isUserAllowed(username, password, rolesSet))
                {
                    requestContext.abortWith(ACCESS_DENIED);
                    return;
                }
            }
        }
    }
    private boolean isUserAllowed(final String username, final String password, final Set<String> rolesSet) 
    {
        boolean isAllowed = false;
          
        //Step 1. Fetch password from database and match with password in argument
        //If both match then get the defined role for user from database and continue; else return isAllowed [false]
        //Access the database and do this part yourself
        //String userRole = userMgr.getUserRole(username);
        String userRole = "ADMIN";
          
        //Step 2. Verify user role
        if(rolesSet.contains(userRole))
        {
            isAllowed = true;
        }
        return isAllowed;
    }
}

Demo

Run the project as Spring boot application. Now test rest resources.

$ mvn spring-boot:run

Access GET /users resource

GET Users collection
$ curl -X GET \
  -H "Content-type: application/json" \
  -H "Accept: application/json" \
  "http://localhost:8080/users" | jq


{
  "user": [
    {
      "id": 1,
      "uri": "/user-management/1",
      "firstName": "John",
      "lastName": "Wick"
    },
    {
      "id": 2,
      "uri": "/user-management/2",
      "firstName": "Harry",
      "lastName": "Potter"
    }
  ]
}

Access POST /users resource with no authtentication details

Look at the returned status code 401.

Auth Required for POST APIs
$ curl -w 'RESP_CODE:%{response_code}' \
  -X POST \
  -H "Content-type: application/json" \
  -H "Accept: application/json" \
  -d '{"firstName":"John123","lastName":"Wick"}' \
  "http://localhost:8080/users"

RESP_CODE:401

Access POST /users resource with authentication details added

Use this link to generate base64 encoded user name and password combination to pass into Authorization header.

Request with Auth Details Success
curl -w 'RESP_CODE:%{response_code}' \
  -X POST \
  -H "Content-type: application/json" \
  -H "Accept: application/json" \
  -H "Authorization: YWRtaW46cGFzc3dvcmQ=" \
  -d '{"firstName":"John123","lastName":"Wick"}' \
  "http://localhost:8080/users"

RESP_CODE:201

Download Sourcecode

To download the sourcecode of this example, click on below given download link on Github:

Drop me your questions in comments section.

Happy Learning !!

Leave a Reply

Your email address will not be published. Required fields are marked *