Integrating RBAC Authorization in Spring Cloud Alibaba Microservices
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::getUrlis 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.