Granular authorization with authority provides detailed control over user access by defining specific permissions rather than broad roles. Here’s a summary:
- Granular vs. Role-Based: Granular authorization focuses on precise permissions (e.g.,
EDIT_USER
), while role-based authorization uses broader roles (e.g., ROLE_ADMIN
) that aggregate multiple permissions. - Authorities: In Spring Security, authorities represent individual permissions (e.g.,
READ_PRIVILEGE
). They allow fine-tuned access control compared to roles. - Method-Level Security: Annotations like
@PreAuthorize
enable permission checks directly on methods, allowing for fine-grained control over who can perform specific actions. - Custom Authorities: Authorities can be customized to fit application needs (e.g.,
MANAGE_INVOICES
). This customization supports detailed access control based on business requirements. - Multiple Authorities: Users can have multiple authorities, granting them a combination of permissions (e.g., both
READ_PRIVILEGE
and WRITE_PRIVILEGE
). - Dynamic Authorization: Granular authorization allows real-time checks based on user context, enabling dynamic decision-making about access.
- Business-Driven Control: It aligns access control with business processes, ensuring that users have permissions directly related to their roles and responsibilities.
Granular authorization offers precise and flexible access management, ensuring users only have the permissions necessary for their tasks.
Roles: These represent high-level categorizations of users, such as USER
, ADMIN
, and MANAGER
. Roles group multiple permissions or authorities together, providing a broad level of access based on user types.
Example: The roles (Manager
, Employee
, Intern
) define the broad level of access and responsibilities.
Authorities: Also known as privileges or permissions, these represent specific, fine-grained access rights within an application. They control detailed actions a user can perform, such as CREATE
, UPDATE
, or DELETE
. For example:
POST_CREATE
, POST_UPDATE
, POST_DELETE
might manage permissions for operations on posts.BLOG_CREATE
, BLOG_UPDATE
, BLOG_DELETE
might control access to blog-related operations.
Roles aggregate these authorities, allowing users to perform a set of actions based on their role. Authorities provide the precise control needed to manage specific operations and resources within an application.
Just like in a company where each role has specific tasks they are authorized to perform, in an application, roles and authorities work together to define what users can and cannot do.
Code
We will have create an enum. Here’s a brief explanation of how you might use these permissions:
POST_VIEW
: Permission to view posts.POST_CREATE
: Permission to create new posts.POST_UPDATE
: Permission to update existing posts.POST_DELETE
: Permission to delete posts.USER_VIEW
: Permission to view user details.USER_CREATE
: Permission to create new users.USER_UPDATE
: Permission to update existing user details.USER_DELETE
: Permission to delete users.
public enum Permissions {
POST_VIEW,
POST_CREATE,
POST_UPDATE,
POST_DELETE,
USER_VIEW,
USER_CREATE,
USER_UPDATE,
USER_DELETE,
}
When signing up, we need to specify the roles and permissions for the user.
import lombok.Data;
import java.util.Set;
@Data
public class SignupDto {
private String email;
private String password;
private String name;
private Set<Roles> roles;
private Set<Permissions> permissions;
}
We will create a hardcoded mapping that will have like one role have many permission so that we don't have to assign permission again and again.
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class PermissionMapping {
private static final Map<Roles, Set<Permissions>> map = Map.of(
Roles.USER, Set.of(Permissions.POST_VIEW,Permissions.USER_VIEW),
Roles.CREATOR,Set.of(Permissions.POST_VIEW,Permissions.USER_UPDATE,Permissions.POST_UPDATE),
Roles.ADMIN,Set.of(Permissions.USER_CREATE,Permissions.USER_DELETE,Permissions.USER_UPDATE,
Permissions.POST_CREATE,Permissions.POST_DELETE,Permissions.POST_UPDATE)
);
public static Set<SimpleGrantedAuthority> getAuthoritiesForRoles(Roles roles){
return map
.get(roles)
.stream()
.map(permissions -> new SimpleGrantedAuthority(permissions.name()))
.collect(Collectors.toSet());
}
}
Here’s the UserEntity class.
getAuthorities()
:
- This method assembles the
GrantedAuthority
collection for Spring Security. - Roles: Each role is converted to a
SimpleGrantedAuthority
with a prefix of "Role_". - Permissions: Uses
PermissionMapping.getAuthoritiesForRoles(role)
to map roles to their associated permissions, adding these permissions as SimpleGrantedAuthority
.
import jakarta.persistence.*;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class UserEntity implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)//indicate that email should be unique
private String email;
private String password;
private String name;
@ElementCollection(fetch = FetchType.EAGER)
@Enumerated(EnumType.STRING)
private Set<Roles> roles; //one user have multiple role
@ElementCollection(fetch = FetchType.EAGER)
@Enumerated(EnumType.STRING)
private Set<Permissions> permissions; //one user have multiple permission
//we have to implement these three methods
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { //GrantedAuthority has 2 things 1.roles 2.permissions
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
roles.forEach(
role -> {
Set<SimpleGrantedAuthority> permission = PermissionMapping.getAuthoritiesForRoles(role);
authorities.addAll(permission);
authorities.add(new SimpleGrantedAuthority("ROLE_"+role.name()));
}
);
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.name;
}
}
Here’s our Configuration class.
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity //by adding this annotation we are telling springboot that we want to we want to configure this SpringSecurityFilterChain
@RequiredArgsConstructor
public class WebSecurityConfig {
private final JWTAuthFilter jwtAuthFilter;
private final OAuth2SuccessHandler oAuth2SuccessHandler;
private static final String[] publicRoutes = {
"/error","/auth/**","/home.html"
};
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests(auth->
auth
.requestMatchers(publicRoutes).permitAll() // Public endpoints
//.requestMatchers("/endpoint/**").hasRole(Roles.ADMIN.name()) // all the routes after endpoint will be allowed for "ADMIN" only without admin if any one is trying to send any request on this endpoint then it will show a forbidden error
.requestMatchers(HttpMethod.GET,"/endpoint/**").permitAll()//anyone can access get method of this endpoint controller
.requestMatchers(HttpMethod.POST,"/endpoint/**")
.hasAnyRole(Roles.ADMIN.name(),Roles.USER.name()) //only Admin and Creator can access this post method of this endpoint controller
.requestMatchers(HttpMethod.POST,"/endpoint/**")
.hasAnyAuthority(Permissions.POST_CREATE.name()) //POST requests on /endpoint/** only for users with the POST_CREATE permission.
.requestMatchers(HttpMethod.GET,"/endpoint/**")
.hasAuthority(Permissions.POST_VIEW.name()) //Allows GET requests on /endpoint/** only for users with the POST_VIEW permission.
.requestMatchers(HttpMethod.PUT,"/endpoint/**")
.hasAuthority(Permissions.POST_UPDATE.name()) //Allows PUT requests on /endpoint/** only for users with the POST_UPDATE permission.
.requestMatchers(HttpMethod.DELETE,"/endpoint/**")
.hasAnyAuthority(Permissions.POST_DELETE.name()) //Allows DELETE requests on /endpoint/** only for users with the POST_DELETE permission.
.anyRequest().authenticated()) // All other requests require authentication
.csrf(csrfConfig-> // Disable CSRF for simplicity, not recommended for production
csrfConfig
.disable())
.sessionManagement(sessionConfig -> // Disable JSESSIONID for simplicity, not recommended for production
sessionConfig
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.oauth2Login(oauth2Config->oauth2Config
.failureUrl("/login?error=true")
.successHandler(oAuth2SuccessHandler));
return httpSecurity.build(); //when you add build this throws an exception
}
@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
After creating some users, here's the User entity table in the database:
Here's the User-Role Entity Table in the database:
After running your application in debug mode and sending a request to the ‘/login’ endpoint for the user ‘Rama’, whose role is ‘CREATOR’, you should be able to see the mapping for the ‘CREATOR’ role.
Since the ‘CREATOR’ role has no permission for authentication and authorization, if you keep the accessToken of ‘Rama’ and send a request to the ‘/endpoint’ route, you will receive a ‘Forbidden error’ again.