“Giving your users roles is like handing out special access cards—only those with the right card can enter the VIP lounge!” 🛂✨
You’ve secured your Spring Boot app with JWT, handling basic login. Now, let’s talk about who can do what. Authorization is all about making sure that authenticated users have the *permission* to access specific resources or perform certain actions. In this post, we’ll dive into implementing role-based authorization, going beyond just knowing who a user is, to knowing what they are allowed to do.
🚩 Prerequisites
- Java Development Kit (JDK) 17+
- A basic Spring Boot project with Spring Security and JWT Authentication set up (like the one from “Spring Boot JWT Security”).
- Familiarity with Spring Security concepts (AuthenticationManager, UserDetails, UserDetailsService).
1️⃣ Defining Roles
First, let’s establish our roles. A simple enum
is a great way to manage this:
// src/main/java/com/example/model/Role.java
package com.example.model;
public enum Role {
ROLE_USER,
ROLE_ADMIN,
ROLE_MODERATOR; // Always prefix with ROLE_ for Spring Security
}
We’ll update our User
entity to hold these roles.
2️⃣ Updating User Entity & AppUserDetails
Modify your User
entity to include a collection of roles:
// src/main/java/com/example/model/User.java (snippet)
// ... existing imports ...
import jakarta.persistence.CollectionTable;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
// ... other fields ...
@ElementCollection(targetClass = Role.class, fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Enumerated(EnumType.STRING)
private Set<Role> roles = new HashSet<>();
// ... other fields and getters/setters ...
public Set<Role> getRoles() { return roles; }
public void setRoles(Set<Role> roles) { this.roles = roles; }
// ... rest of the User class ...
When a user registers, you can assign them a default role:
// src/main/java/com/example/controller/AuthController.java (signup method snippet)
// ...
user.setRoles(Collections.singleton(Role.ROLE_USER)); // Assign default ROLE_USER
// ...
Next, adapt AppUserDetails
to provide these roles as GrantedAuthority
objects:
// src/main/java/com/example/security/AppUserDetails.java (snippet)
// ... existing imports ...
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import com.example.model.User;
import com.example.model.Role; // Don't forget this import
// ...
public AppUserDetails(User u) {
this.id = u.getId();
this.email = u.getEmail();
this.password = u.getPassword();
// Map roles to SimpleGrantedAuthority
this.authorities = u.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.name()))
.collect(Collectors.toList());
}
// ... rest of the AppUserDetails class ...
3️⃣ Enabling Method-Level Security with @PreAuthorize
To use annotations like @PreAuthorize
, you need to enable global method security in your SecurityConfig
:
// src/main/java/com/example/config/SecurityConfig.java (snippet)
// ... existing imports ...
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@Configuration
@EnableMethodSecurity // Enable method-level security
public class SecurityConfig {
// ... existing beans and filterChain method ...
}
Now, you can apply @PreAuthorize
directly on your controller or service methods:
// src/main/java/com/example/controller/AdminController.java
package com.example.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@GetMapping("/dashboard")
@PreAuthorize("hasRole('ADMIN')") // Only users with ROLE_ADMIN can access
public String getAdminDashboard() {
return "Welcome to the Admin Dashboard, Chief!";
}
@GetMapping("/users")
@PreAuthorize("hasAnyRole('ADMIN', 'MODERATOR')") // Admins OR Moderators
public String getAllUsers() {
return "Listing all users (for Admin/Moderator eyes only!)";
}
@GetMapping("/public")
@PreAuthorize("isAuthenticated()") // Any authenticated user
public String getPublicAdminContent() {
return "This is admin content, but visible to any authenticated user.";
}
}
hasRole('ADMIN')
: Checks if the authenticated user has the `ROLE_ADMIN` authority.hasAnyRole('ADMIN', 'MODERATOR')
: Grants access if the user has either `ROLE_ADMIN` or `ROLE_MODERATOR`.isAuthenticated()
: Checks if the user is authenticated (logged in), regardless of roles.
4️⃣ URL-Based Authorization in SecurityConfig
For broader path-based restrictions, you can configure authorization directly in your SecurityConfig
:
// src/main/java/com/example/config/SecurityConfig.java (filterChain method snippet)
// ...
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
// ...
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
JwtAuthenticationFilter jwtFilter)
throws Exception {
http
.cors(AbstractHttpConfigurer::disable) // Or configure CORS properly
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(sm->
sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(eh->
eh.authenticationEntryPoint(new RestAuthenticationEntryPoint()))
.authorizeHttpRequests(auth->auth
.requestMatchers("/auth/**","/oauth2/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN") // Only ADMIN for /api/admin paths
.requestMatchers("/api/public/**").permitAll() // Example: public paths
.anyRequest().authenticated())
.addFilterBefore(jwtFilter,
UsernamePasswordAuthenticationFilter.class)
.oauth2Login(oauth2->oauth2
// ... OAuth2 configurations ...
);
return http.build();
}
.requestMatchers("/api/admin/**").hasRole("ADMIN")
: Any request to paths starting with `/api/admin/` will require the `ROLE_ADMIN` role.- **Note**: Method-level security (`@PreAuthorize`) takes precedence over URL-based security for specific methods.
5️⃣ Testing Your Authorization Rules
You can use tools like Postman or cURL to test your endpoints:
- Login as a regular user (with `ROLE_USER`) to get a JWT.
- Try accessing `/api/admin/dashboard` with the user’s JWT. You should get a **403 Forbidden** response.
- Now, **create a user with `ROLE_ADMIN`** (you might need a temporary endpoint to update user roles in your database for testing).
- Login as the admin user and try accessing `/api/admin/dashboard`. You should get a **200 OK** response.
💡 Monkey-Proof Tips
- Use `hasRole()` for roles, `hasAuthority()` for general permissions. While `hasRole(‘ADMIN’)` internally checks `ROLE_ADMIN`, you can define more granular permissions (e.g., `user:read`, `user:write`) and check them with `hasAuthority(‘user:write’)`.
- Layer your security: Start with URL-based security for broad access control, then use method-level security (`@PreAuthorize`) for fine-grained control within specific methods.
- Avoid hardcoding roles in `User` entity for large apps: For more complex scenarios, store roles/permissions in separate tables and fetch them when loading `UserDetails`.
- Custom Security Expressions: For very specific and complex authorization logic, consider creating custom security expressions by extending `MethodSecurityExpressionRoot`.
🚀 Challenge
- Implement a new endpoint: Create a `/api/moderator/review` endpoint that only users with `ROLE_MODERATOR` or `ROLE_ADMIN` can access.
- Dynamic Role Assignment: Add an admin-only endpoint (`/api/admin/assign-role`) that allows an `ADMIN` to assign `ROLE_MODERATOR` to an existing `USER` based on their email. Remember to save the updated `User` entity.
- Custom Access Denied Handler: Implement a custom `AccessDeniedHandler` to return a specific JSON error message (e.g., `{“error”: “Access Denied”, “message”: “You do not have the necessary permissions.”}`) instead of the default Spring Security response when a user lacks authorization.
👏 You’ve now mastered the art of authorization in Spring Security! Your applications are not just authenticated, but securely controlled, ensuring every user stays in their designated lane. Keep building secure and robust Java applications!