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:
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方法设置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


猜你喜欢
- 本文实例讲述了C#实现json格式转换成对象并更换key的方法。分享给大家供大家参考。具体分析如下:由于是不标准的序列化对象类型,因此你无法
- 此解决方案是针对window的,因为日志默认保存路径在C盘,linux忽略。学习RocketMQ过程中,总是出现com.alibaba.ro
- 一、DurationDuration主要用来衡量秒级和纳秒级的时间,使用于时间精度要求比较高的情况。先来看看Duration的定义:publ
- 队列简介队列是一个有序列表,可以用数组或是链表来实现。遵循先入先出的原则。即先存入队列的数据,先取出,后存入的后取出。示意图:(使用数组模拟
- 本文主要给大家介绍了关于RxJava的一些特殊用法,分享出来供大家参考学习,需要的朋友们下面来一起看看吧。一、按钮绑定通过 RxView 可
- 场景:有一个喜欢吃饺子,他有三种不同的方式去吃,蒸饺子,煮饺子,煎饺子,想要用策略模式来设计这个场景,怎么弄?1.复习简单工厂模式具体的代码
- Http定义了与服务器交互的不同方法,最基本的方法有4种,分别是GET,POST,PUT,DELETE。URL全称是资源描述符,我们可以这样
- 本文总览本篇来学习Kotlin循环结构的知识1. While循环while循环用于重复迭代代码块,只要给定条件为 true就会执行一次循环代
- 在我们对程序进行操作过程中,一般都需要有一个操作流程的记录显示。用C#进行编程时可以很容易实现这个功能。本经验提供案例仅供参考下面小编就来介
- 带参数的try(){}语法含义带资源的try语句(try-with-resource)最简形式为try(Resource res = xxx
- 现在很多应用中都会要求用户上传一张图片来作为头像,首先我在这接收使用相机拍照和在相册中选择图片。接下来先上效果图: 接下来看代码:
- 一、面向对象的描述面向对象是一种现在最为流行的程序设计方法,几乎现在的所有应用都以面向对象为主了,最早的面向对象的概念实际上是由IBM提出的
- 比如定义了一个错误的枚举类型public enum eErrorDetailCode : int &nbs
- 最近一直在对接接口,上游返回的都是 JSON 数据,我们需要将这些数据进行保存,我们可以解析成 Map 通过 key 的方式进行获取,然后
- 一、介绍1.两种方式,一种使用tomcat的websocket实现,一种使用spring的websocket2.tomcat的方式需要tom
- Spring整合mybatis注解扫描是否成功IDEA spring整合mybatis会使用注解扫描的配置<context:compo
- 本文实例讲述了C#分布式事务的超时处理的方法。分享给大家供大家参考。具体分析如下:事务是个很精妙的存在,我们在数据层、服务层、业务逻辑层等多
- 一:工具栏(JToolBar)代码示例:import javax.swing.*;//工具栏的使用案例public class JToolB
- 问题背景实际项目碰到一个上游服务商接口有10秒的查询限制(同个账号)。项目中有一个需求是要实时统计一些数据,一个应用下可能有多个相同的账号。
- 大家都知道NPOI组件可以在你本地没有安装office的情况下来 读取,创建excel文件。但是大家一般都是只默认读取一个excel文件的第