Spring Boot HandBook

    Introduction :#

    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.

    Role vs Authority#

    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.

    Implementations :#

    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(); } }

    Output :#

    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.

    Last updated on Dec 09, 2024