Spring Security
- Mark Kendall
- Jan 31
- 5 min read
Let's turn this into a more structured tutorial on Spring Security and roles.
## Spring Security and Roles: A Practical Tutorial
This tutorial demonstrates how to implement role-based authorization in a Spring Boot REST API using Spring Security. We'll cover user authentication, role assignment, and how to protect API endpoints based on roles.
1. Project Setup (using Spring Initializr):
Include the following dependencies:
* Spring Web
* Spring Security
* Spring Data JPA (or your preferred data access method)
* A database driver (e.g., H2, MySQL, PostgreSQL)
* Lombok (optional, for reducing boilerplate)
* JWT library (e.g., jjwt)
2. User Entity (AppUser):
```java
@Entity
@Data
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password; // Store securely hashed!
@ElementCollection(fetch = FetchType.EAGER) // Or a separate Role entity for more complex scenarios
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id")) // Specify table for roles
@Column(name = "role") // Specify column for role
private List<String> roles; // List of roles (e.g., "ROLE_ADMIN", "ROLE_CLIENT")
// ... other fields
}
```
3. User Repository:
```java
public interface UserRepository extends JpaRepository<AppUser, Long> {
AppUser findByUsername(String username);
}
```
4. User Service (Implementing UserDetailsService):
```java
@Service
@RequiredArgsConstructor
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public AppUser signup(AppUser user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
return userRepository.save(user);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AppUser appUser = userRepository.findByUsername(username);
if (appUser == null) {
throw new UsernameNotFoundException("User not found");
}
// Convert roles to GrantedAuthorities
List<GrantedAuthority> authorities = appUser.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role))
.collect(Collectors.toList());
return new User(appUser.getUsername(), appUser.getPassword(), authorities);
}
// ... other methods (e.g., retrieving user by username)
}
```
5. Security Configuration:
```java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // Enable method-level security annotations like @PreAuthorize
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtAuthenticationFilter; // Your JWT filter
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors().and()
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/auth/**").permitAll() // Allow authentication endpoints
.antMatchers("/admin/**").hasRole("ADMIN") // Protect admin endpoints
.antMatchers("/client/**").hasAnyRole("ADMIN", "CLIENT") // Protect client endpoints
.anyRequest().authenticated() // All other endpoints require authentication
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // Use a strong password encoder
}
}
```
6. JWT Filter (JwtAuthenticationFilter - Example):
```java
public class JwtAuthenticationFilter extends OncePerRequestFilter {
// ... (Inject JWT utility class, UserDetailsService, etc.)
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = extractJwtToken(request); // Helper function to extract token
if (token != null && jwtUtil.validateToken(token)) {
String username = jwtUtil.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()); // Get authorities (roles)
SecurityContextHolder.getContext().setAuthentication(authentication); // Set authentication in context
}
filterChain.doFilter(request, response);
}
// ... (Helper functions: extractJwtToken, etc.)
}
```
7. Controller Example:
```java
@RestController
@RequestMapping("/admin")
@PreAuthorize("hasRole('ROLE_ADMIN')") // Secure at the controller level
public class AdminController {
@GetMapping("/dashboard")
public String adminDashboard() {
return "Admin Dashboard (Accessible only by admins)";
}
}
@RestController
@RequestMapping("/client")
public class ClientController {
@GetMapping("/profile")
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_CLIENT')") // Both admin and client can access
public String clientProfile() {
return "Client Profile";
}
@GetMapping("/data")
@PreAuthorize("hasRole('ROLE_CLIENT')") // Only client can access
public String clientData() {
return "Client Data";
}
}
@RestController
@RequestMapping("/auth")
public class AuthenticationController {
// ... signup and signin endpoints
}
```
8. Key Improvements and Explanations:
*`@EnableMethodSecurity`:** This annotation is crucial for using `@PreAuthorize` in your controllers.
*`UserDetailsService`:** The `loadUserByUsername` method is where you retrieve user details (including roles) from your database and convert them into `UserDetails` objects. This is the bridge between your user data and Spring Security.
*`GrantedAuthority`:** Roles are represented as `GrantedAuthority` objects in Spring Security.
*`@PreAuthorize`:** This annotation allows you to secure individual methods based on roles. `hasRole()` checks for a specific role (e.g., "ROLE_ADMIN"), while `hasAnyRole()` checks for any of the specified roles.
*Role Prefix:** Spring Security automatically adds a "ROLE_" prefix to roles (e.g., "ADMIN" becomes "ROLE_ADMIN"). This is why you use "ROLE_ADMIN" in `@PreAuthorize`.
*JWT Filter:** The `JwtAuthenticationFilter` (or your authentication mechanism) is responsible for extracting the JWT (or other credentials), validating it, and setting the authentication in the `SecurityContextHolder`. This is where the roles from the JWT are loaded into the `Authentication` object.
*Database Design:** Consider a separate `Role` entity for more complex role management. This is especially useful if you have hierarchical roles or permissions.
*Password Hashing:** Use `PasswordEncoder` (like `BCryptPasswordEncoder`) to hash passwords before storing them in the database. Never store passwords in plain text.
9. Testing:
Use tools like Postman or curl to test your API endpoints. Make sure to include the JWT in the `Authorization` header for protected endpoints. Test with different users having different roles to verify that the authorization is working correctly.
This tutorial provides a comprehensive example of how to implement role-based authorization using Spring Security. Remember to adapt the code to your specific needs and always prioritize security best practices.
Comments