Two methods of dynamically configuring URL permissions by spring security
origin
In standard RABC, permissions need to support dynamic configuration. Spring security specifies permissions in the code by default. In real business scenarios, role access permissions usually need to support dynamic configuration, that is, configure the access role corresponding to the URL at runtime.
Based on spring security, how to implement this requirement?
The simplest way is to customize a filter to complete permission judgment, but this is divorced from the spring security framework. How to implement it gracefully based on spring security?
Spring security authorization review
Spring security uses filterchainproxy as a filter registered to the web. Filterchainproxy contains multiple built-in filters at a time. First, we need to understand the various built-in filters of spring security:
The most important is the filtersecurityinterceptor, which implements the main authentication logic. The core code is here:
protected InterceptorStatusToken beforeInvocation(Object object) {
// 获取访问URL所需权限
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
Authentication authenticated = authenticateIfrequired();
// 通过accessDecisionManager鉴权
try {
this.accessDecisionManager.decide(authenticated,object,attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object,attributes,authenticated,accessDeniedException));
throw accessDeniedException;
}
if (debug) {
logger.debug("Authorization successful");
}
if (publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object,authenticated));
}
// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildrunAs(authenticated,attributes);
if (runAs == null) {
if (debug) {
logger.debug("RunAsManager did not change Authentication object");
}
// no further work post-invocation
return new InterceptorStatusToken(SecurityContextHolder.getContext(),false,object);
}
else {
if (debug) {
logger.debug("Switching to RunAs Authentication: " + runAs);
}
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
// need to revert to token.Authenticated post-invocation
return new InterceptorStatusToken(origCtx,true,object);
}
}
It can be seen from the above that to realize dynamic authentication, we can start from two aspects:
Let's see how to implement them.
Customize accessdecisionmanager
The three official accessdecisionmanagers implement authority authentication based on accessdecisionvoter, so we only need to customize one accessdecisionvoter.
Customization mainly implements the accessdecisionvoter interface. We can follow the official rolevoter to implement one:
public class RoleBasedVoter implements AccessDecisionVoter<Object> {
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public int Vote(Authentication authentication,Object object,Collection<ConfigAttribute> attributes) {
if(authentication == null) {
return ACCESS_DENIED;
}
int result = ACCESS_ABSTAIN;
Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);
for (ConfigAttribute attribute : attributes) {
if(attribute.getAttribute()==null){
continue;
}
if (this.supports(attribute)) {
result = ACCESS_DENIED;
// Attempt to find a matching granted authority
for (GrantedAuthority authority : authorities) {
if (attribute.getAttribute().equals(authority.getAuthority())) {
return ACCESS_GRANTED;
}
}
}
}
return result;
}
Collection<? extends GrantedAuthority> extractAuthorities(
Authentication authentication) {
return authentication.getAuthorities();
}
@Override
public boolean supports(Class clazz) {
return true;
}
}
How to add dynamic permissions?
The type of object in vote (authentication, collection < configattribute > attributes) is filterinvocation. You can get the URL of the current request through getrequesturl:
FilterInvocation fi = (FilterInvocation) object;
String url = fi.getRequestUrl();
Therefore, there is a large expansion space here. You can load it dynamically from the DB, and then judge the configattribute of the URL.
How do I use this rolebased voter? Use the accessdecisionmanager method in configure to customize. We still use the official unanimusbased, and then add the customized rolebasedvoter.
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(corsFilter,UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.and()
.csrf()
.disable()
.headers()
.frameOptions()
.disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 自定义accessDecisionManager
.accessDecisionManager(accessDecisionManager())
.and()
.apply(securityConfigurerAdapter());
}
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<? extends Object>> decisionVoters
= Arrays.asList(
new WebExpressionVoter(),// new RoleVoter(),new RoleBasedVoter(),new AuthenticatedVoter());
return new UnanimousBased(decisionVoters);
}
Custom securitymetadatasource
Custom filterinvocationsecuritymetadatasource can only implement the interface, and dynamically load rules from DB in the interface.
In order to reuse the definitions in the code, we can bring the securitymetadatasource generated in the code and pass in the default filterinvocationsecuritymetadatasource in the constructor.
public class AppFilterInvocationSecurityMetadataSource implements org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource {
private FilterInvocationSecurityMetadataSource superMetadataSource;
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
public AppFilterInvocationSecurityMetadataSource(FilterInvocationSecurityMetadataSource expressionBasedFilterInvocationSecurityMetadataSource){
this.superMetadataSource = expressionBasedFilterInvocationSecurityMetadataSource;
// TODO 从数据库加载权限配置
}
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
// 这里的需要从DB加载
private final Map<String,String> urlRoleMap = new HashMap<String,String>(){{
put("/open/**","ROLE_ANONYMOUS");
put("/health","ROLE_ANONYMOUS");
put("/restart","ROLE_ADMIN");
put("/demo","ROLE_USER");
}};
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
FilterInvocation fi = (FilterInvocation) object;
String url = fi.getRequestUrl();
for(Map.Entry<String,String> entry:urlRoleMap.entrySet()){
if(antPathMatcher.match(entry.getKey(),url)){
return SecurityConfig.createList(entry.getValue());
}
}
// 返回代码定义的默认配置
return superMetadataSource.getAttributes(object);
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
How do you use it? Unlike the accessdecisionmanager, the expressionurlauthorizationconfigurator does not provide a set method to set the filterinvocationsecuritymetadatasource of the filtersecurityinterceptor, how to do?
Find an extension method withobjectpostprocessor. You can modify the filtersecurityinterceptor by customizing an objectpostprocessor that handles the filtersecurityinterceptor type through this method.
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.and()
.csrf()
.disable()
.headers()
.frameOptions()
.disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 自定义FilterInvocationSecurityMetadataSource
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(
O fsi) {
fsi.setSecurityMetadataSource(mySecurityMetadataSource(fsi.getSecurityMetadataSource()));
return fsi;
}
})
.and()
.apply(securityConfigurerAdapter());
}
@Bean
public AppFilterInvocationSecurityMetadataSource mySecurityMetadataSource(FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource) {
AppFilterInvocationSecurityMetadataSource securityMetadataSource = new AppFilterInvocationSecurityMetadataSource(filterInvocationSecurityMetadataSource);
return securityMetadataSource;
}
Summary
This paper introduces two methods to implement dynamic permissions based on spring security, one is to customize accessdecision manager, the other is to customize filterinvocationsecuritymetadatasource. You can choose flexibly according to your needs in the actual project.
Extended reading:
Spring security architecture and source code analysis