spring security动态配置url权限的2种实现方法

作者:JadePeng 时间:2021-06-25 15:31:12 

缘起

标准的RABC, 权限需要支持动态配置,spring security默认是在代码里约定好权限,真实的业务场景通常需要可以支持动态配置角色访问权限,即在运行时去配置url对应的访问角色。

基于spring security,如何实现这个需求呢?

最简单的方法就是自定义一个Filter去完成权限判断,但这脱离了spring security框架,如何基于spring security优雅的实现呢?

spring security 授权回顾

spring security 通过FilterChainProxy作为注册到web的filter,FilterChainProxy里面一次包含了内置的多个过滤器,我们首先需要了解spring security内置的各种filter:

AliasFilter ClassNamespace Element or Attribute
CHANNEL_FILTERChannelProcessingFilterhttp/intercept-url@requires-channel
SECURITY_CONTEXT_FILTERSecurityContextPersistenceFilterhttp
CONCURRENT_SESSION_FILTERConcurrentSessionFiltersession-management/concurrency-control
HEADERS_FILTERHeaderWriterFilterhttp/headers
CSRF_FILTERCsrfFilterhttp/csrf
LOGOUT_FILTERLogoutFilterhttp/logout
X509_FILTERX509AuthenticationFilterhttp/x509
PRE_AUTH_FILTERAbstractPreAuthenticatedProcessingFilter SubclassesN/A
CAS_FILTERCasAuthenticationFilterN/A
FORM_LOGIN_FILTERUsernamePasswordAuthenticationFilterhttp/form-login
BASIC_AUTH_FILTERBasicAuthenticationFilterhttp/http-basic
SERVLET_API_SUPPORT_FILTERSecurityContextHolderAwareRequestFilterhttp/@servlet-api-provision
JAAS_API_SUPPORT_FILTERJaasApiIntegrationFilterhttp/@jaas-api-provision
REMEMBER_ME_FILTERRememberMeAuthenticationFilterhttp/remember-me
ANONYMOUS_FILTERAnonymousAuthenticationFilterhttp/anonymous
SESSION_MANAGEMENT_FILTERSessionManagementFiltersession-management
EXCEPTION_TRANSLATION_FILTERExceptionTranslationFilterhttp
FILTER_SECURITY_INTERCEPTORFilterSecurityInterceptorhttp
SWITCH_USER_FILTERSwitchUserFilterN/A

最重要的是FilterSecurityInterceptor,该过滤器实现了主要的鉴权逻辑,最核心的代码在这里:


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, attributes, authenticated));
}

// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
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,
 attributes, 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, attributes, object);
}
}

从上面可以看出,要实现动态鉴权,可以从两方面着手:

  • 自定义SecurityMetadataSource,实现从数据库加载ConfigAttribute

  • 另外就是可以自定义accessDecisionManager,官方的UnanimousBased其实足够使用,并且他是基于AccessDecisionVoter来实现权限认证的,因此我们只需要自定义一个AccessDecisionVoter就可以了

下面来看分别如何实现。

自定义AccessDecisionManager

官方的三个AccessDecisionManager都是基于AccessDecisionVoter来实现权限认证的,因此我们只需要自定义一个AccessDecisionVoter就可以了。

自定义主要是实现AccessDecisionVoter接口,我们可以仿照官方的RoleVoter实现一个:


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;
}
}

如何加入动态权限呢?

vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes)里的Object object的类型是FilterInvocation,可以通过getRequestUrl获取当前请求的URL:


FilterInvocation fi = (FilterInvocation) object;
String url = fi.getRequestUrl();

因此这里扩展空间就大了,可以从DB动态加载,然后判断URL的ConfigAttribute就可以了。

如何使用这个RoleBasedVoter呢?在configure里使用accessDecisionManager方法自定义,我们还是使用官方的UnanimousBased,然后将自定义的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);
}

自定义SecurityMetadataSource

自定义FilterInvocationSecurityMetadataSource只要实现接口即可,在接口里从DB动态加载规则。

为了复用代码里的定义,我们可以将代码里生成的SecurityMetadataSource带上,在构造函数里传入默认的FilterInvocationSecurityMetadataSource。


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);
}
}

怎么使用?和accessDecisionManager不一样,ExpressionUrlAuthorizationConfigurer 并没有提供set方法设置FilterSecurityInterceptor的FilterInvocationSecurityMetadataSource,how to do?

发现一个扩展方法withObjectPostProcessor,通过该方法自定义一个处理FilterSecurityInterceptor类型的ObjectPostProcessor就可以修改FilterSecurityInterceptor。


@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()
 // 自定义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;
}

小结

本文介绍了两种基于spring security实现动态权限的方法,一是自定义accessDecisionManager,二是自定义FilterInvocationSecurityMetadataSource。实际项目里可以根据需要灵活选择。

延伸阅读:

Spring Security 架构与源码分析

来源:http://www.cnblogs.com/xiaoqi/p/spring-security-rabc.html

标签:spring,security,url权限
0
投稿

猜你喜欢

  • 浅谈Synchronized和Lock的区别

    2023-10-26 04:28:33
  • Java数据结构学习之栈和队列

    2022-02-21 11:32:45
  • 如何通过zuul添加或修改请求参数

    2022-08-13 05:05:20
  • C#枚举类型与位域枚举Enum

    2023-03-02 06:52:27
  • Spring定时任务使用及如何使用邮件监控服务器

    2023-01-12 16:38:58
  • java新特性之for循环最全的用法总结

    2022-07-08 22:14:51
  • java后端解决跨域的几种问题解决

    2022-01-05 06:34:24
  • c#窗体传值用法实例详解

    2022-04-04 03:44:15
  • Java类的继承实例详解(动力节点Java学院整理)

    2023-01-28 13:19:31
  • Java中为什么start方法不能重复调用而run方法可以?

    2023-11-15 03:04:02
  • opencv利用鼠标滑动画出多彩的形状

    2023-11-03 05:20:57
  • Flutter加载图片流程MultiFrameImageStreamCompleter解析

    2023-07-19 02:45:55
  • C#使用oledb操作excel文件的方法

    2023-06-13 19:19:42
  • Java代码块与代码加载顺序原理详解

    2023-06-03 12:56:42
  • java实现socket客户端连接服务端

    2021-12-02 03:52:07
  • Java中this和super的区别及this能否调用到父类使用

    2023-01-05 12:03:13
  • 基于java实现租车管理系统

    2022-02-08 12:48:49
  • c#基于opencv,开发摄像头播放程序

    2023-06-20 11:54:29
  • Android编程中避免内存泄露的方法总结

    2023-07-27 19:32:50
  • 分布式医疗挂号系统SpringCache与Redis为数据字典添加缓存

    2023-06-28 02:26:55
  • asp之家 软件编程 m.aspxhome.com