DEV Community

Ed Legaspi
Ed Legaspi

Posted on

How to Secure a Spring Rest Service using Spring Aspect

Learn how to secure Spring REST endpoints using Aspect and without using Spring Security.

1. Introduction

In this project we introduce a security interceptor that pre-handles a REST request and where a bearer token is converted into an application user. If the user does not exist or does not have the required permission, an error will be thrown. If the user is valid, it's id is save in the request attribute and can be pass into the REST endpoint, so that it can be used for future use.

With this approach you can build your own user, role, permission table without using Spring Security.

2. The User Entity

We need to define the user, role, and permision table below.

Image description

2.1 User table, which contains the user information that we need for logging in.

2.2 Role, which is linked in the permission table.

2.3 UserRole, which is the bridge table between a user and a role.

We will need this information later when we intercept the request.

3. Roles and Permissions

We willd define a role EmployeeManager with permissions list_employee, read_employee, update_employee, etc. But we will only assign the list_employee permission to our user.

4. User Holder

We need to define a pojo that will hold our current user.

package com.czetsuyatech.springaspect.web.security;

import java.util.Set;
import lombok.Data;

@Data
public class CurrentUser {

  private String username;
  private Set<String> permissions;
}
Enter fullscreen mode Exit fullscreen mode

5. Security Aspect

Let's inspect the classes that secure our application.
Security interface with hasPermission method. As we can see, we use this interface to annotate our endpoints. It can be defined on a class level and inherited by its methods.

package com.czetsuyatech.springaspect.web.security;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Security {

  String hasPermission();
}
Enter fullscreen mode Exit fullscreen mode

SecurityInterceptor, it intercepts any request to our REST service and get the bearer-token from the HTTP headers. From the bearer-token, we can get the external id which we can map to a local user stored in our database. Then we can get the permissions associated to this user.

package com.czetsuyatech.springaspect.web.security;

import java.util.Arrays;
import java.util.HashSet;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;

public class SecurityInterceptor implements HandlerInterceptor {

  private static final Pattern AUTH_HEADER_PATTERN = Pattern.compile("Bearer (\\S+)");

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

    // get the bearer token
    //    String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
    //
    //    if (StringUtils.hasLength(authorization)) {
    //      throw new AccessDeniedException("Authorization header is empty");
    //    }
    //    Matcher matcher = AUTH_HEADER_PATTERN.matcher(authorization);
    //    if (!matcher.matches()) {
    //      throw new AccessDeniedException(String.format("Invalid authorization header: [%s]", authorization));
    //    }
    //    String accessToken = matcher.group(1);

    // get the user from the bearer token
    // Long userId = getUserIdByAccessToken(accessToken);

    CurrentUser currentUser = new CurrentUser();
    currentUser.setUsername("czetsuyatech.com");
    currentUser.setPermissions(new HashSet<>(Arrays.asList("list_employee")));

    // catch null user here

    // use for security
    UserThreadLocalHolder.set(currentUser);

    // use to fetch the user info
    request.setAttribute("currentUserId", 1L);

    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode

6. UserThreadLocalHolder

This is where we store the user data for the current thread which we can access later in our aspect for authorization.

package com.czetsuyatech.springaspect.web.security;

public class UserThreadLocalHolder {

  private static ThreadLocal<CurrentUser> threadLocal = new ThreadLocal<>();

  public static void set(CurrentUser currentUser) {
    threadLocal.set(currentUser);
  }

  public static CurrentUser get() {
    return threadLocal.get();
  }

  public static void remove() {
    threadLocal.remove();
  }
}
Enter fullscreen mode Exit fullscreen mode

7. SecurityAspect

This class is called in any method annotated with our Security interface and do authorization base on the authorization token and user data in our table.

package com.czetsuyatech.springaspect.web.security;

import com.czetsuyatech.springaspect.web.security.exception.UserDoesNotExistsException;
import com.czetsuyatech.springaspect.web.security.exception.UserNotAuthorizedException;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class SecurityAspect {

  @Before(
      value = "@annotation(security)",
      argNames = "joinPoint,security"
  )
  public void methodSecurity(
      JoinPoint joinPoint,
      Security security
  ) {
    authorize(joinPoint, security);
  }

  @Before(
      value = "@within(security)",
      argNames = "joinPoint,security"
  )
  public void classSecurity(
      JoinPoint joinPoint,
      Security security
  ) {
    authorize(joinPoint, security);
  }

  private void authorize(JoinPoint joinPoint, Security security) {

    CurrentUser currentUser = UserThreadLocalHolder.get();
    if (currentUser == null) {
      throw new UserDoesNotExistsException();

    } else if (currentUser.getPermissions() == null || !currentUser.getPermissions()
        .contains(security.hasPermission())) {
      throw new UserNotAuthorizedException(currentUser.getUsername(),
          joinPoint.getSignature().getName());
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

8. The Controller

We will define two endpoints one with list_employee constraint and the second one with update_employee. Thus, we should get an error in the second.

package com.czetsuyatech.springaspect.web.controllers;

import com.czetsuyatech.springaspect.web.security.Security;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/employees")
public class EmployeeController {

  @Security(hasPermission = "list_employee")
  @GetMapping
  public String getEmployees(@RequestAttribute Long currentUserId) {
    return String.format("currentUserId=%d", currentUserId);
  }

  @Security(hasPermission = "read_employee")
  @GetMapping("/{id}")
  public String getEmployee(@PathVariable String id, @RequestAttribute Long currentUserId) {
    return String.format("currentUserId=%d", currentUserId);
  }
}
Enter fullscreen mode Exit fullscreen mode

9. Summary

In this blog, we learn how to secure our application using Spring Aspect and without Spring Security.

10. Disadvantages

This is a very basic definition that fits a very particular use-case. If you want more flexibility, I highly suggest to use Spring Security.

11. Git Repository

Source code is available at https://github.com/czetsuya/spring-aspect-security.

Top comments (1)

Collapse
 
user1111333 profile image
Sacred (void*)

Cool stuff. Recently I just started re-study Spring. It's really cool that Spring somehow reinvented everything from servlet-api.

All of these aspect, and deeply-managed stuff by Spring is freaking good, but as a chad servlet guru, I prefer stick with less complex stuff like a simple servlet-filter maybe.

Thanks for sharing, good article.