Spring Security: Define secured URLS dynamically

After browsing the official document of Spring Security, I found there is no such a solution to help with dynamically configure url authorization. By default, Spring Security will initialize the url authorization as the applilcation starts.

There is a question in the document 44.4.6. How do I define the secured URLs within an application dynamically?. After reading the solution to this problem, I draw a solution with two steps:

  1. Provide data which describes the rules of access.
    SecurityMetadataSource obtains the metadata for a particular method or filter invocation
  2. Customize an interceptor and add it into the Spring Security’s filterChain.

Authorization Datasource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* Config authentication data source to implement dynamically authorization load
*/
@Component
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
// Authorization data
private Map<RequestMatcher, Collection<ConfigAttribute>> requestMap;

/**
* Find the authorization data according to current url in the initial authorization datasource
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
FilterInvocation fi = (FilterInvocation) object;
HttpServletRequest request = fi.getRequest();

// traverse the initialized authorization data and find authorization according to current url
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
.entrySet()) {
if (entry.getKey().matches(request)) {
return entry.getValue();
}
}
return null;
}

@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}

@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}

/**
* Update authorization collection
*/
public void setRequestMap(List<SysAuthorityVo> authorityVoList){
Map<RequestMatcher, Collection<ConfigAttribute>> map = new ConcurrentHashMap<>();
for (SysAuthorityVo sysAuthorityVo : authorityVoList) {
String authorityName = sysAuthorityVo.getAuthorityName();
if (StringUtils.isEmpty(sysAuthorityVo.getAuthorityContent())) continue;
for (String url : sysAuthorityVo.getAuthorityContent().split(",")) {
Collection<ConfigAttribute> value = map.get(new AntPathRequestMatcher(url));
if (StringUtils.isEmpty(value)) {
ArrayList<ConfigAttribute> configs = new ArrayList<>();
configs.add(new SecurityConfig(authorityName));
map.put(new AntPathRequestMatcher(url), configs);
} else {
value.add(new SecurityConfig(authorityName));
}
}
}
this.requestMap = map;
}
}

Customize Dynamically Url Interceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* Customize DynamicallyUrlInterceptor
*/
public class DynamicallyUrlInterceptor extends AbstractSecurityInterceptor implements Filter {

// label customized url interceptor has already been loaded
private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied_dynamically";

private FilterInvocationSecurityMetadataSource securityMetadataSource;
private boolean observeOncePerRequest = true;


@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void destroy() {
}

public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
return this.securityMetadataSource;
}

public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}

public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
this.securityMetadataSource = newSource;
}

@Override
public void setAccessDecisionManager(AccessDecisionManager accessDecisionManager) {
super.setAccessDecisionManager(accessDecisionManager);
}

public void invoke(FilterInvocation fi) throws IOException, ServletException {

if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}

InterceptorStatusToken token = super.beforeInvocation(fi);

try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}

super.afterInvocation(token, null);
}
}
}

Decision Manager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* Decision Manager
*/
public class MyAccessDecisionManager extends AbstractAccessDecisionManager {

MyAccessDecisionManager(List<AccessDecisionVoter<?>> decisionVoters) {
super(decisionVoters);
}

@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
int deny = 0;

for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);

if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}

switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;

case AccessDecisionVoter.ACCESS_DENIED:
deny++;

break;

default:
break;
}
}

if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}

// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
}

Spring Security provided several authorization strategy, here we are using RoleVoter.

Configure filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    //filter
@Bean
public DynamicallyUrlInterceptor dynamicallyUrlInterceptor(){
// Retrieve the datasource
List<SysAuthorityVo> authorityVoList = sysAuthorityService.list(new SysAuthorityVo()).getData();
myFilterInvocationSecurityMetadataSource.setRequestMap(authorityVoList);
// Initialize interceptor and load the datasource
DynamicallyUrlInterceptor interceptor = new DynamicallyUrlInterceptor();
interceptor.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);

// Configure RoleVoter Strategy
List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<>();
decisionVoters.add(new RoleVoter());

// Set Access Decision Manager
interceptor.setAccessDecisionManager(new MyAccessDecisionManager(decisionVoters));
return interceptor;
}
}

Add filter into Spring Security’s Filter

1
2
3
4
5
6
7
8
9
10
11
12
http
// customize the url access authority and read the authentication dynamically

.addFilterAfter(dynamicallyUrlInterceptor(), FilterSecurityInterceptor.class)
.authorizeRequests()

// permit all which means no limitation against authority
.antMatchers("/favicon.ico","/common/**", "/webjars/**", "/getVerifyCodeImage","/error/*").permitAll()

// other interfaces are accessible only after login
.anyRequest().authenticated()
.and();