Method-Level Authorization in Spring Security
In this post, we'll explore how to use Spring Security to control access both at the endpoint and method level using a sample project. We'll cover role-based and authority-based security, showing how both can be configured and used to enhance your application's overall security posture.
- Source code : Spring Security Method-Level Authorization Demo
Spring Security Configuration
To get started, we need to configure Spring Security. Below is the SecurityConfig
class, which is responsible for setting up the security rules and ensuring proper authentication and authorization for our application. With Spring Security, you can create custom security policies that help protect your application from unauthorized access.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll() // Allow access to public endpoints
.requestMatchers("/admin/api/**").hasRole("ADMIN") // Require ADMIN role for admin endpoints
.requestMatchers("/api/**").hasRole("USER") // Require USER role for general endpoints
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
The SecurityFilterChain
bean is responsible for defining how requests are secured. We start by disabling CSRF protection for simplicity (note that in production, you should consider enabling it for non-API requests).
Spring evaluates the matchers in the order they are defined in the authorizeHttpRequests block, stopping at the first match. This means that if multiple matchers overlap or are too generic, the earlier ones will take precedence, potentially overriding more restrictive rules.
This layered approach to authorization allows us to provide clear boundaries for different types of users within our application, ensuring that each user only has access to the features they need.
@Bean
public UserDetailsService userDetailsService() {
var user1 = User.withUsername("user1")
.password("{noop}user1")
.roles("USER")
.build();
var admin1 = User.withUsername("admin1")
.password("{noop}admin1")
.roles("ADMIN")
.build();
var admin2 = User.withUsername("admin2")
.password("{noop}admin2")
.authorities("ROLE_ADMIN", "CREATE_ORDER")
.build();
return new InMemoryUserDetailsManager(user1, admin1, admin2);
}
In this configuration, we set up an InMemoryUserDetailsManager
with three users: user1
, admin1
, and admin2
. The user user1
has the role USER
, while admin1
has the role ADMIN
. Additionally, admin2
has both the ADMIN
role and an extra authority called CREATE_ORDER
, which allows them to perform more specific actions, such as creating orders.
In addition to HTTP Basic authentication, this configuration can easily be extended to include OAuth2 or JWT-based authentication to provide more sophisticated security mechanisms.
Applying Method-Level Authorization
To illustrate how to implement method-level authorization, we have two controllers in our project:
- The
AdminController
handles administrative operations related to orders, while - the general
Controller
manages regular user orders.
By having separate controllers for different roles, we ensure that the application follows the principle of least privilege, where users only access the data they need.
@RestController
@RequestMapping("/admin/api/orders")
public class AdminController {
@GetMapping
public String getOrders() {
return "Admin orders returned";
}
@PostMapping
@PreAuthorize("hasAuthority('CREATE_ORDER')")
public String createOrder() {
return "Order created";
}
}
In the AdminController
, there are two endpoints. The GET
endpoint returns a list of orders, while the POST
endpoint is used to create a new order. The POST
endpoint is protected using the @PreAuthorize
annotation to ensure that only users with the CREATE_ORDER
authority can access it.
The @PreAuthorize
annotation is a powerful feature provided by Spring Security that allows you to specify authorization requirements at the method level. In this case, only users with the CREATE_ORDER
authority are allowed to create a new order. This allows us to provide more granular control over access, ensuring that sensitive actions are restricted to users with the appropriate permissions.
@RestController
@RequestMapping("/api/orders")
public class Controller {
@GetMapping
public String getOrders() {
return "Orders returned";
}
}
The general Controller
is used for managing orders accessible to regular users. This controller does not have any special method-level security annotations, as it is intended for users with the USER
role.
Response Examples for Endpoint Requests
Here are some examples of the responses for different requests made to the API endpoints:
GET localhost:8080/api/orders
HTTP 401 Unauthorized
{
"error": "Unauthorized",
"message": "Full authentication is required to access this resource"
}
GET localhost:8080/admin/api/orders
Authorization: Basic admin1 admin1
HTTP 200 OK
Admin orders returned
POST localhost:8080/admin/api/orders
Authorization: Basic admin1 admin1
HTTP 403 Forbidden
{
"error": "Forbidden",
"message": "Access is denied"
}
Roles vs Authorities
Roles are a specific type of authority in Spring Security, distinguished by the "ROLE_"
prefix in their names. For example, .roles("ADMIN")
is equivalent to .authorities("ROLE_ADMIN")
.
However, caution is needed when mixing roles
and authorities
while configuring user permissions. In the example below, the user will have only the CREATE_ORDER
authority because .authorities("CREATE_ORDER")
overrides .roles("ADMIN")
.
var admin = User.withUsername("admin")
.password("{noop}admin")
.roles("ADMIN") // This is translated to "ROLE_ADMIN"
.authorities("CREATE_ORDER") // This overrides the role
.build();
In such cases, explicitly specify all desired authorities in .authorities()
to avoid unintentional overrides.
Additional Considerations for Method-Level Security
Additionally, method-level security annotations like @PreAuthorize
can be combined with other annotations such as @PostAuthorize
, @Secured
, and @RolesAllowed
to provide even more flexibility. For instance, @PostAuthorize
can be used to validate the response after the method has executed, which can be helpful in certain scenarios, such as ensuring that a user only sees data they are allowed to access.
Happy coding! 💻