缘起

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

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

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

spring security 授权回顾

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

Alias Filter Class Namespace Element or Attribute
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 AbstractPreAuthenticatedProcessingFilter Subclasses 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,该过滤器实现了主要的鉴权逻辑,最核心的代码在这里:

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方法设置FilterSecurityInterceptorFilterInvocationSecurityMetadataSource,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 架构与源码分析


作者:Jadepeng

出处:jqpeng的技术记事本--http://www.cnblogs.com/xiaoqi

您的支持是对博主最大的鼓励,感谢您的认真阅读。

本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

spring security实现动态配置url权限的两种方法的更多相关文章

  1. Spring Security之动态配置资源权限

    在Spring Security中实现通过数据库动态配置url资源权限,需要通过配置验证过滤器来实现资源权限的加载.验证.系统启动时,到数据库加载系统资源权限列表,当有请求访问时,通过对比系统资源权限 ...

  2. 配置 yum 源的两种方法

    配置 yum 源的两种方法 由于 redhat的yum在线更新是收费的,如果没有注册的话不能使用,如果要使用,需将redhat的yum卸载后,重启安装,再配置其他源,以下为详细过程:  1.删除red ...

  3. spring 配置文件 引入外部的property文件的两种方法

    spring  的配置文件 引入外部的property文件的两种方法 <!-- 引入jdbc配置文件    方法一 --> <bean id="propertyConfig ...

  4. DataGridView动态添加新行的两种方法

    简单介绍如何为DataGridView控件动态添加新行的两种方 法: 方法一: int index=this.dataGridView1.Rows.Add();this.dataGridView1.R ...

  5. [转]Spring Security 可动态授权RBAC权限模块实践

    RBAC:基于角色的访问控制(Role-Based Access Control) 先在web.xml 中配置一个过滤器(必须在Struts的过滤器之前) <filter> <fil ...

  6. spring security实现简单的url权限拦截

    在一个系统中,权限的拦截是很常见的事情,通常情况下我们都是基于url进行拦截.那么在spring security中应该怎么配置呢. 大致步骤如下: 1.用户登录成功后我们需要拿到用户所拥有的权限,并 ...

  7. spring mvc 防止重复提交表单的两种方法,推荐第二种

    第一种方法:判断session中保存的token 比较麻烦,每次在提交表单时都必须传入上次的token.而且当一个页面使用ajax时,多个表单提交就会有问题. 注解Token代码: package c ...

  8. spring boot:解决cors跨域问题的两种方法(spring boot 2.3.2)

    一,什么是CORS? 1,CORS(跨域资源共享)(CORS,Cross-origin resource sharing), 它是一个 W3C 标准中浏览器技术的规范, 它允许浏览器向非同一个域的服务 ...

  9. javascript 获取当前 URL 参数的两种方法

    window.location.host; //返回url 的主机部分,例如:www.xxx.com window.location.hostname; //返回www.xxx.com window. ...

随机推荐

  1. [转]JS根据useAgent来判断edge, ie, firefox, chrome, opera, safari 等浏览器的类型及版本

    js根据浏览器的useAgent来判断浏览器的类型 userAgent 属性是一个只读的字符串,声明了浏览器用于 HTTP 请求的用户代理头的值. JavaScript语法:navigator.use ...

  2. 【转】Java并发编程:并发容器之ConcurrentHashMap

    JDK5中添加了新的concurrent包,相对同步容器而言,并发容器通过一些机制改进了并发性能.因为同步容器将所有对容器状态的访问都串行化了,这样保证了线程的安全性,所以这种方法的代价就是严重降低了 ...

  3. struts2框架之文件下载(参考第三天学习笔记)

    下载 1. 下载是一种响应方式 正常的响应:响应正文是html:response.getWriter().print("html"); 下载的响应: 1.一个流:字节数据:resp ...

  4. struts2框架之拦截器(参考第二天学习笔记)

    拦截器 1. 什么是拦截器 1). 与JavaWeb中的Filter比较相似. 2). 拦截器只能拦截Action!!! 2. Struts中定义了很多拦截器,其中defaultStack中的拦截器会 ...

  5. spring集成cxf实现webservice接口功能

    由于cxf的web项目已经集成了Spring,所以cxf的服务类都是在spring的配置文件中完成的.以下是步骤:第一步:建立一个web项目.第二步:准备所有jar包.将cxf_home\lib项目下 ...

  6. Django ----- 模板2

    tags for <ul> {% for user in user_list %} <li>{{ user.name }}</li> {% endfor %} #结 ...

  7. 【原创】大叔经验分享(32)docker挂载文件修改生效

    docker经常需要挂载文件到容器中,比如启动nginx # docker run -d --name test_nginx -v /tmp/nginx.conf:/etc/nginx/nginx.c ...

  8. appium+java(五)微信小程序自动化测试实践

    前言: 上一篇<appium+java(四)微信公众号自动化测试实践>中,尝试使用appium实现微信公众号自动化测试,接着尝试小程序自动化,以学院小程序为例 准备工作 1.java-cl ...

  9. 关于KEIL编译报错和警告问题

    编译时候报错.. Library reports error: __use_no_semihosting was requested, but _ttywrch was referenced 上网找了 ...

  10. Confluence 6 Microsoft SQL Server 设置准备

    在开始前,请检查: 请查看 Supported Platforms 页面来获得 Confluence 系统支持的 SQL Server 数据库版本.你需要在安装 Confluence 之前升级你的 O ...