本文介紹的spring security動態配置url權限認證基于的是spring-boot-2.0.0、spring-security 5.X來編寫的。
筆者瀏覽完spring security官方文檔之后,發現并沒有詳細的介紹說明如何動態的配置我們的url權限認證。spring security默認的權限配置確只會在啟動工程的時候初始化一次url權限配置。但是實際情況我們項目的權限會隨時動態的更改,這樣我們就需要重新啟動項目以便新配置的權限生效。這樣的處理顯然不合理。當然spring是具有非常好的拓展性,我們就抓主spring的這個特性,模仿默認的認證方式來拓展我們需要的認證規則。
在spring security的官方文檔的Spring Security FAQ里有這么一個問題解答44.4.6. How do I define the secured URLs within an application dynamically?
這里的解答非常重要
1.需要提供認證數據規則源數據(類似默認配置在代碼里的url權限數據)
2.自定義一個攔截器然后把它添加到spring security的filterChain中
通過這兩個主要思路,在逐步通過來代碼來實現我們的動態url權限配置
具體步驟
1.配置認證數據源
public class MyFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private final Map<RequestMatcher, Collection<ConfigAttribute>> requestMap;
/*
* 這個例子放在構造方法里初始化url權限數據,我們只要保證在 getAttributes()之前初始好數據就可以了
*/
public MyFilterSecurityMetadataSource() {
Map<RequestMatcher, Collection<ConfigAttribute>> map = new HashMap<>();
AntPathRequestMatcher matcher = new AntPathRequestMatcher("/home");
SecurityConfig config = new SecurityConfig("ROLE_ADMIN");
ArrayList<ConfigAttribute> configs = new ArrayList<>();
configs.add(config);
map.put(matcher,configs);
AntPathRequestMatcher matcher2 = new AntPathRequestMatcher("/");
SecurityConfig config2 = new SecurityConfig("ROLE_ADMIN");
ArrayList<ConfigAttribute> configs2 = new ArrayList<>();
configs2.add(config2);
map.put(matcher2,configs2);
this.requestMap = map;
}
/**
* 在我們初始化的權限數據中找到對應當前url的權限數據
*
* @param object
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
FilterInvocation fi = (FilterInvocation) object;
HttpServletRequest request = fi.getRequest();
String url = fi.getRequestUrl();
String httpMethod = fi.getRequest().getMethod();
// Lookup your database (or other source) using this information and populate the
// list of attributes (這里初始話你的權限數據)
//List<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>();
//遍歷我們初始化的權限數據,找到對應的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);
}
這里使用不了WebExpressionConfigAttribute這個類配置ConfigAttribute,應為它只能在package中使用,這也導致了在認證管理器中,我們不能使用對應WebExpressionVoter來解析我們使用的SecurityConfig加載的數據。具體原因如下:
這里做了一個類的實例檢查。所以在后面使用了RoleVoter來做認證數據的解析。
2.自定義動態數據攔截器
public class DynamicallyUrlInterceptor extends AbstractSecurityInterceptor implements Filter {
//標記自定義的url攔截器已經加載
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);
}
}
這個類參考的是FilterSecurityInterceptor這個類寫的,只是修改了FILTER_APPLIED的值。主要是重寫了父類的方法,添加認證數據源值的設置。使父類在調用方法是能找到對應的數據。
3.提供一個認證管理器
public class DynamicallyUrlAccessDecisionManager extends AbstractAccessDecisionManager {
public DynamicallyUrlAccessDecisionManager(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默認提供了
有這么幾種具體的認證決策,這里最終使用的是RoleVoter這個決策認證類。
4.配置filter
@Bean
public DynamicallyUrlInterceptor dynamicallyUrlInterceptor(){
DynamicallyUrlInterceptor interceptor = new DynamicallyUrlInterceptor();
interceptor.setSecurityMetadataSource(new MyFilterSecurityMetadataSource());
//配置RoleVoter決策
List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<AccessDecisionVoter<? extends Object>>();
decisionVoters.add(new RoleVoter());
//設置認證決策管理器
interceptor.setAccessDecisionManager(new DynamicallyUrlAccessDecisionManager(decisionVoters));
return interceptor;
}
然后在把我們的filter添加到spring security的filter中
附上spring security的fiter信息的連接
https://docs.spring.io/spring-security/site/docs/4.2.4.RELEASE/reference/htmlsingle/#ns-custom-filters
最后,本例子只是技術探討。具體使用這種動態的url驗證機制是否合適,還需要結合實際情況來分析。
項目地址:https://gitee.com/longguiyunjosh/spring-security-dynamically-demo
參考文章:
https://segmentfault.com/a/1190000010672041
https://www.cnblogs.com/visoncheng/p/3335768.html