Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Integrating RBAC Authorization in Spring Cloud Alibaba Microservices

Tech 1

Overview

In previous articles, we focused on implementing the authentication module within the Spring Cloud ecosystem, verifying the identity of logged-in users. In this article, we will explore the authorization functionality in Spring Cloud, determining whether a user has access to specific features.

Authentication vs Authorization

Many developers confuse authentication and authorization, treating them as one concept. However, they are distinct concepts. Here's an easy-to-understand example:

You are Zhang San, a moderator of a well-known forum. When you log into the forum by entering your username and password, it confirms that you are Zhang San—this process is called authentication. After logging in, the system recognizes that you are a moderator and allows you to highlight or pin posts made by others—this verification process is authorization.

In short, authentication tells you who you are, while authorization tells you what you can do.

In the Spring Cloud ecosystem, authorization is generally implemented using two methods:

  • Path-based Authorization: All requests go through the Spring Cloud Gateway. Upon receiving a request, the gateway checks if the current user has permission to access the requested path, primarily using the ReactiveAuthorizationManager#check(Mono<Authentication> authenticationMono, AuthorizationContext authorizationContext) method for validation.

    This approach considers the resources a user possesses based on their access paths.

  • Method-level Interception: This method does not intercept at the gateway level. Instead, Spring Security annotations are added to methods requiring permission checks, verifying whether the current user has access to those methods. Custom annotations or AOP can also be used for interception. These approaches are collectively referred to as method-level interception.

    This method typically evaluates permissions based on resource identifiers a user has.

We will now implement both methods for authorization in Spring Cloud.

Core Implementation

Regardless of which approach is chosen, we first need to know the roles and resources associated with the current user. Therefore, we'll establish a simple RBAC model with tibles for users, roles, and resources, along with corresponding Service and DAO layers in the project.

(Resource table includes both resource identifier and request path fields to facilitate code logic)

Path-based Authorization Implementation

  • Modify Custom UserDetailsService

Recall our custom UserDetailsService. In the loadUserByUsername() method, we previously returned a fixed 'ADMIN' role. Now, we must fetch real roles from the database and include all permissions associated with these roles in the UserDetails object.

@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
    // Fetch local user
    SysUser sysUser = sysUserMapper.selectByUserName(userName);
    if (sysUser != null) {
        // Get all roles for the user
        List<SysRole> roleList = sysRoleService.listRolesByUserId(sysUser.getId());
        sysUser.setRoles(roleList.stream().map(SysRole::getRoleCode).collect(Collectors.toList()));
        List<Integer> roleIds = roleList.stream().map(SysRole::getId).collect(Collectors.toList());
        // Get permissions for all roles
        List<SysPermission> permissionList = sysPermissionService.listPermissionsByRoles(roleIds);
        sysUser.setPermissions(permissionList.stream().map(SysPermission::getUrl).collect(Collectors.toList()));
        // Build OAuth2 user
        return buildUserDetails(sysUser);
    } else {
        throw new UsernameNotFoundException("User [" + userName + "] does not exist");
    }
}

/**
 * Build OAuth2 user, assign roles and permissions to the user. Roles use ROLE_ prefix.
 *
 * @param sysUser System user
 * @return UserDetails
 */
private UserDetails buildUserDetails(SysUser sysUser) {
    Set<String> authSet = new HashSet<>();
    List<String> roles = sysUser.getRoles();
    if (!CollectionUtils.isEmpty(roles)) {
        roles.forEach(item -> authSet.add(CloudConstant.ROLE_PREFIX + item));
        authSet.addAll(sysUser.getPermissions());
    }
    List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(authSet.toArray(new String[0]));
    return new User(
        sysUser.getUsername(),
        sysUser.getPassword(),
        authorityList
    );
}

Note: Here, SysPermission::getUrl is added to the user's permissions.

  • Modify AccessManager for Permission Checking
@Autowired
private AccessManager accessManager;

@Bean
SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception {
    ...
    http
        .httpBasic().disable()
        .csrf().disable()
        .authorizeExchange()
        .pathMatchers(HttpMethod.OPTIONS).permitAll()
        .anyExchange().access(accessManager)
    ...
    return http.build();
}

In the gateway configuration, we injected a custom ReactiveAuthorizationManager for permission checking. We need to implement logic to match the request path against the user’s accessible resource paths. If a match exists, forward to the backend service; otherwise, return "no permission".

@Slf4j
@Component
public class AccessManager implements ReactiveAuthorizationManager<AuthorizationContext> {
    private Set<String> permitAll = new ConcurrentHashSet<>();
    private static final AntPathMatcher antPathMatcher = new AntPathMatcher();

    public AccessManager() {
        permitAll.add("/");
        permitAll.add("/error");
        permitAll.add("/favicon.ico");
        // If Swagger debugging is enabled in production
        permitAll.add("/**/v2/api-docs/**");
        permitAll.add("/**/swagger-resources/**");
        permitAll.add("/webjars/**");
        permitAll.add("/doc.html");
        permitAll.add("/swagger-ui.html");
        permitAll.add("/**/oauth/**");
    }

    /**
     * Perform permission validation
     */
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authenticationMono, AuthorizationContext authorizationContext) {
        ServerWebExchange exchange = authorizationContext.getExchange();
        // Requested resource
        String requestPath = exchange.getRequest().getURI().getPath();
        // Check if should be permitted directly
        if (permitAll(requestPath)) {
            return Mono.just(new AuthorizationDecision(true));
        }

        return authenticationMono.map(auth -> {
            return new AuthorizationDecision(checkAuthorities(auth, requestPath));
        }).defaultIfEmpty(new AuthorizationDecision(false));
    }

    /**
     * Check if the request path is a static resource
     *
     * @param requestPath Request path
     * @return true if permitted
     */
    private boolean permitAll(String requestPath) {
        return permitAll.stream()
            .filter(r -> antPathMatcher.match(r, requestPath))
            .findFirst().isPresent();
    }

    /**
     * Check permissions
     *
     * @param auth User authorities
     * @param requestPath Request path
     * @return true if authorized
     */
    private boolean checkAuthorities(Authentication auth, String requestPath) {
        if (auth instanceof OAuth2Authentication) {
            Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
            return authorities.stream()
                .map(GrantedAuthority::getAuthority)
                .filter(item -> !item.startsWith(CloudConstant.ROLE_PREFIX))
                .anyMatch(permission -> antPathMatcher.match(permission, requestPath));
        }
        return false;
    }
}
  • Testing

    • View all permissions for the current user
    • Request resources within permission scope
    • Attempt to access unauthorized resources

Method-level Interception Implementation

This method uses Spring Security's built-in annotation @PreAuthorize, with a custom validation method hasPrivilege() for implementation. As noted, there are various ways to achieve this, and this approach is just one option.

This approach requires code logic to be implemented in the resource server (i.e., the backend service providing business logic). Since each backend service needs to include this logic, its recommended to extract a common starter module for reuse across services.

  • Modify UserDetailsService

Similar to above, but here we place resource identifiers into the user's permissions instead of URLs.

sysUser.setPermissions(
    permissionList.stream()
        .map(SysPermission::getPermission)
        .collect(Collectors.toList())
);
  • Remove Gateway Interception Configuration

Since no gateway interception is needed, remove the access validation logic in AccessManager and always return true.

  • Custom Method Validation Logic
/**
 * Custom permission validation
 *
 * @author mx
 */
class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
    private static final AntPathMatcher antPathMatcher = new AntPathMatcher();

    public CustomMethodSecurityExpressionRoot(Authentication authentication) {
        super(authentication);
    }

    private Object filterObject;
    private Object returnObject;

    public boolean hasPrivilege(String permission) {
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        return authorities.stream()
            .map(GrantedAuthority::getAuthority)
            .filter(item -> !item.startsWith(CloudConstant.ROLE_PREFIX))
            .anyMatch(x -> antPathMatcher.match(x, permission));
    }
    ...
}
  • Custom Method Interception Handler
/**
 * @author mx
 */
class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {

    private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();

    @Override
    protected MethodSecurityExpressionOperations createSecurityExpressionRoot(
        Authentication authentication, MethodInvocation invocation) {
        CustomMethodSecurityExpressionRoot root = new CustomMethodSecurityExpressionRoot(authentication);
        root.setPermissionEvaluator(getPermissionEvaluator());
        root.setTrustResolver(this.trustResolver);
        root.setRoleHierarchy(getRoleHierarchy());
        return root;
    }
}
  • Enable Method-Level Security
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        CustomMethodSecurityExpressionHandler expressionHandler = new CustomMethodSecurityExpressionHandler();
        return expressionHandler;
    }
}
  • Add Annotation to Methods Requiring Permissions
@ApiOperation("select interface")
@GetMapping("/account/getByCode/{accountCode}")
@PreAuthorize("hasPrivilege('queryAccount')")
public ResultData<AccountDTO> getByCode(@PathVariable(value = "accountCode") String accountCode){
    log.info("get account detail, accountCode is :{}", accountCode);
    AccountDTO accountDTO = accountService.selectByCode(accountCode);
    return ResultData.success(accountDTO);
}
  • Testing

    • Debug shows that the user permissions retrieved are resource identifiers from the resource table.

Summary

In my opinion, one of the most complex modules in Spring Cloud microservice architecture is the authentication and authorization module. This article addresses the authorization problem using two methods, solving the question of "what you can do". Based on actual business scenarios, you can choose either implementation method. Personally, I recommend the first approach using path-based authorization, which only requires interception at the gateway layer.

Tags: Spring Cloud

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.