spring security动态配置url权限的2种实现方法
缘起
标准的RABC,权限需要支持动态配置,springsecurity默认是在代码里约定好权限,真实的业务场景通常需要可以支持动态配置角色访问权限,即在运行时去配置url对应的访问角色。
基于springsecurity,如何实现这个需求呢?
最简单的方法就是自定义一个Filter去完成权限判断,但这脱离了springsecurity框架,如何基于springsecurity优雅的实现呢?
springsecurity授权回顾
springsecurity通过FilterChainProxy作为注册到web的filter,FilterChainProxy里面一次包含了内置的多个过滤器,我们首先需要了解springsecurity内置的各种filter:
| Alias | FilterClass | NamespaceElementorAttribute |
|---|---|---|
| CHANNEL_FILTER | ChannelProcessingFilter | http/intercept-url@requires-channel |
| SECURITY_CONTEXT_FILTER | SecurityContextPersistenceFilter | http |
| CONCURRENT_SESSION_FILTER | ConcurrentSessionFilter | session-management/concurrency-control |
| HEADERS_FILTER | HeaderWriterFilter | http/headers |
| CSRF_FILTER | CsrfFilter | http/csrf |
| LOGOUT_FILTER | LogoutFilter | http/logout |
| X509_FILTER | X509AuthenticationFilter | http/x509 |
| PRE_AUTH_FILTER | AbstractPreAuthenticatedProcessingFilterSubclasses | N/A |
| CAS_FILTER | CasAuthenticationFilter | N/A |
| FORM_LOGIN_FILTER | UsernamePasswordAuthenticationFilter | http/form-login |
| BASIC_AUTH_FILTER | BasicAuthenticationFilter | http/http-basic |
| SERVLET_API_SUPPORT_FILTER | SecurityContextHolderAwareRequestFilter | http/@servlet-api-provision |
| JAAS_API_SUPPORT_FILTER | JaasApiIntegrationFilter | http/@jaas-api-provision |
| REMEMBER_ME_FILTER | RememberMeAuthenticationFilter | http/remember-me |
| ANONYMOUS_FILTER | AnonymousAuthenticationFilter | http/anonymous |
| SESSION_MANAGEMENT_FILTER | SessionManagementFilter | session-management |
| EXCEPTION_TRANSLATION_FILTER | ExceptionTranslationFilter | http |
| FILTER_SECURITY_INTERCEPTOR | FilterSecurityInterceptor | http |
| SWITCH_USER_FILTER | SwitchUserFilter | N/A |
最重要的是FilterSecurityInterceptor,该过滤器实现了主要的鉴权逻辑,最核心的代码在这里:
protectedInterceptorStatusTokenbeforeInvocation(Objectobject){
//获取访问URL所需权限
Collectionattributes=this.obtainSecurityMetadataSource()
.getAttributes(object);
Authenticationauthenticated=authenticateIfRequired();
//通过accessDecisionManager鉴权
try{
this.accessDecisionManager.decide(authenticated,object,attributes);
}
catch(AccessDeniedExceptionaccessDeniedException){
publishEvent(newAuthorizationFailureEvent(object,attributes,authenticated,
accessDeniedException));
throwaccessDeniedException;
}
if(debug){
logger.debug("Authorizationsuccessful");
}
if(publishAuthorizationSuccess){
publishEvent(newAuthorizedEvent(object,attributes,authenticated));
}
//Attempttorunasadifferentuser
AuthenticationrunAs=this.runAsManager.buildRunAs(authenticated,object,
attributes);
if(runAs==null){
if(debug){
logger.debug("RunAsManagerdidnotchangeAuthenticationobject");
}
//nofurtherworkpost-invocation
returnnewInterceptorStatusToken(SecurityContextHolder.getContext(),false,
attributes,object);
}
else{
if(debug){
logger.debug("SwitchingtoRunAsAuthentication:"+runAs);
}
SecurityContextorigCtx=SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
//needtoreverttotoken.Authenticatedpost-invocation
returnnewInterceptorStatusToken(origCtx,true,attributes,object);
}
}
从上面可以看出,要实现动态鉴权,可以从两方面着手:
- 自定义SecurityMetadataSource,实现从数据库加载ConfigAttribute
- 另外就是可以自定义accessDecisionManager,官方的UnanimousBased其实足够使用,并且他是基于AccessDecisionVoter来实现权限认证的,因此我们只需要自定义一个AccessDecisionVoter就可以了
下面来看分别如何实现。
自定义AccessDecisionManager
官方的三个AccessDecisionManager都是基于AccessDecisionVoter来实现权限认证的,因此我们只需要自定义一个AccessDecisionVoter就可以了。
自定义主要是实现AccessDecisionVoter接口,我们可以仿照官方的RoleVoter实现一个:
publicclassRoleBasedVoterimplementsAccessDecisionVoter
如何加入动态权限呢?
vote(Authenticationauthentication,Objectobject,Collection
FilterInvocationfi=(FilterInvocation)object; Stringurl=fi.getRequestUrl();
因此这里扩展空间就大了,可以从DB动态加载,然后判断URL的ConfigAttribute就可以了。
如何使用这个RoleBasedVoter呢?在configure里使用accessDecisionManager方法自定义,我们还是使用官方的UnanimousBased,然后将自定义的RoleBasedVoter加入即可。
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true)
publicclassSecurityConfigurationextendsWebSecurityConfigurerAdapter{
@Override
protectedvoidconfigure(HttpSecurityhttp)throwsException{
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
publicAccessDecisionManageraccessDecisionManager(){
List>decisionVoters
=Arrays.asList(
newWebExpressionVoter(),
//newRoleVoter(),
newRoleBasedVoter(),
newAuthenticatedVoter());
returnnewUnanimousBased(decisionVoters);
}
自定义SecurityMetadataSource
自定义FilterInvocationSecurityMetadataSource只要实现接口即可,在接口里从DB动态加载规则。
为了复用代码里的定义,我们可以将代码里生成的SecurityMetadataSource带上,在构造函数里传入默认的FilterInvocationSecurityMetadataSource。
publicclassAppFilterInvocationSecurityMetadataSourceimplementsorg.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource{
privateFilterInvocationSecurityMetadataSourcesuperMetadataSource;
@Override
publicCollectiongetAllConfigAttributes(){
returnnull;
}
publicAppFilterInvocationSecurityMetadataSource(FilterInvocationSecurityMetadataSourceexpressionBasedFilterInvocationSecurityMetadataSource){
this.superMetadataSource=expressionBasedFilterInvocationSecurityMetadataSource;
//TODO从数据库加载权限配置
}
privatefinalAntPathMatcherantPathMatcher=newAntPathMatcher();
//这里的需要从DB加载
privatefinalMapurlRoleMap=newHashMap(){{
put("/open/**","ROLE_ANONYMOUS");
put("/health","ROLE_ANONYMOUS");
put("/restart","ROLE_ADMIN");
put("/demo","ROLE_USER");
}};
@Override
publicCollectiongetAttributes(Objectobject)throwsIllegalArgumentException{
FilterInvocationfi=(FilterInvocation)object;
Stringurl=fi.getRequestUrl();
for(Map.Entryentry:urlRoleMap.entrySet()){
if(antPathMatcher.match(entry.getKey(),url)){
returnSecurityConfig.createList(entry.getValue());
}
}
//返回代码定义的默认配置
returnsuperMetadataSource.getAttributes(object);
}
@Override
publicbooleansupports(Class>clazz){
returnFilterInvocation.class.isAssignableFrom(clazz);
}
}
怎么使用?和accessDecisionManager不一样,ExpressionUrlAuthorizationConfigurer并没有提供set方法设置FilterSecurityInterceptor的FilterInvocationSecurityMetadataSource,howtodo?
发现一个扩展方法withObjectPostProcessor,通过该方法自定义一个处理FilterSecurityInterceptor类型的ObjectPostProcessor就可以修改FilterSecurityInterceptor。
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true)
publicclassSecurityConfigurationextendsWebSecurityConfigurerAdapter{
@Override
protectedvoidconfigure(HttpSecurityhttp)throwsException{
http
.addFilterBefore(corsFilter,UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.and()
.csrf()
.disable()
.headers()
.frameOptions()
.disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//自定义FilterInvocationSecurityMetadataSource
.withObjectPostProcessor(newObjectPostProcessor(){
@Override
publicOpostProcess(
Ofsi){
fsi.setSecurityMetadataSource(mySecurityMetadataSource(fsi.getSecurityMetadataSource()));
returnfsi;
}
})
.and()
.apply(securityConfigurerAdapter());
}
@Bean
publicAppFilterInvocationSecurityMetadataSourcemySecurityMetadataSource(FilterInvocationSecurityMetadataSourcefilterInvocationSecurityMetadataSource){
AppFilterInvocationSecurityMetadataSourcesecurityMetadataSource=newAppFilterInvocationSecurityMetadataSource(filterInvocationSecurityMetadataSource);
returnsecurityMetadataSource;
}
小结
本文介绍了两种基于springsecurity实现动态权限的方法,一是自定义accessDecisionManager,二是自定义FilterInvocationSecurityMetadataSource。实际项目里可以根据需要灵活选择。
延伸阅读:
SpringSecurity架构与源码分析
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。