There are several ways to do what you need:
Develop your custom MethodSecurityExpressionOperations
In this tutorial you will see how to deal with a new custom security method (section 5) or override the current hasAuthority
one (section 6)
Develop your custom method to use in SpEL
Probably an esier option, the steps could be the following ones:
1. Include the allowed roles in your application.yml
(or properties
)
security:
rolesAllowed: ADMIN,USER
2. Define the class to check those roles and authorized user ones. For example:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toSet;
@Component
public class FromPropertyRoleSecurityCheck {
private final static String ROLE_SEPARATOR = ",";
@Value("${security.rolesAllowed}")
private String rawRolesAllowed;
public boolean verifyRoles() {
return getPrincipalAuthorities()
.map(auth -> {
Set<String> rolesAllowed = Stream.of(rawRolesAllowed.split(ROLE_SEPARATOR))
.map(String::trim)
.collect(toSet());
return verifyAllowedRoles(rolesAllowed, auth);
})
.orElse(false);
}
private Optional<Collection<? extends GrantedAuthority>> getPrincipalAuthorities() {
return ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.map(Authentication::getAuthorities);
}
private boolean verifyAllowedRoles(final Collection<String> rolesAllowed,
final Collection<? extends GrantedAuthority> principalAuthorities) {
if (CollectionUtils.isEmpty(rolesAllowed)) {
return true;
}
if (CollectionUtils.isEmpty(principalAuthorities)) {
return false;
}
Set<String> rolesDiff = principalAuthorities.stream().map(GrantedAuthority::getAuthority).collect(toSet());
rolesDiff.removeAll(rolesAllowed);
return rolesDiff.size() != principalAuthorities.size();
}
}
3. Add the security check:
@PreAuthorize("@fromPropertyRoleSecurityCheck.verifyRoles()")
public ResponseEntity<MyDto> findById(@PathVariable @Positive Integer id) {
...
}
If you don't want to recompile/deploy the project every time those roles change, you can save them in an external storage like database for example (shouldn't be a problem to update any of provided examples to deal with such situations). In the second one I used a property to keep it simple, but is quite easy to include a Repository
in FromPropertyRoleSecurityCheck
to get them from database.
PD. Examples of provided link and custom one were developed in Controller layer, but they should work in the Service one too.